S.O.L.I.D PRINCIPLES
In software development world, where several technological innovations and advancements prosper each passing minute, no matter which latest technology is used, foundation pillars should always adhere to well-designed and architected software built by following set of design practices and principles. Otherwise such software will perish over a period of time, when necessary enhancements are to be done but software is not easily extensible to fulfil ever-growing business demands in a competitive marketplace, in as much less time as possible.
SOLID (developed by Robert C. Martin) is a collection of programming practices used across object-oriented design spectrum. The purpose of these principles is to make software designs more understandable, easier to maintain and easier to extend. It is a coding standard which states that all developers should have a clear concept for developing software to avoid bad design. When applied properly, it keeps code more extendable, logical and easier to read.
When a developer builds a software following bad design, code becomes inflexible and more brittle, small changes in software result in bugs.
Though it takes good amount of time and discipline to understand but writing code by adhering to these principles improves code quality gradually and will help to build most well-designed and scalable software. SOLID is a conglomerate of below listed 5 coding practices:
- S: Single responsibility principle
- O: Open/close principle
- L: Liskov substitution principle
- I: Interface segregation principle
- D: Dependency inversion principle
Single responsibility principle
A class should have one, and only one responsibility/ reason to change.
All methods and properties of a class should be cohesive i.e. directly relate to its responsibility, by working towards a common goal. When a class serves multiple purposes/ responsibilities, it becomes really difficult to read, repair or to expand it, at which point it should be made into a new class. This principle is very noticeable in case of an application that begins to grow with time, with enriched functionalities added to already existing classes and their capabilities.
Example:
Problem: Employee class has to fulfil 3 different sets of responsibilities, all mixed up within one class namely Calculation logic, Database logic, Reporting logic
When there are multiple responsibilities combined into one class, it becomes difficult to change one part without breaking others. Mixing responsibilities also makes the class harder to understand and test thereby decreasing cohesion.
Solution: Split class into 3 different classes, each fulfilling only one responsibility namely CalculatePay, SaveEmployee, DescribeEmployee.
Open/close principle
Software entities should be open for extension, but closed for modification.
Software entities (classes, modules, functions, etc.) should be extendable without actually changing contents of class being extended. If this principle is followed strongly enough, it is possible to then modify behaviour of code without ever touching the piece of original code. The application must be ready for extensions, as it has to continuously evolve based on the changes of external system.
Example:
Problem: For below class, new payment method to support credit card support needs to be added.
Adding an “if” statement would the simplest solution, but it will enforce a change to existing class violating OCP.
Solution: Definer an interface PaymentMethod, create 2 implementation classes namely CashPayment, CreditPayment where each class will handle appropriate payment method. With this approach, when another payment method must be added say Overdraft; a new class can be created OverdraftPayment, to handle the specific logic without necessitating any change to existing classes.
Liskov substitution principle
This principle was coined by Barbar Liskov regarding data abstraction and type theory. It also derives from the concept of Design by Contract (DBC) by Bertrand Meyer. Barbara Liskov and Jeanette Wing formulated it succinctly in 1994 paper as follows.
Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.
A derived class is substitutable for its base class if:
- Preconditions cannot be strengthened in a subtype.
- Postconditions cannot be weakened in a subtype.
- Invariants of the super-type must be preserved in a subtype.
Robert Martin made definition sound more concisely in 1996 :
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.
Or simply : Subclass/derived class should be substitutable for their base/parent class.
Any implementation of an abstraction (interface) should be substitutable in any place, that the abstraction is accepted. While coding using interfaces, there is not only have a contract of input which interface receives but also the output returned by different classes implementing that interface; they should be of the same type. Functions that use pointers or references to base classes must also be able to use class objects that inherit from base classes, without having a thorough knowledge of these objects.
Example:
Problem: The Square class derives from a Rectangle class and always assumes that width is equal with height. If Square object is used in a context where Rectangle is expected, then unexpected behaviour may occur because dimensions of a Square cannot (or rather should not) be modified independently.
Solution: Modify setter methods in Square class to preserve Square invariant (i.e., keep dimensions equal).
Interface segregation principle
Clients should not be forced to implement unnecessary methods which they will not use.
Break interfaces in many smaller ones, so they better satisfy exact needs of clients. Avoid ripple effect and repetition by dividing software into multiple, independent parts. Do not add additional functionality to an existing interface by adding new methods, instead create new interface and let appropriate class implement multiple interfaces if needed i.e. Many dedicated interfaces are better than one overall. The interface should give a specific shape to class and methods that must be implemented within the class, should be common to all implementation classes.
Example:
Problem: The interface Messenger has several methods, where each method fulfils different piece of functionality. Classes which implements Messengers interface are forced to implement all methods, though some of them are not even needed or unwanted and which would only be applicable to other Classes.
Solution: Split Messenger interface into multiple interfaces each having cohesive set of functionalities.
Dependency inversion principle
Depend on abstractions, not on concretions.
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
With Dependency Inversion, lower-level modules can be easily changed by other modules just changing the dependency module and High-level module will not be affected by any such changes. Usually by abstraction, we mean an abstract class or interface. Essentially, this means that we are introducing a certain abstraction, which allows us to exchange individual elements of the program with other ones more suitable for a specific task. We try not to enter into classes depending on its smaller parts.
Example:
Problem/Solution: A program depends on Reader and Writer interfaces that are abstractions and Keyboard and Printer are details that depend on those abstractions by implementing those interfaces. CharCopier is oblivious to low-level details of Reader and Writer implementations, once can pass in any Device that implements Reader and Writer interface and CharCopier would still work correctly.
YAGNI - You ain’t gonna need it YAGNI on Wiki
Don't create functionality unnecessarily until it is actually needed, to avoid having code which is not going to be used in any way.
DRY - Don’t repeat yourself DRY on Wiki
The code must always be analysed and improved. By having several classes with similar properties, not only creates confusion but gives rise to redundancy issues. This is a sign which indicates that the code is common and must be separated into another class, which will deal with repetitive tasks in one single place. All the classes will use the same piece of code and thus the probability of error will drop eventually.
KISS - Keep It Simple, Stupid! KISS on Wiki
This rule is often discussed with regard to architecture evaluation. Its essence lies in striving to maintain, an elegant and transparent structure, without adding unnecessary elements.
SOC - Separation of Concerns SOC on Wiki
Create a system in which each part plays a significant role while maintaining the possibility of maximum adaptation to changes. SoC does not refer only to system architecture, but to various issues, e.g. to divide the application into layers namely Presentation, Business logic, Access to data, Database etc. The functionality of the application is divided into separate modules that overlap with as little functionality as possible, giving rise to module program. Each element of the system should have its separate and singular application.
CQS - Command Query Separation CQS on Wiki
Every method in the system should be classified into one of two groups:
- Command - Methods which change state of the application and do not return anything.
- Query - Methods which return something, but do not change state of the application.
Keep SOLID Principles in Your Toolbox
SOLID principles are valuable tools in your toolbox which you should keep in the back of your mind, when designing next feature or application.
References: