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.

By the way, If you want to directly jumps to other design principles, then below is the quick links:

  1. SRP – Single Responsibility Principle
  2. OCP – Open/Closed Principle
  3. LSP – Liskov Substitution Principle
  4. ISP – Interface Segregation Principle
  5. DIP – Dependency Inversion Principle

The code snippets you see throughout this series of articles are simplified not sophisticated. So you often see me not using keywords like override, final, public(while inheritance) just to make code compact & consumable(most of the time) in single standard screen size. I also prefer struct instead of class just to save line by not writing “public:” sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way possible rather than the standard way or using Jargons.

Note:

  • If you stumbled here directly, then I would suggest you go through What is design pattern? first, even if it is trivial. I believe it will encourage you to explore more on this topic.
  • All of this code you encounter in this series of articles are compiled using C++20(though I have used Modern C++ features up to C++17 in most cases). So if you don’t have access to the latest compiler you can use https://wandbox.org/ which has preinstalled boost library as well.

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.”

Motivation: Violating the Single Responsibility Principle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.
    • Things related to Journal
    • Saving the Journal
  • Moreover, code will also become repetitive, bloated & hard to maintain.

Solution: Single Responsibility Principle Example in C++

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.