Are you interested in software that lets you implement upcoming requirements without breaking old functionality? Software that means you won't hear “but nobody told us (developers) that this would be required” and “If we had known...” Does this sound like science fiction? It doesn't have to be. SOLID is a set of relatively simple principles that, once in use, take software to another dimension.
Have you ever looked at source code and had no idea what on earth was going on? The class or method seems to do everything but its name says it should. The name also refers to something the code used to do years ago. The code is full of tracing and debugging information. Lots of switch and if statements, couple of loops. All within one unit of code! There is a question behind where the desired functionality is hiding. The algorithms are too complex, coupled in such a way that any minor change requires a huge effort. How about unstructured components connected to each other based on “There is a quick workaround”. A complete nightmare.
Does this sound familiar?
The following aspect comes to mind. What distinguishes software development from software engineering? In both disciplines code is written in the correct syntax of the language, classes and methods are constructed, etc. What is the difference between them?
Software has two major functions. Secondary is all about delivering desired functionality to the consumer which of course is the most important aspect for the user. In order to fulfil the current needs of the user, a software can be implemented once and never needs to be changed. This is Software development. However, in the dynamic world with constantly changing business needs, also the requirements for existing software implementation change. For this reason classes and methods must be designed in a way, they can be easily changed, extended, etc. The primary function therefore is to enable the software to adapt changes easily. This is software engineering.
As we are not able to guess future customer needs, what is the correct course of action in software engineering? There is one approach: keep the source code clean. It was said a lot of time, but what does clean code actually mean? Let´s introduce the SOLID principles. The plural indicates that there are several, but how many? Each letter in SOLID stands for a separate rule. The following paragraphs describe them one by one. They are a set of rules applying to the code, which every engineer shall be familiar with. They describe how to keep things decoupled, maintainable and readable.
So, fasten the seatbelts and let’s take a journey into the SOLID principles.
1. Single Responsibility Principle (SRP)
The first rule of SOLID principles is represented by the letter “S” and is called the Single Responsibility Principle. It says that each OO software unit shall be composed of one and only one responsibility.
What is actually meant here by 'responsibility'? It is all about its consumers. They are the target audience, for whom it is implemented, so it is needed to keep them happy. One class -> one sort of consumer. Consider a piece of software with the following class:
Rectangle Class Diagram (1)
Do the methods work together?
Imagine a new method in the class in a future version. The method is about drawing the rectangle in three dimensions and is called VisualizeTo3D.
There are two kinds of consumers: One works with UI and another relies on business logic only. The change forces recompilation and redeployment, including for the consumer who does not need it. This causes unnecessary effort for the customer and for software engineers.
One can imagine the logging responsibility which is actually a separate one, but strongly linked to the primary responsibility. So they cannot be delivered separately. What can be done is extract logging functionality to the private class for reasons of readability and maintainability.
How the items are kept decoupled? One solution is the façade pattern, where one class contains knowledge about all particular responsibilities but they are hidden from the consumer.
Façade Pattern Diagram (2)
Another solution would be to use Interface Segregation, which is described in paragraph 5 in more detail. It is the exact opposite of the previous solution. There is a group of interfaces, each representing one responsibility, and one concrete class implementing all of them.
Interface Segregation Diagram (3)
The problem of coupling is partially solved; developers or engineers might use the API worked on before; the actual implementation is like a set of unrelated functionalities. To keep the code clean in such a case is a challenge. So welcome to engineering. The world is not perfect and the problem will never be completely solved. This is an essential principle for OO software.
2. Open Closed Principle (OCP)
This one is about polymorphism and inheritance. In simple terms, the classes shall be open for extensions but closed for modifications. The consumer will be able, if desired, to extend functionality in accordance with what a class or a method offers. The consumer will never be able to modify existing units of code. In other words it will be possible to change the behaviour of the software, but not the model itself. How is this possible?
Decoupled code. There are a couple of reasons here for this. The first reason, as is already known, is to fulfil the SRP. Another is a result of the following problem. The decision trees based on magic flags (the best are Boolean) or argument type checking are well-known. The diagram 4 shows how a payment is being executed in the CRAP system
IPayment example 1 (4)
This is already the second iteration of this method since, at the beginning, only a cash one was considered. How did this happen? The class was created by Pablo in order to charge a customer of CRAP. The source code was quickly implemented. Pablo was recognised by his Project Leader for his performance. Sometime later, credit card functionality was requested. So a quick change is made and voilà, the code is there. What happens if one day, long after CRAP was released to several customers, PayPal appears on the market and this type of payment will be requested? Shall another “if else” statement be added? The existing strategy was already far from perfect. The Strategy Pattern will be used instead:
IPayment Strategy example (5)
The new payment method required an extension but not a modification. Isn't that great?
Using P-O-L-Y-M-O-R-P-H-I-S-M (yes, it is the answer to the pending question) recompilation and redeployment can be skipped.
3. Liskov Segregation Principle (LSP)
In the late '80s, Barbara Liskov described subtyping as follows. “If for each object O1 of type S there is an object O2 of type T such that for all programs P defined in terms of T, the behaviour of P is unchanged when O1 is substituted for O2 then S is a subtype of T.” (1) …. Is it though, right? It took me a couple of readings to get it fully.
Liskov Diagram (6)
The code might be infinitely reused, building structures based on the existing code. The P might use S or even other subtypes without even noticing it. The principle is again about polymorphism, but also about subtyping. Type itself is a set of operations. As in one of our previous examples, a rectangle has height and width which can be set and area can be calculated based on these.
The classic example is explained here. There is a rectangle class in the CRAP system. As is already known, CRAP is being used world-wide by several customers. One day the requirement “a square needs to be supported” arrives. Clever Pablo takes a pencil and starts drawing.
LSP1 Pablo1 (7)
Everybody was told by their teacher that a square is a rectangle. Is it also the case in software representation? A rectangle has two sides (width and length) that can be set using various available methods. Once complete, the area can be calculated and used for further calculations. By having a square with an “is” relationship to the rectangle, CRAP sees some strange behaviour. The square has one side, but inherits two fields and operations from its base. Firstly, there is something strange since two variables are needed to keep one value and memory gets consumed too much. Another unexpected aspect is filling these values while calling one of its operations. Shall the both variables be fed by each operation call? This might lead to unexpected behaviour.
Unexpected behaviour leads to the software getting fragile so lots of unintended dirty hacks are seen and it becomes buggy. It's pretty difficult to maintain. So how about extending the rectangle class in such a way that it recognises type of a caller’s type (and runs a different algorithm)? It builds a type dependency … Was anything broken? Right, OCP.
Pablo has learnt something from his previous mistakes. He started to experiment with the pencil and found the solution. Inheritance inversion.
LSP Inversion (8)
Is it really a solution? What about fields, operation naming, behaviour of area calculation? What should the other side of the rectangle be called? Once this method is applied to an object of rectangle subtype, how should the area be calculated? There are lots of unanswered questions. And Pablo’s new, smart colleague has a “quick workaround”. No, thank you!
What is the solution?
The real one is to avoid inheritance. Since an “is” relationship does not apply here, these two things are unrelated to each other at a software level.
4. Interface Segregation Principle (ISP)
What are interfaces? Interfaces are pure virtual classes in C++ vocabulary or abstract classes with no implementation. Actually this prevents the known software paradox of the diamond problem that occurs with multiple inheritances. Is it really the solution? The reader should do his own research. But why should interfaces be used? The reason is the user does not have to know about actual implementation. It is enough for him to know there is such functionality. It simplifies the work between two parties when developing any piece of code. There is also no need to recompile and redeploy the whole system just because one little detail has changed in the system. Furthermore, the caller does not need to know (and probably is not interested in knowing) which object type he is working on. All he needs to know is that he wants to perform a particular procedure in order to fulfil his logical and business needs. An interface shall be understood as a property/functionality of the particular type. The interfaces were discussed in the diagrams above. Let’s have a closer look at this.
The interfaces have more to do with its consumers and less to do with its implementers, so their names shall be adjectives and they will be directed towards users to indicate what they are about.
The principle segregates the objects based on the functionality they have, which avoids the need to rely on fat classes the CRAP has. All in all, the main target of this rule is “Do not depend on the details you do not need”! It is completely unimportant to the consumer how the method draws its object. What matters is it draws it. Ideally the class once compiled and deployed to the consumer shall not be changed any more.
5. Dependency Inversion Principle (DIP)
The last of the SOLID principles is about keeping software pieces completely decoupled. Besides the model the rectangle is in, it also has some other models (B, C and X), which are in a separate dll, jar or any other file. The order is the way they depend on each other. So, the rectangle’s model depends on B, which depends on C, which depends on X. Remember the requirement about the square? Consider that one of Pablo's great ideas was implemented and recompiled. Since the compiler needs to access the model details the rectangle relies upon, under some circumstances the models also need to be recompiled and redeployed. In the worst case scenario, the whole CRAP has to be recompiled and redeployed, due to some little detail within the rectangle model. Incredible, isn't it? On the other hand, during development, we want to avoid dependencies explosion impacting upon teams carrying out implementation depending on the model Pablo is about to change.
The examples used within this article are simple ones and do not cover the topic fully. This was never the intention. The intention was to shed some light on common problems in software development and to show how they can be handled. No-one can foresee everything. The human imagination is endless, so one day we may get a requirement that is completely unexpected, for which massive changes are required. To implement it within the existing model might be pretty difficult. Implementing everything up front is too expensive. However the simple principles described above make life easier. The solution is to remain constructive; keep observing the system’s customers’ needs and modify things on the fly, once you realise that a particular wish will probably be expressed. This approach is known as AGILE, and SOLID helps to keep software in a changeable state.
The principles overlap within the areas in which they apply. One implies another. Finally, they form one whole ideology, which makes the system maintainable, readable and probably enjoyable to work on.
- Clean Code” by Robert C Martin