This article is the first part of a five-part series about SOLID as
Rock design principle series. The SOLID design principles focus on
developing software that is easy to maintainable, reusable &
extendable. In this article, we will see an example of the Single Responsibility Principle in C++ along with its benefits & generic guideline.
Rock design principle series. The SOLID design principles focus on
developing software that is easy to maintainable, reusable &
extendable. In this article, we will see an example of the Single Responsibility Principle in C++ along with its benefits & generic guideline.
/!\: Originally published @ www.vishalchovatiya.com.
By the way, If you haven't gone through my previous articles on design principles, then below is the quick links:
Intent
A class should have only one reason to change
In other words, SRP states that classes should be cohesive to the
point that it has a single responsibility, where responsibility defines
as "a reason for the change."
point that it has a single responsibility, where responsibility defines
as "a reason for the change."
Motivation: Violating the Single Responsibility Principle
class Journal {
string m_title;
vector<string> m_entries;
public:
explicit Journal(const string &title) : m_title{title} {}
void add_entries(const string &entry) {
static uint32_t count = 1;
m_entries.push_back(to_string(count++) + ": " + entry);
}
auto get_entries() const { return m_entries; }
void save(const string &filename) {
ofstream ofs(filename);
for (auto &s : m_entries) ofs << s << endl;
}
};
int main() {
Journal journal{"Dear XYZ"};
journal.add_entries("I ate a bug");
journal.add_entries("I cried today");
journal.save("diary.txt");
return EXIT_SUCCESS;
}
- Above C++ example seems fine as long as you have a single domain object i.e. Journal. but this is not usually the case in a real-world application.
- As we start adding domain objects like Book, File, etc. you have to implement save method for everyone separately which is not the actual problem.
- The real problem arises when you have to change or maintain save
functionality. For instance, some other day you will no longer save
data on files & adopted database. In this case, you have to go
through every domain object implementation & need to change code all over which is not good. - Here, we have violated the Single Responsibility Principle by providing Journal class two reason to change i.e. i. Things related to Journal, ii. Saving the Journal
- Moreover, code will also become repetitive, bloated & hard to maintain.
Solution: Single Responsibility Principle Example in C++
- As a solution what we do is a separation of concerns.
class Journal {
string m_title;
vector<string> m_entries;
public:
explicit Journal(const string &title) : m_title{title} {}
void add_entries(const string &entry) {
static uint32_t count = 1;
m_entries.push_back(to_string(count++) + ": " + entry);
}
auto get_entries() const { return m_entries; }
//void save(const string &filename)
//{
// ofstream ofs(filename);
// for (auto &s : m_entries) ofs << s << endl;
//}
};
struct SavingManager {
static void save(const Journal &j, const string &filename) {
ofstream ofs(filename);
for (auto &s : j.get_entries())
ofs << s << endl;
}
};
SavingManager::save(journal, "diary.txt");
- Journal should only take care of entries & things related to the journal.
- And there should be one separate central location or entity which does the work of saving. In our case, its SavingManager.
- As your SavingManager grows, you have all the saving related code will be at one place. You can also templatize it to accept more domain objects.
Benefits of Single Responsibility Principle
=> Expressiveness
- When the class only does one thing, its interface usually has a
small number of methods which is more expressive. Hence, It also has a
small number of data members. - This improves your development speed & makes your life as a software developer a lot easier.
=> Maintainability
- We all know that requirements change over time & so does the
design/architecture. The more responsibilities your class has, the more
often you need to change it. If your class implements multiple
responsibilities, they are no longer independent of each other. - Isolated changes reduce the breaking of other unrelated areas of the software.
- As programming errors are inversely proportional to complexity,
being easier to understand makes the code less prone to bugs &
easier to maintain.
=> Reusability
- If a class has multiple responsibilities and only one of those needs
in another area of the software, then the other unnecessary
responsibilities hinder reusability. - Having a single responsibility means the class should be reusable without or less modification.
Yardstick to Craft SRP Friendly Software in C++
- SRP is a double-edged sword. Be too specific & you will end up
having hundreds of ridiculously interconnected classes, that could
easily be one. - You should not use SOLID principles when you feel you are
over-engineering. If you boil down the Single Responsibility Principle,
the generic idea would be like this:
The SRP is about limiting the impact of change. So, gather together the things that change for the same reasons. Separate those things that change for different reasons.
- Adding more to this, If your class constructor has more than 5-6 parameters then it means either you are not followed SRP or you are not aware of builder design pattern.
Conclusion
The SRP is a widely quoted justification for refactoring. This is
often done without a full understanding of the point of the SRP and its
context, leading to fragmentation of codebases with a range of negative
consequences. Instead of being a one-way street to minimally sized
classes, the SRP is actually proposing a balance point between
aggregation and division.
often done without a full understanding of the point of the SRP and its
context, leading to fragmentation of codebases with a range of negative
consequences. Instead of being a one-way street to minimally sized
classes, the SRP is actually proposing a balance point between
aggregation and division.