Why should you care about software engineering principles? Shouldn’t it be enough to just know how to program? These may be some of the questions you might have as a new programmer. To answer those questions simply, software engineering principles are what guides us to write well-designed programs, similar to how having a clear structure for your emails or essays can help to make them more effective and readable. One of such software engineering principles is the S.O.L.I.D principles. Kindly note that this tutorial requires at least basic knowledge in Object Oriented Programming such as inheritance, classes and interfaces.
Coined and theorized by Robert C. Martin in the year 2000, S.O.L.I.D principles are a set of 5 software engineering principles that helps to guide programmers to coding programs that are easy to maintain and extend.
S.O.L.I.D is an acronym that stands for:
S — Single Responsibility Principle
O — Open-closed Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle
Let’s explore what each principle means and how you can implement them in your own programs:
1) S — Single Responsibility Principle
As the name of the principle suggests, the Single Responsibility Principle states that “a class should have one and only one reason to change” or in short, a single responsibility. If a class has many different responsibilities, it will be harder to debug your program as the root cause of the bug will be harder to pinpoint. Furthermore, extending one responsibility may result in the breaking or the changing of other responsibilities in the same class due to how coupled the responsibilities since they belong to the same class.
Let’s take a look at a simple example. In this example, we are trying to model a small auditing company that has the following responsibilities, auditing, hiring new employees and managing existing employees.
In the above model, we can see that there is only one class representing the whole company which is responsible for all three of the responsibilities mentioned above. This is what a model would look like if no consideration is placed on the Single Responsibility Principle.
When we consider the Single Responsibility Principle, we would model the example as shown above where three separate classes, Accountant, Human Resource and Manager are created to handle each of the three responsibilities. In this way, we decouple the different responsibilities, reducing the chance of breaking other responsibilities when a responsibility is modified.
Do note that the above model can be decoupled even further as each method call above can be split into even smaller method calls. However, if a change in the application does not cause two or more responsibilities to change, further decoupling the model may lead to an increase in complexity that is not necessary or helpful.
2) O — Open-closed Principle
The open-closed principle states that a code unit, such as a class, interface, etc, should be open for extension but closed for modification. Let’s take a look at the following example where we are modeling an audio player that is able to currently take in a CD and play the audio clip in it.
Let’s take a look at how we may extend the audio player’s functionality to be able to play audio clips from thumb drives as well. We may be inclined to simply add a new method into the class that takes in a thumb drive and play the audio clip in it as shown below.
However, as you can see, it has broken the open-closed principle as a modification to the class has to be made to extend the functionality of the audio player. This may lead to the introduction of unwanted side effects or bugs by modifying an already implemented class. So how can we ensure that the above example meets the open-closed principle? Let’s take a look below.
By creating an interface called Device, we are able to keep Audioplayer closed to modification while at the same time extending the types of devices that it can read by simply adding new classes that implement the Device interface. In this way, we were able to keep Audioplayer open for extension and close for modification.
3) L — Liskov substitution Principle
Liskov Substitution Principle states that “if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program”. What this means is that, if a class T is a subclass of a class S, we should be able to substitute all instances of class T with its parent class, S. This is done so as to ensure that inheritance is not used without careful consideration, where classes that should not be inheriting from certain classes ends up doing so and hence inheriting undesirable or redundant properties. Let’s take a look at the simple and common example to illustrate a violation of this principle.
In the above example, the Penguin class inherits from the Bird class. In this way, it also inherits the fly method from the Bird class. However, Penguins are not able to fly, and doing so causes certain properties of the Penguin class to be broken. In order to solve this violation, we can introduce two additional subclasses, flying birds and flightless birds, and have the Penguin class inherit from flightless birds instead. This will also help us to remove the assumption that all birds can fly as seen from the example above.
4) I — Interface Segregation Principle
Interface Segregation Principle states that classes that implement interfaces should not be forced to depend on methods that they do not use. This is done to avoid forcing programmers from having to write dummy methods, implement redundant methods. This also helps to make the overall code more readable as it is no longer polluted with redundant code. All of these make the maintenance and debugging process much easier too. Let’s take a look at a simple example of how this principle may be violated and how we may fix such a violation.
In the above example, both IT_Employee and HR_Employee classes implement the Employee interface. However, IT_Employee will never need to do HR work while HR_Employees will never have to do IT work. This results in both classes having to implement redundant methods due to having implemented the Employee interface. To resolve this violation, we can simply have another layer of interfaces for each of the different types of employees. Take a look at the illustration of the solution below.
5) D — Dependency Inversion Principle
Dependency Inversion Principle consists of two parts:
1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend on details. Details should depend on abstractions.
Essentially, what the dependency inversion principle means is that high-level modules should be decoupled from low-level modules. This is so that when there are changes made to low-level modules, high-level modules need not change and also allows for better readability and scalability of the code. This can be done by having high-level modules provide interfaces specifying what low-level modules should implement but not how they should implement it. In this way, the high-level modules are decoupled from the low-level modules. Take a look at the example shown for the open-closed principle below.
From the example, we can see that the high-level module, the AudioPlayer, is independent of changes in the low-level modules like the CD and Thumbdrive. Furthermore, the interface Device is used to specify to the low-level modules what the audio player requires but does not specify implementation details. The low-level modules are both free to implement getAudio in a way that best fits them and the high-level module need not need to know the details to be able to carry out its own functionalities.
From this example, we can also note that when adhering to some principles, we are also indirectly adhering to others. In the above example, by ensuring the open-closed principle is adhered to, we are also adhering to the dependency inversion principle.
To conclude, although these principles can help us to write more readable and scalable codes, it is important to exercise judgment as taking these principles to the extreme could introduce unnecessary complexities in your code, making them harder to maintain and debug.
Robert C. Martin (2000). Design Principles and Design Patterns