Practical Use Of SOLID Design Principles In Code
We want our software to adapt to changing requirements and allow for decentralized development. The SOLID design principles can be used to exploit many options provided by object-oriented languages for above. Here’s a cheatsheet for putting these principles into practice in your code:
What Are SOLID Design Principles?
SOLID is an acronym for Object-Oriented design principles coined by Robert Martin:
- Single Responsibility Principle (SRP)- A class should have one and only one task/responsibility.
- Open/Closed Principle (OCP)- Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. Design and implementation should be done in such a way that new functionality may be introduced with the least amount of code modifications.
- Liskov Substitution Principle (LSP)- The derived class should be able to substitute the base class without violating the intent or semantics of the abstraction it is inheriting from or implementing.
- Interface Segregation Principle (ISP)- Each interface should be responsible for a specific task. Clients should not be forced to depend upon methods that they do not use.
- Dependency Inversion Principle (DIP)- Each module should be separated from other using an abstract layer which binds them together. The higher-level policy abstractions drive the implementation details, not the other way around.
What Does It Mean To Code Using SOLID Principles?
- Maintain a consistent level of abstraction across all methods. If one method operates at one level of abstraction while another operates at a different level, the class could have multiple responsibilities.
- Check if there are attributes that are only used by a subset of methods in the class. This could suggest that these attributes and methods are a separate responsibility from the rest of the class.
- Use Design by Contract for Liskov Substitution Principle. The idea is that a class and its methods have an explicitly stated contract in the form of pre/post conditions. A sub type can weaken (but not strengthen) the precondition for a method it overrides. A sub type can strengthen (but not weaken) the postcondition for a method it overrides.
- When should a class change should be based on the business’s view of the concept rather than a purely logical separation of concepts.
- Avoid any static code that maintains state. Static code should be utility/stateless.
- Use Strategy pattern instead of switch statement as latter usually implies multiple responsibilities.
- Use extension points, callbacks, overridable methods to make software open for adding new functionality without modifying existing code.
- Use generics when a single implementation can work for multiple independent types.
- Make sure that new derived classes are extending the base classes without changing their behavior.
- Use Decorator pattern, Factory Method, Observer pattern to design an application that must be easy to change with minimum changes in the existing code.
- Use Adaptor pattern when dealing with external dependencies and existing ISP violations as it converts the interface of a class into another interface clients expect and allows classes that would otherwise be incompatible to function together.
- Prefer multiple client interfaces rather than one general interface and each interface should have a specific responsibility. A client can implement multiple interfaces if required. Interfaces should not have any external dependencies.
- Minimize accessibility of a class or method to the least privilege level required. This protects ability to make future changes by providing proper encapsulation around implementations.
- Leverage Command Query Responsibility Segregation (CQRS) if you need a fast/primitive access to data layer for user queries.
- Use layered design and have separate layers for user interface, service, business logic, implementation and data access.
- Do not combine interfaces and implementations in one package. Instead, provide capability to weave in the implementation via run-time mechanism (like Spring IoC).
- Code that relies on the interface should only ever know about the interface. It should not know about any of the specific classes that implement the interface.
- Prefer Aggregation and Composition over Inheritance. Aggregation is a “has a” relationship and Inheritance is a “is a” relationship. For instance: Car is a Vehicle, so Car can extend a Vehicle. But Car has Wheels; Car is composed of a collection of Wheels it does not extend it.
Following these principles is not a cure-all and won’t avoid design issues. That said, when principles followed correctly, they lead to better code in terms of readability, extensibility, maintainability, and testability.
Thank you for reading! If you found this helpful, here are some next steps you can take:
- This blog is part of my System Design Series. Check out the other blogs of the series!
- Send some claps my way! 👏
- Follow me on Medium & subscribe below to get a notification whenever I publish! 📨