This is the first of a two part series explaining the SOLID design principles.
As developers who has coded for some time, you may tend to feel that you know most of the key concepts of the object-oriented programming paradigm, which in its basic form talks about encapsulation, inheritance, and polymorphism. For the most part, these concepts are all you need to solve a variety of software problems. But these concepts by themselves fall short in preventing developers from writing bad OOP code and managing dependencies.
The SOLID principles serve as guidelines for object oriented design. If applied properly, they could help you as a developer while working on a software application to eliminate code smells
( a surface indication of deeper design problem) by making you refactor source code as many times as is required until your code is both legible and extensible.
S.O.L.I.D is a mnemonic acronym coined by Robert C. Martin for Michael Feather’s “first five principles” of object-oriented design. SOLID forms part of the strategies of agile.
The SOLID design principles are five basic principles of object-oriented design. Before I dive into the details let me makes some clarifications to aid your understanding of these principles. First Let me be clear on the difference between object oriented design and object oriented programming as these terms tend to be used interchangeably, but in reality are two similar but different concepts.
Object Oriented Design is the processes of planning a system of interacting objects which solve a given software problem. The keyword here is the “planning”, it is not the actual implementation which done by actual coding. This done usually done in the elaboration phase of the rational unified process during the creation of the software architecture. Object oriented programming is just a style of programming or in other words, a programming paradigm which uses the process of Object oriented design to create objects and their interactions using a programming language some of which may support OOP.
Object oriented programming is a superset of two programming paradigms:
- Prototype-based programming: is a paradigm of object-oriented programming in which behavior reuse is performed by cloning existing objects that serve as prototypes.
An example of how this works in practice is the animal kingdom. An “Animal” object would represent the properties and functionality of animals in general. A “Dog” object would be cloned from the
“Animal” object, and would also be extended to include the general properties specific to dogs.
- Class-based programming: This is the most popular and developed model of OOP, supported by many popular OOP languages such as C++, Java, C#, just to name a few. In this paradigm, reuse is achieved by defining classes (template) and creating instances of the class called objects, as opposed to creating from an already existing object (cloning) as in prototype-based programming. An instance Classes serve as blueprints for the creation of objects which consists of data called attributes and operation which can be performed on the data. Operations are called methods.
Due to the fact that class-based approach is the most popular of the two, the solid principles tend to be built around this approach of OOP.
The SOLID design principles are
S– Single Responsibility Principle
O– Open/Closed Principle
L– Liskov Substitution Principle
I– Interface Segregation Principle
D– Dependency Inversion Principle
Single Responsibility Principle:
This principle states that every class of a software should be responsible for a single part of the software’s functionality, and the responsibility must be encapsulated by the class. In other words, a class should have just one responsibility.
According to its creator Robert Martins, a responsibility is a “reason to change”, and he concludes that a class should have one, and only one, “reason to change”.
A “reasons for change” depends on how the requirements of the project could change.
If the application is not changing in ways that cause what is assumed to be two responsibilities to change at different times, then we can consider them as one responsibility.
As an example let’s consider a class that adds a new user to a database and send a welcome email.
Imagine such a class can be changed for two reasons. First, we may want to change how we save the data to the database. Second, we want to change the formatting of the email. The single responsibility principle says that these two aspects of the problem are really two separate responsibilities,
and should, therefore, be in separate classes. It would be a bad design to couple two things that change for different reasons at different times.
The most common way to break the single responsibility principle is by the creation of GOD classes. These are classes that keep track of a lot of information and have several responsibilities. God classes should be avoided at all times.
The Open/Close Principle is the second principle (represented by “O”) of SOLID and was originally coined by Bertrand Mayer in his 1988 book, Object-Oriented Software Construction. It states:
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”
Your first thought and indeed mine when I first learned about this was, “how is that even possible?”, but let’s go through it together. This principle suggests that the source code of a software entity should not be modified in order to change its behaviors (Closed) after is has been unit tested, but rather new behavior can be added by using either implementation inheritance (base class inheritance) or interface inheritance (pure abstract base class inheritance or interfaces) to extend/modify its functionality. In this way, Let’s go through some code example to drive this concept home, below are two code examples written in C#
Example using implementation inheritance:
Here we see a sample application that uses a class A, which defines a set of operations to solve a hypothetical problem. Operation 1 is executed, before operations 2 and 3. Let’s say this program is unit tested and passes it integration test, and after a few years a better algorithm is developed to solve operation 2, this principle proposes that instead of editing the code of class A, we should rather do the following:
Here you can see that by using implementation inheritance we were able to extend the functionality of Class A, without modifying the code of A. This is a rather simple example of this principle, but the implications are more pronounce in larger applications.
Example using interface inheritance:
Here we use an interface i to define the logic of our application. The interface i is implemented by class A here. But let’s say we have a better way of solving this hypothetical problem. Instead of editing the source, we can easily just create a new class B which improves on class A’s implementation.
Here you see that we completely changed the functionality of our application without changing the old source code in class A. A more robust solution will be to use a factory method for classes A and B and then we wouldn’t need to touch even the main method.
In both cases the, goal of this principle is to eliminate introducing new bugs into existing code.
This is the end of this part of the blog post, the second part is available for reading here.