Principles to be used judiciously: SOLID
"Brilliant... to my engineering team as mandatory reading."
A solution architect works in the gap between enterprise architects and software architects. This is one of several articles written to help solution architects understand the software architecture of enterprise applications, and to supplement the syllabuses covered in our courses to industry certificates for architects of all threee kinds.
Many universities teach SOLID principles for software architecture. Are they helpful above the finest-grained level of software architecture? Are they so widely interpreted that they have no consistent meaning?
Contents: SOLID in short. The appropriate software architecture level. First thoughts. Longer discussion and questions. What do other commentators say? Concluding remarks.
SOLID in short
The five definitions quoted below are copied from this source.
S = Single Responsibility Principle
“A class should have one, and only one, reason to change. One class should serve only one purpose. All its methods and properties should work towards the same goal. When a class serves multiple purposes or responsibilities, it should be made into a new class.”
O = Open-Closed Principle
“Entities should be open for extension, but closed for modification. Software entities (classes, modules, functions, etc.) should be extendable without actually changing the contents of the class you’re extending. If we could follow this principle strongly enough, it is possible to then modify the behavior of our code without ever touching a piece of the original code.”
L = Liskov Substitution Principle
Simply put:
"Subclass/derived classes should be substitutable for their base/parent class. Any implementation of an abstraction (interface) should be substitutable in any place that the abstraction is accepted. "
I = Interface Segregation Principle
“A client should not be forced to implement an interface that it doesn’t use. This rule means we should break our interfaces into many smaller ones, so they better satisfy the exact needs of our clients. Similar to the Single Responsibility Principle, the goal is to minimize side consequences and repetition by dividing the software into multiple, independent parts”
D = Dependency Inversion Principle
“High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. Or : Depend on abstractions, not on concretions."
This article does on to explore and question the prnciples above. Are they helpful above the finest-grained level of software architecture - at the level of a distributed microservices architecture? Or are they so widely interpretable in different levels that they have no consistent meaning?
The appropriate software architecture level
It is said the principles make code more extendable, logical, and easier to read - when applied properly.
What does "properly" mean? In the original article on SOLID principles (“Design Principles and Design Patterns” (Robert C. Martin (www.objectmonitor.com), Marting suggested a three level decomposition hierarchy
Gabor Bella has pointed out to me that this is rather similar to the C4 model, which decomposes an application into three levels.
The original article concentrated on classes related to each other within a component (as in the C++ language) rather than the higher levels.
As I read them, the principes were defined for case 1 below rather than case 2.
First thoughts
S = Single responsibility: This principle is interpreted as encouraging the division of a large class into smaller ones. But since (like goals) responsibilities can be composed and decomposed in a hierarchy, one coarser-grained component may have several-finer grained responsibilities.
O = Open-closed principle: This principle is OK for adding subtypes to a class hierarchy, but questionable where client components delegate to server components, since it can lead to bloat in a system, as new server components are added on top of old server components that are closed to amendment.
L = Liskov substitution: Obviously, any component that implements the operations in an API, and meets non-functional requirements, should be replacable by another that does so. However, this principle was witten with a view to class hierarchies in which subtypes inherit from supertypes.
I = Interface segregation: As "S" above, this encourages the division of one large interface into smaller, client-specific, interfaces. Does this mean the FTP interface (with more than 100 operations) a bad thing? What if many clients need overlapping sets of operations? What if the component is a strongly cohesive REST-compliant server-side component?
D= Dependency inversion: Calling abstract interfaces rather than concrete implementations shield client components from some changes to server components. It cannot shield clients from all changes, and if e taken to excess, to code that is "interfaced to hell".
It seems to me that the principles tend to push in one direction. A theme that runs throughout my classes is that architects should a) understand there are always different design options, and b) understand the trade offs between those options.
Longer discussion and questions
Again, the five definitions below (longer versions this time) are quoted from this source.
S = Single Responsibility Principle
“A class should have one, and only one, reason to change. One class should serve only one purpose. All its methods and properties should work towards the same goal. When a class serves multiple purposes or responsibilities, it should be made into a new class.”
Questions that strike me
This is vague. In many cases, the words purpose, goal and responsibility may be interchanged with little or no loss of meaning. And, just like systems and their components, all three concepts are hierarchically composable and decomposable.
So, mapping one component to one responsibility leaves much room for interpretation and debate. It is commonly proposed that the operations of one component are cohesive if all are responsible for maintaining some element(s) in one cohesive data structure. But at what level of granularity? Should one class be responsible for one attribute? one normalised entity? one aggregate entity? or a wider "bounded context"?
Might it be better to read the “S”, as standing for the older idea of "separating concerns"?
O = Open-Closed Principle
“Entities should be open for extension, but closed for modification. Software entities (classes, modules, functions, etc.) should be extendable without actually changing the contents of the class you’re extending. If we could follow this principle strongly enough, it is possible to then modify the behavior of our code without ever touching a piece of the original code.”
Questions that strike me
This principle may be interpreted in at least two different contexts.
Can I read the principle as: Don’t screw up other subtypes or clients of a supertype by modifying the supertype when you add new subtype?
At the level of solution architecture of interest to me, delegation trumps inheritance. So, does this principle stack up outside of inheritance trees?
Can I read the original principle as: Don’t screw up other subtypes or clients of an interface by modifying the interface when you add new server component?
Adding a new server component on top of (in front of) an old one is sometimes a good idea. In the long-term, however, it can mean code written for the first set of requirements becomes obscurely buried behind newer code written for requirements that have been both extended and changed. This is one way that code can become bloated, over-complex and incomprehensible. And likely, one day, there will be a case for refactoring, or even throwing away, old code?
L = Liskov Substitution Principle
“Barbara Liskov and Jeannette Wing formulated the principle succinctly in a 1994 paper as follows: Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T."
The same source goes on to say.
"The human-readable version repeats pretty much everything that Bertrand Meyer already has said, but it relies totally on a type-system:
Recommended by LinkedIn
“Robert Martin made the definition smoother and more concise in 1996: Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.
"Or simply: Subclass/derived classes should be substitutable for their base/parent class. It states that any implementation of an abstraction (interface) should be substitutable in any place that the abstraction is accepted. Basically, it takes care that while coding using interfaces in our code, we not only have a contract of input that the interface receives, but also the output returned by different classes implementing that interface; they should be of the same type.”
Questions that strike me
Obviously, any component that implements the operations in an API, and meets non-functional requirements, should be replacable by another that does so. However, this principle was witten with a view to class hierarchies in which subtypes inherit from supertypes.
Can I read the original as: Don’t screw up an operation in a supertype (or interface) by changing its specification in a subtype (or implementation)?
So, an operation (say, calculateArea) inherited from a class (Quadrangle) should meet the same specification when invoked on a subtype object (Square or Parallelogram). It should not be more constrained by preconditions, or produce different results or effects.
What if developers implement an abstract/polymorphic operation in different ways, by overloading it in different subtypes to do different things? E.g. What if I implement an abstract “add” operation in a supertype by coding a numerical addition operation in one subtype and coding a number concatenation operation in another?
I = Interface Segregation Principle
“A client should not be forced to implement an interface that it doesn’t use. This rule means we should break our interfaces into many smaller ones, so they better satisfy the exact needs of our clients. Similar to the Single Responsibility Principle, the goal is to minimize side consequences and repetition by dividing the software into multiple, independent parts”
Questions that strike me
Clearly, the principle is OK if each client needs a different set of operations. Otherwise, surely, defining multiple interfaces can increase complexity and maintenance effort?
What if several clients need different, overlapping, subsets of the operations definable in one interface? Say, different subsets of the operations in the FTP protocol? Will segregating interfaces lead to replicated code and complexities when fixing bugs or making changes?
The original article answered “clients should be categorized by their type, and interfaces for each type of client should be created.” The classification of clients is up to you. What if one client wants to improve an operation in their interface that is currently replicated in another client's interface? Might the other client want the same modification?
(It is said that "smaller client-specific interfaces prevent clients becoming dependent on operations they don’t need". But why would clients ever invoke operations they don’t need?)
D = Dependency Inversion Principle
“High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. Or : Depend on abstractions, not on concretions.
"By applying the Dependency Inversion Principle, the modules can be easily changed by other modules just changing the dependency module. Any changes to the low-level module won’t affect the high-level module. There’s a common misunderstanding that dependency inversion is simply another way to say dependency injection. However, the two are not the same.”
Questions that strike me
Class-level thinking: This can mean that a client depends on an abstract supertype (or interface) that is in turn <realised by> one or more concrete subtypes.
But suppose instead that high to low means client components <delegate to> server components in a layered architecture. Then the principle means client components should not invoke concrete server components directly, but should instead invoke abstract interfaces. Does this imply "interfaced-to-hell" code in which no components communicate directly with each other? Or do we insert interfaces only between component containers that are physically distributed?
A client component that calls a server component via an interface not only depends on that interface, but also depends upon the server in important ways. While the client may be physically decoupled from the location and technology of the server, it is not decoupled logically from the services the server provides.
Generally, client components cannot be shielded from all changes to server components. True, in a TCP/IP network stack, each layer encapsulates and ignores the data "known" by the layer above. But by contrast, in a three layer software architecture (UI <> Business <> Data) for an enterprise application, every layer processes the same data items, and relates them to each other in the same way (say, one order has several order items). This knowledge does not "leak" from one layer to another - it is shared by all of them. That is why a change to one layer may ripple through all, despite the presence of interfaces between the layers.
Moreover, all design is trade offs. As Craig Larman observed, decoupling today to facilitate an unlikely tomorrow, is not time well spent. Complexification, for agility in a possible future, is a hindrance to agility today.
By the way, "inversion" seems an odd term. In what sense do server components depend on the interfaces that clients use to invoke them? Might some interpret this to mean that request-reply invocations should be implemented via an event-driven architecture? That would seem a needless complexity and costly overhead.
What do other commentators say?
This defense of the principles doesn't convince me. And the comments at the end of the article add to my doubts
José Arturo Cano wrote: January 2, 2019 at 7:51 pm
“I find your interpretations very compelling, but I’ve found so diverse interpretations of these principles on the web that I can say they are close to useless. The idea of a software design principle IMHO, is to provide a base framework for developers to discuss software development best practices. But when the advocators can’t even agree on what their principles mean, it’s time to look for alternatives.
I also have found that people trying to follow these principles create extremely over-modularized architectures. Mostly because they decompose simple implementations into even smaller modules, disperse over the project. Which makes it close to impossible to discern the purpose of these micro-modules in the context of the whole project.”
CWS wrote: May 10, 2019 at 2:49 am
“I would agree with Joel on this. Things like Test Driven Design are good for old codebases where things don’t change much. Or you have an amazing client who knows exactly what they want and things aren’t going to change, and the architect has sat with the client to define every class, every method, its parameters and returns, and the programmers just write unit tests around that, then write the code…..
Wait….let’s just stop right there. I’ve never had a client that knows know what they really want. In 20 years of coding for employers and working for myself as a contractor.. not once. It’s not just me – that is the real world.
Writing a bunch of unit tests before you’ve even written code is a waste of time. Like Joel mentioned, you have all these unit tests, then things change. So now you have to edit all the unit tests to compensate for the changes. THEN change your code so that the unit tests pass… then two days later the client changes their mind again – or adds a new feature that suddenly impacts a big chunk of code, including any UI layouts… so now you have to go change the unit tests AGAIN… and so on and so on.
How many man hours did you spend writing unit tests that really, really don’t matter?
The same [reasoning can] be used against SOLID. There’s just too many changes in a real world environment where features are adding, removed, edited to an ever evolving piece of software.
You really have to take each project as a separate project and trying to cram an ideal into every project is going to hamper productivity and creativity in the end.”
Concluding remarks
As Michael (M. A.) Jackson said in 1975, of the principles of program design.
However, software engineers work in a wide variety of knowledge domains, and not all patterns and principles work well in every context.
"You really have to take each project as a separate project and trying to cram an ideal into every project is going to hamper productivity and creativity in the end."
The SOLID principles may help you sometimes, but they are interpreted in different ways, and can be counter-productive if you don't minimize the complexities resulting from
I don't see SOLID as a universal foundation stone for design to meet the three, sometimes conflicting, aims I was taught.
My view in short
S? I prefer Separate Concerns, along with the suggestion of Larry Constantine in 1968. "Separate the processing of different data structures (input, output and stored); strive for tight cohesion within a module and loose-coupling between modules."
O and L? Do these suggest a faith that base classes and inheritance trees are stable (or even eternal or universal truths), which is not generally the case in business domain models?
I and D? Might these over complexify a design?
What else to say about the scope of the much coarser grained software components assigned to a programmer or small teams? As per Conway's law (1968) a component should be no larger than the cognitive capacity of the person or team. And a team or more than 7 or 8 people will struggle to mind share.
The dividing lines between larger software components still need to be drawn well. And for that, two rules of thumb for scoping a component are:
Further reading
A solution architect works in the gap between enterprise architects and software architects. This is one of several articles written to help solution architects understand the software architecture of enterprise applications, and to supplement the syllabuses covered in our courses to industry certificates for architects of all threee kinds.
I'll try to answer some of the numerous questions, more probably bit by bit and not necessarily in order. I'm numbering my comments for reference in case there are comments to them.
Turning digital transformation promises into results.
3ySOLID principles are meant to be understood and applied at the higher OOP design level. As soon as you break them down and interpret them using code details or even some programming language characteristics, there will be room for misinterpretation. People might have all sorts of "striking" questions or even doubts about the purpose and definition, usually backed up by the (wrong) detail context applied. Many popular open source OOP libraries, written in different languages, are following SOLID design principles. As far as we can see, they already prevail since many years, while persistently keeping the promise of delivering effective, flexible and robust software products, based upon the same unchanged and simple set of 5 rules. I see SOLID more like a conceptual promise to make, not a coding process to follow.
Coder in heart and liver.
3yThe S example of dividing up a database class has nothing with the S. One class can do Sql queries for the whole database. The only concern for the class is the Sql language. If one uses several types of Sql databases, for each type a separate class should be made inheriting from the base class. Their concern is the specifics of the database type. If the database contents have to be zipped and emailed, those are not concerns of these classes. A separate class should receive the contents (a stream of characters) and zip the data; its concern is the compression. Another class should do the emailing of the result of the zip class; its concern is the email protocol. Etc.
Enthusiastic Business Value Obsessed Nerd
3yBrilliant. Slacking to my engineering team as mandatory reading. Cheers Graham Berrisford
Architecture and Data Engineering for SA DOD
3yNot ever a software architect... but here is my 2 cents. It is said..the only constant is change... If we look at the construction industry as an analogy there too the principles had to adapt to "evolving" materials. Today we have huge and constant increases in processing, bandwidth and storage ... Surely this should dictate change in the software domain.