You probably know the "SOLID principles", which should help you to be a better programmer. And maybe, you are still struggeling with the "S", the Single-responsibility principle (SRP). Maybe I can give you a different view on this topic.
Ok, we are talking about OOP, the Object Oriented Programming. As Robert C. Martin (Uncle Bob), the originator of the term, was editor-in-chief of C++ Report magazine, we can assume he probably was using this langauge. Did you ever wonder about the strange formulation:
"A class should have only one reason to change"?
Often enough, we find this explanation: "A class should have only one reason to change, meaning it should only have one job or responsibility". But, what is "a job?" Is it a complex task or keeping a single state? If a class has the job to print something, in which kind is it changed, if it is doing the job? Let´s say: We get more questions than answers.
So, maybe we should have a look back to what C++ was used for in 2003. This is an overview of the Microsoft Foundataion Classes (MFC):
Ok, this is the 1st of three charts, but all classes are derived from CObject, which implements the most basic behavoir of MFC-Objects. You will find all kinds of Interface objects that can be used to build User Interfaces, but also handle Drag&Drop-Actions, store the current UI state and so on. This are the building blocks of all Windows Applications:
"The Microsoft Foundation Class (MFC) Library and Visual C++ provide an environment that you can use to easily create a wide variety of applications." (Source).
If you are building with User interfaces in C++, you usually do not start building classes from scratch. You select an appropriate class that provides most of what you need, so you just add some specific code. So, you start with one of the foundation classes or one of it´s decendants. As an example, you want to create a new input field to be used in the Application UI. As the UI only accepts classes that are derived from certain core classes, you need to start with one of the existing classes. Therefore you inherit a lot of skills that each UI-element must have. To minimize your work, you select an element that is very similar to what you want and add only what you need. Thanks to inheritance and polymorphism, this is possible without breaking the existing code.
Later Robert C Martin explained the SRP as cohesion: "Gather together the things that change for the same reasons. Separate those things that change for different reasons". You surely need more than one thing to gather something.
In C++, class hierarchies can be quite complex and deeply nested. But if you follow the SOLID principles, you will be able to manage this complexity. Think of a class always as part of a hierarchy. Each new class, you add to this hierarchy, sould have a single task. This does not mean, it should only have one method or one state. But all properties and methods it implements should be focused on the same task. I suppose, this is, what Uncle Bob meant.
There is another aspect of the SRP that you can only understand thinking in hierarchies: A hierarchy is like a tree, each method you add to the root is inherited by all the branches and leafs. If you choose the wrong position, you might be forced to implement the same code in multiple branches. To keep your code maintainable, you should try to find a position where every task needs to be implemented only once. This could also be seen as a "single responsibility principle": For each task (in a class family), there should only be one member that is responsible for this task.
Finally I´m not sure what Uncle Bob was really trying to say, but I know, that using OOP without taking benefit of inheritance does not make much sense.
Top comments (4)
I like this take! Good one :)
Over-reliance on inheritance can indeed lead to tightly coupled systems, which is a significant concern in OOP. When a base class is modified, it forces changes in all subclasses, potentially introducing bugs or breaking existing functionality. This tight coupling reduces flexibility and can make maintaining the codebase more difficult, especially in large-scale systems where inheritance hierarchies can become complex and deeply nested.
Inheritance isn't always the optimal solution for implementing SRP. While inheritance promotes code reuse, it often results in rigid relationships between classes, which can be difficult to refactor or extend. Instead, composition—where objects are made up of smaller, specialized components—often provides more flexibility and avoids the pitfalls of deep inheritance chains.
The term “reason for change” in SRP can also be somewhat ambiguous. While the core idea is that a class should only change for one concern, defining what constitutes a “reason for change” can vary depending on context. For example, is a class's reason to change related to user interface changes, business logic changes, or data handling? These different reasons can overlap, leading to a blurry line between what responsibilities belong to a class and which should be delegated to others.
Adhering to SOLID principles, especially SRP, can help manage complexity in software systems. However, applying them too rigidly can result in over-abstraction, where the design becomes fragmented into many small classes that each handle a tiny piece of functionality. While this can make each class more focused, it can also lead to increased complexity due to the sheer number of components, making the system harder to understand and maintain. This is a common pitfall where an over-engineered design can make the codebase unmanageable and difficult to navigate.
Ultimately, while SOLID principles, including SRP, are invaluable for structuring maintainable systems, they must be applied judiciously. Striking a balance between abstraction and simplicity is key to avoiding the problems of over-engineering. Too much focus on minimizing responsibility per class can lead to unnecessary complexity, whereas too little attention to responsibility can result in bloated, less maintainable code. The goal should be to create systems that are both modular and easy to understand, without losing sight of practical simplicity.
This is not what OOP was intended and does not align with my experience. There are surely some rules you need to follow to avoid those problems, but my general experience is that inheritance helps you to keep concerns separated.
A major task is to keep interfaces narrow, so a class exposes only a minimal set of methods and properties to the public and even to it´s family. You can still update functionalities in a child class, but without breaking the code.
There are surely differences between languages, and different styles and there are always multiple ways to catch a pony. I suppose, the SOLID principles have been introduced with a certain language and topic in mind, but they are not the only concept you can follow. I have refractured and updated large projects multiple times, and OOP always helped me to keep the task simple. Some of the base classes introduced very large families and hundreds of procedures accessible in one class. This is surely nothing Uncle Bob would like, but worked flawlessly. So, in my opinion, SOLID is one set of rules to follow, but not the only one.
I appreciate your perspective and the real-world experience you've shared. I completely agree that inheritance, when used properly, can help keep concerns separated and simplify refactoring tasks, particularly in large projects. Narrow interfaces and the ability to update child classes without breaking the code are definitely strengths of inheritance in OOP, and I've seen it work effectively in many cases as you’ve described.
That said, I think the concern about tight coupling in deep inheritance hierarchies remains a valid one, especially when dealing with large-scale systems that evolve over time. As inheritance chains become deeper, small changes in base classes can ripple throughout the entire hierarchy, introducing unintended side effects. This can be challenging to manage, especially when the system grows in complexity or when it becomes necessary to refactor parts of the hierarchy. Composition tends to provide more flexibility in these cases, as it avoids the entanglements of deep inheritance.
I also think that while SOLID principles may have been introduced with certain languages and paradigms in mind, they still provide a useful framework for thinking about maintainability and extensibility. But I agree with you that SOLID is not a one-size-fits-all solution. In some cases, an over-reliance on these principles can lead to over-abstraction, where the system becomes harder to maintain due to excessive fragmentation. It's important to strike a balance between modularity and simplicity, and sometimes the best solution might not strictly adhere to all of the SOLID principles.
Ultimately, it’s about finding the right balance based on the project’s needs and complexity. If inheritance helps you achieve that balance, that’s great. But if it starts to introduce unnecessary complexity, composition or other alternatives might be worth considering.