SOLID Principles for software development - gouthamv03/notes GitHub Wiki
SOLID
- Single Responsibility Principle (SRP)
- Open Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
These principles when used together solve
- Fragility: Code breaks in many places when it is changed
- Rigidity: Hard to change code even for simple things. Requires a cascade of changes in different modules.
Single Responsibility Principle
Each module should have an independent responsibility. If a module is doing more than one thing, eg, doing business logic, serializing, db operations etc, we need to break them down into their separate modules
Open Closed Principle
Classes, functions and modules must be open to extension but closed for modification. Assume all written code is immutable. Create new classes for new functionality. This can be done by inheritance where the new class can extend the original class. But it introduces coupling. To avoid it, we can use the Strategy pattern where we use interfaces instead and a factory method to return appropriate objects for each type.
Liskov Substitution Principle
Any objects of a type can be replaced by objects of a derived type without affecting the correctness of the program. Instead of thinking in terms of "is-a" relationships, start thinking of "is substitutable by" when creating sub-classes.
Eliminate incorrect relationships between objects
- Remove an incorrect extends relationship and create a separate class.
- Remove an interface that has inapplicable methods and create a different one.
Use "Tell Dont ask" principle to avoid type-checking and casting
Any actions that are done specifically for a type should be done directly within that type's method
Checklist
- Make sure the derived type can fully substitute the base class
- Keep base classes lean and focussed
- Keep interfaces lean and focussed
Interface Segregation Principle
Clients should not be forced to depend on methods that they don't use.
Symptoms:
- Methods that do nothing or throw an exception in a derived class.
- Interfaces with a lot of methods and low cohesion, where an interface has methods that don't really belong there.
If code is in our control, this can be fixed by breaking up heavy interfaces to smaller focussed interfaces. If not, the adapter pattern can be used to create a usable adapter interface in our code.
Dependency Inversion Principle
- High level modules (business logic or what or abstract) should not depend on low-level modules (implementation details or how or concrete, eg. IO, logging). Instead both should depend on abstractions.
- Abstractions should not depend on details, details should depend on abstractions. Abstractions are typically interfaces or abstract classes.
Business logic --> Abstraction of Low-level logic <--- Low-level logic
This can be implemented by removing direct dependency of low-level logic in high-level logic and replacing it with an abstraction. Factory pattern can be used to "new" up the low-level module but return an object of the abstract type.
Dependency injection (DI)
Allows us to create dependent objects outside the class and then use them in the class. Eg. Get the low-level module passed in as part of the constructor.
The factory pattern had introduced some dependency above. With this approach, we can eliminate the dependency and push the factory to the calling client. However, DI can also get complicated with multiple inter-class dependencies.
Inversion of control
Control of creation, configuration and object lifecycle is passed to another container or framework. Eg. Spring.
It is useful for some objects like services, controllers, data access but not for other ones that have limited uses.