Can you remember what SOLID stands for? Chris Oldwood distills it down to two easy to remember principles.
The SOLID acronym and the design principles behind it have been the guiding light for achieving good object-orientated (OO) designs for as long as I can remember. It also features in nearly every job interview I’ve attended, and yet I have to revise it because I can never remember exactly what each of the letters stands for!
Over the last few years the reason for my inability to remember these 5 core design principles has become apparent – they are just not dissimilar enough to make them memorable. You would think that any design review meeting where OO was being used as the fundamental paradigm would feature one or more of these terms at almost every turn. But no, there are two other terms which I find always seem better suited and universally covers all the SOLID principles, but with much less confusion – Separation of Concerns (SoC) and Program to an Interface (PtaI).
This article attempts to highlight the seeming redundancy in the SOLID principles and shows where either of the two terms above could be used instead. This does not mean the SOLID principles themselves have no intrinsic value, individually they may well hold a much deeper meaning, but that at a higher level, which is where you might initially look for guidance, it is more beneficial to keep it simple (KISS).
Already on shaky ground
In recent years I have seen a number of talks where this theme has come up. Kevlin Henney, a man who seems to know a thing or two about object-orientated design (amongst many other things), has given a talk where he has deconstructed SOLID [ Henney11 ] and even provided (in collaboration with Anders Norås) his own alternative – FLUID [ Henney12 ].
More recently I attended a talk at the 2014 ACCU conference by James Grenning [ Grenning14 ] about applying the SOLID principles to a non-native OO language – C. During this talk he raised a similar question about which are the two core principles of SOLID, and provided the Single Responsibility Principle (SRP) and the Dependency Inversion Principle (DIP) as his selection.
Whilst the recognition that there is some duplication within the SOLID principles is useful, I wanted to avoid the confusion that would result from trying to always pick two of the five and then use them consistently in any future discussion. What I realised is that I’ve naturally always side-stepped this issue by using the terms Separation of Concerns (SoC) and Program to an Interface (PtaI) instead.
The remainder of this article looks at each of the SOLID principles and frames them within the context of the two more generalised ones. Rather than discuss them in acronym order I’ve therefore grouped them in two so they are discussed collectively under the same banner.
Separation of concerns
The basis of the Separation of Concerns (SoC) principle is not specifically within software design; it transcends that and applies to many other pursuits in life too, such as writing. Consequently it is far less prescriptive in nature, instead preferring only to state that ‘stuff’ should be thought of in a different light to other ‘stuff’. The exact nature of that stuff, how big it is, what it does, who owns it, etc. is entirely context dependent. In terms of a software system we could be talking about the client and server, or marshalling layer and business logic, or production code and test code. It’s also entirely possible that the same two areas of discussion be considered separate in one context and yet be united in another, e.g. subsystems versus components.
Single Responsibility Principle
It should be fairly obvious that the Single Responsibility Principle (SRP) is essentially just a more rigid form of SoC. The fact that an actual value has been used to define how many responsibilities can be tolerated has lead some to taking this principle literally, which is why I’ve found a personal preference for the more general term.
The subtext of SRP is that any class should only ever have one reason to change when that class has a single, well-defined role within the system. For example, I once worked on a codebase where a class was used to enrich an object model with display names for presentation. The class that did the enriching looked similar to Listing 1.
public ProductNameEnricher : Enricher { public void Enrich(Order order) { // code to make HTTP request to retrieve // product details // code to transform the object model } } |
Listing 1 |
When it came to testing the class a virtual method was introduced to allow the HTTP request to be mocked out; however virtual methods in C# are often a design smell [ Oldwood13 ]. Although it might seem that the class has only one responsibility – enrich the object model with data – in this instance it really has two:
- Make the remote HTTP call to retrieve the enrichment data
- Transform the object model to include the presentation data
When it comes to further responsibilities later, such as error handling of the remote call, caching of data, additional data attributes, etc. the entire responsibility of the class grows and often for more than one reason – HTTP request invocation or object model transformation.
Hence the two concerns can be split out into separate classes that can then be independently tested (therefore removing the need for the virtual method), e.g. see Listing 2.
public ProductService : ProductFinder { public Product[] FindProducts(...) { // code to make HTTP request to retrieve // product details } } public ProductNameEnricher : Enricher { public void Enrich(Order order) { var products = _proxy.FindProducts(...); Enrich(order, products); } private static void Enrich(order, products) { // code to transform the object model } private readonly ProductFinder _proxy; } |
Listing 2 |
Interface Segregation Principle
The clue that the Interface Segregation Principle (ISP) is just a specialisation of SoC is the use of the word ‘segregation’. Replace it with ‘separation’, jiggle the words around a little and soon you have Separation of Interfaces.
The idea behind ISP is that although a class may support a rich interface, its clients are quite often only interested in a (common) subset of those at any one time. Consequently it would be better to split the interface into just the surface areas that are commonly of interest. A typical example of this is a class for manipulating files or in-memory streams:
public class MemoryStream { void Read(byte[] buffer, int count); void Write(byte[] buffer, int count); }
It is more prevalent for a client to only be concerned with reading or writing to/from a stream at any one time. Therefore we can use the Extract Interface refactoring [ Fowler ] and split those responsibilities into two separate interfaces (see Listing 3).
public interface Reader { void Read(byte[] buffer, int count); } public interface Writer { void Write(byte[] buffer, int count); } public class MemoryStream : Reader, Writer { void Read(byte[] buffer, int count); void Write(byte[] buffer, int count); } |
Listing 3 |
Program to an interface
I first came across the expression ‘program to an interface, not an implementation’ when reading the seminal work on Design Patterns by the Gang of Four [ GoF ]. The idea is that you should not (need to) care about how a class is implemented, only that its interface provides you with the semantics you require. Unlike languages such as Java and C#, C++ (amongst others) does not support interfaces as a first-class concept which means the notion of ‘interface’ is somewhat subtler than just the methods declared within an independently defined interface-style type.
Where the two SOLID principles that fall into the SoC category above are fairly easily to guess from their names alone, the latter three are probably a little more obscure. And this, I feel, is in stark contrast to the name of this encompassing principle.
Liskov Substitution Principle
The Liskov Substitution Principle (LSP) is usually ‘explained’ by quoting directly from it. What it boils down to is the proposition that two types are related if one can be used in place of the other and the code still works exactly as before. One less intuitive example of this would be switching containers in C++ (see Listing 4).
int sum(const std::vector<int>& values) { int sum = 0; for (auto it = values.begin(); it != values.end(); ++it) sum += *it; return sum; } |
Listing 4 |
In this example the container could be replaced with
std::dequeue
or
std::list
and the program would still compile, execute and give the exact same result. And yet none of the C++ containers derive from a common ‘container interface’; they merely implement the same set of methods to provide the same semantics and can therefore be considered directly substitutable.
Interestingly Kevlin Henney, in the first talk I saw about SOLID [ Henney11 ], made the observation that the Liskov Substitution Principle states that a condition of the relationship is based on there being “ no change in the program’s behaviour ”. The example above adheres precisely to that constraint. However many interpret the principle as allowing substitution if it conforms to the same abstract interface, despite the fact that the implementation of that interface could produce a program that does something completely different (Listing 5).
public interface Writer { void Write(string message); } public class FileWriter : Writer { void Write(string message); } public class DatabaseWriter : Writer { void Write(string message); } |
Listing 5 |
Whichever way you interpret it, the ability to substitute one implementation for another is predicated on the need of the two implementations to confirm to the same interface, and for the consumer of that interface to only invoke it in such a way that is compatible with the common semantics of all potential implementations.
Dependency Inversion Principle
Whilst LSP considers what it means for the client code to be able to consume similar types based on semantics, the Dependency Inversion Principle (DIP) tackles how the implementation can be physically partitioned without the client needing direct access to it. If the client only depends on abstractions, then the opportunity to substitute different implementations becomes possible. And because the details of the abstraction is all that is required by the client, then the implementation can live in a different part of the codebase.
The ‘inversion’ aspect comes from the idea of a layered architecture where the consuming code that only relies on the abstraction can live in the lower layers whilst the actual implementation can live in the higher ones. In an onion-like architecture the abstractions can live in the core whilst the implementations live in the outer service layers.
The word ‘abstraction’ could be considered as synonymous with ‘interface’ if considering it in the context of a programming language that supports it as a first-class concept. In a duck typing environment the subtler meaning is harder to translate as there is nothing concrete to use as an aid in ensuring you’re only relying on the abstract behaviour. Either way Program to an Interface is really another way of saying Program to an Abstraction.
Open/Closed Principle
The Open/Closed Principle (OCP) has taken quite a beating in recent times with the likes of Jon Skeet [ Skeet13 ] putting it under the microscope. It appears that some have taken the ‘closed for modification’ aspect quite literally meaning that the code cannot be modified under any circumstances, even to fix a bug! Others have interpreted it a little more liberally and employed polymorphism as a means to shield both the caller and 3rd party (i.e. uncontrolled) implementations from change.
For example, in languages where polymorphism is not supported as a first-class concept, such as C, switching on a field holding an object’s underlying ‘type’ is a common way of varying behaviour (Listing 6).
switch (shape->type) { . . . case Shape::Square: DrawSquare((const Square*)shape); break; case Shape::Circle: DrawCircle((const Circle*)shape); break; . . . } |
Listing 6 |
With direct support for polymorphism the client code reduces to a single line and is ‘open for extension’ without requiring modification of the caller by virtue of the fact that any new subclasses will be automatically supported:
shape->Draw();
Similarly an implementation can be made ‘open for extension’ by breaking the functionality down into small, focused methods that can be overridden piecemeal by a derived class, e.g. via the Template Method design pattern [ Wikipedia ]. See Listing 7.
class DocumentPrinter { public: virtual void Print(Document doc) { PrintHeader(doc); PrintBody(doc); PrintFooter(doc); } protected: virtual void PrintHeader(); virtual void PrintBody(); virtual void PrintFooter(); }; |
Listing 7 |
In both these cases the mechanism that decouples the client from the implementation and allows the implementation to be composed at an abstract-level is the interface. Whilst inheritance is a common choice for implementation reuse, composition is more desirable due to its lower coupling. The interface provides the means of describing the extensible behaviour.
If taken in a literal sense the notion of programming to an interface encourages the design of abstractions rather than concrete types which in turn promotes looser coupling between caller and callee. Consequently it also relaxes the need to rely on inheritance as the sole method of extension and instead paves the way for composition to be used as an alternative.
Further simplification
In a private conversation about this idea Kevlin Henney mused that you could even drop Program to an Interface as that is in itself just another variation of the Separation of Concerns principle:
“If you think about it, Programming to an Interface is an articulation of separating concerns: separate interface from implementation, have dependent code work in terms of interface, i.e., separate from implementation dependency.”
Whilst in theory this is an interesting observation, I not sure what, if any, practical value it has. Einstein once said “ Everything should be made as simple as possible, but no simpler ”. The reduction of SOLID down to just the Separation of Concerns and Program to an Interface principles feels to me to be a valuable simplification, whilst the above would be a step too far.
Summary
The goal of this article was to reduce the five core SOLID principles to a more manageable and memorable number for the purposes of everyday use. Instead of getting distracted with the finer details of SOLID I’ve shown that the two more generalised principles of Program to an Interface and Separation of Concerns are able to provide just as much gravitas to shape a software design.
Acknowledgements
This article is merely a refinement of similar ideas already put forward. As such I’m just standing on the shoulders of the giants that are Kevlin Henney and James Grenning. The Overload peer reviewers have also helped me stand on my tippy-toes to see that little bit further afield.
References
[Fowler] ‘Refactoring: Improving the Design of Existing Code’ by Martin Fowler – http://refactoring.com/catalog/extractInterface.html
[GoF] Design Patterns: Elements of Reusable Object-Oriented Software by Ralph Johnson, John Vlissides, Richard Helm, and Erich Gamma
[Grenning14] ‘Designing SOLID C’ by James Grenning (ACCU Conference 2014) – http://www.slideshare.net/JamesGrenning/solid-c-accu2014key
[Henney11] ‘Will the Real OO Please Stand Up?’ by Kevlin Henney (ACCU 2011 conference) – http://accu.org/content/conf2011/Kevlin-Henney-Will-the-Real-OO-Please-Stand-Up.pdf and http://www.slideshare.net/Kevlin/solid-deconstruction
[Henney12] ‘SOLID deconstruction’ by Kevlin Henney (ACCU 2012 conference) – http://accu.org/content/conf2012/Kevlin_SOLID_Deconstruction.pdf and http://www.slideshare.net/Kevlin/introducing-the-fluid-principles
[Oldwood13] ‘Virtual Methods in C# Are a Design Smell’ by Chris Oldwood – http://chrisoldwood.blogspot.co.uk/2013/09/virtual-methods-in-c-are-design-smell.html
[Skeet13] ‘The Open-Closed Principle’, in review by Jon Skeet – http://msmvps.com/blogs/jon_skeet/archive/2013/03/15/the-open-closed-principle-in-review.aspx
[Wikipedia] ‘Template Method from Design Patterns’ – http://en.wikipedia.org/wiki/Template_method_pattern