SOLID Design Principles
Software design is used to imagine the structure of a system and to determine the potential cost and time required to build said software. Of course, these should be used as estimates; successfully determining the cost and time required to build software is a task that probably has never been achieved in the real world, or resulted in either compromise in quality, or a feature-stripped piece of software that does not provide its intended value. After all the time and effort of building software, if you are so lucky to find yourself in the case of successfully building a piece of software that provides value, this software will most likely go into a maintenance phase. This maintenance phase will most likely comprise roughly 80% of the entire project’s development time and cost, provided that it continues to provide value to the organization (Gupta and Sharma, 2015). Therefore, it is imperative that from the initial development stages, we implement design principles that will result in indirectly reducing the cost and effort for the maintenance phase, if we build something worth maintaining.
This is where SOLID design principles come in. The principles were theorized by Robert C. Martin, and the acronym was coined by Michael Feathers in the year 2000. The five principles are as follows:
1. S: Single Responsibility Principle
2. O: Open/Closed Principle
3. L: Liskov Substitution Principle
4. I: Interface Segregation Principle
5. D: Dependency Inversion Principle
It is important to note that these principles should be used only as guidelines and although they will provide tremendous value when implemented correctly, it is very easy to over-implement these principles, which can result in a project that is oh so terribly difficult to navigate and debug. Especially with the Single Responsibility and Interface Segregation Principle.
S: Single Responsibility Principle:
A class should have only one reason to change (Martin, 2000). If a class has more than one reason to change, then it is considered fragile, which directly correlates to one of the principles of rotting design. When a piece of software is fragile then it is more prone to break in areas that have no relationship with the changed component. Over time every fix to this class makes it worse, introducing more problems and increasing the probability of breakage. This results in software that is impossible to maintain and ultimately will result in the organization implementing exploratory testing for every small change made to ensure that the rest of the untouched features still work. Meaning the time to deliver is increased as well as the cost for every change or fix implemented. All of this can be avoided by logically separating your classes. This does not mean that your class should do one trivial thing only; instead it should complete one task and complete it well.
Example: Instead of creating a single class called NotificationProvider to handle both sending SMSs and emails, create two separate classes called SmsProvider and EmailProvider. This is a simple example of how you can implement the Single Responsibility Principle today right now in your project, and no, it is not too late. This will provide value even if 99% of the codebase does not follow this principle.
O: Open/Closed Principle:
Software Entities like classes, modules, and functions should be open for extension but closed for modification (Martin, 2000). You should try to design and implement classes that will never change when the requirements change. Instead, rather extend the behaviour and existing functionality by adding new code instead of modifying the existing code that works. This can be done by leveraging interfaces or inheritance or even the strategy pattern.
Example:
In the above piece of code, we will assume that the CalculateDiscount method was created to determine a discount based on the value of the purchase. We will assume this code is live and working as intended. Requirements have changed and now the discount needs to be determined on the type of item being sold instead. To successfully implement the Open-Closed principle, you would extend the existing functionality without modifying the existing working code. This is achieved by overloading the original method with a new parameter, thereby leaving the existing working code untouched and extending the functionality of the discount entity. Of course, this extension can be achieved by various other means as well.
L: Liskov Substitution Principle:
Derived types must fully support the substitution of their base types. Subtypes must be replaceable by their base types (Martin, 2000). In other words, one class should not be extended from another just because there are commonalities between the two. An example is required for this to make sense.
So this simple example above shows that there is a base class of type Employee, and this object has a salary. Developers, managers as well as CEO’s all have a Salary as well. So why not inherit from the Employee class? Well, the method SetManagedBy is the problem here. In the highlighted code you can see that we are assigning the base employee object to that of the manager object, then assigning this object a manager. If you were to change that line to new CEO( ), the next line would throw an exception. This directly goes against the Liskov substitution principle. (This exception is more likely to occur if reflection is used, or the strategy pattern is implemented.)
So a simple rule of thumb: if every property or method is not used by the child of a base class, then that class should not inherit from the base class. This can easily be mitigated by creating smaller more specific interfaces: you could instead have an IEmployee interface, and an IManagable interface, and then implement their signatures appropriately.
I: Interface Segregation Principle:
Clients should not be forced to depend upon interfaces that they do not use (Martin, 2000). This is a simple and easy to follow principle. We will use the previous example but with interfaces to demonstrate this.
This is what it would look like if we implemented the previous example using interfaces. Notice that the CEO entity does not need the ManagedBy property or the SetManagedBy method; however it is being forced to implement it due to the IEmployee signature. We can instead create two interfaces, namely IEmployee and IManageable.
As you can see above, the CEO entity should only implement the signature of the IEmployee interface and does not need to implement the IManageable interface as it does not make use of its signature. The above results in the Interface Segregation Principle being followed accordingly.
The actual benefit of implementing this is:
• Higher Cohesion, which makes the code more robust
• Lower Coupling, which provides better maintainability and resistance to change
D: Dependency Inversion Principle
High-Level Modules should not depend upon Low-Level Modules (Martin, 2000). Both should depend upon Abstractions. Abstractions should not depend upon Details. However, details should depend upon abstractions. Dependency inversion is the strategy of depending upon interfaces or abstract functions, rather than concrete functions and classes, because the abstractions are less likely to change than their concrete counterparts. This can be achieved by using dependency injection implemented with interfaces. An example should assist.
In this simple example, we can see that we have a registrationController, and a notification is being sent once a user has registered, the INotificationProvider interface is being used here and the SMSprovider is being injected here. If for some reason, the business has decided to change the notification stream to an email, the required change would be to create a new EmailProvider that implements the INotificationProvider interfaces signature and change dependency injection. Those two small changes would be enough for this migration to be completed. The changes would look like this. For simplicity, I will pass the “injected” provider through the constructor, but as you can see in the original code, nothing needs to be changed to keep the existing functionality; only a new provider is created, and the dependency injection is updated.
A real-world example of this could be, you have implemented a third party SmsProvider, and it has now been live for two years and is being used throughout the project. Now Amazon has released their own sms services which costs considerably less, you must use it. By using Dependency Inversion you can migrate to those amazon services considerably faster, as no matter how many references to the old SmsProvider existed, there will still only be two changes to complete the entire migration.
Conclusion
Software is volatile. You don’t know if what you have built will succeed. However, you do know that you can build something robust, scalable and considerably easier to maintain, which can result in you very promptly building new features and responding to changes in the market, increasing your chances of succeeding. By implementing SOLID design principles, you can leverage the interface segregation principle to migrate to other services rapidly, make use of the single responsibility principle to reuse features and rapidly develop new features; you can have the reassurance that your changes will not affect existing working features by implementing the open closed principle, And have even less unexpected exceptions due to the Liskov design principle. All of these benefits will set you up for a successful piece of software.
References:
Chebanyuk, E. and Markov, K. (2016) ‘An approach to class diagrams verification according to SOLID design principles’, MODELSWARD 2016 — Proceedings of the 4th International Conference on Model-Driven Engineering and Software Development, (Modelsward), pp. 435–441. doi:
Gupta, A. and Sharma, S. (2015) ‘Software Maintenance : Challenges and Issues’, International Journal of Computer Science Engineering (IJCSE), 4(01), pp. 23–25.
Martin, R. (2000) ‘Design principles and design patterns’, Object Mentor, ©, pp. 1–34. Available at: http://www.cogs.susx.ac.uk/users/ctf20/dphil_2005/Photos/Principles_and_Patterns.pdf%5Cnhttp: //scm0329.googlecode.com/svn-history/r78/trunk/book/Principles_and_Patterns.pdf.
Singh, H. and Hassan, S. I. (2015) ‘Effect of SOLID Design Principles on Quality of Software: An Empirical Assessment’, 6(4), pp. 1321–1324.
https://www.youtube.com/watch?v=5RwhyZnVRS8&ab_channel=IAmTimCorey