4 minute read

Updated 27.11.2023

What is SOLID

SOLID is a set of five design principles authored by Robert “Uncle Bob” Martin, related to object-oriented programming:

  • Single Responsibility Principle: A class should have only one reason to change.
  • Open/Closed Principle: Software entities should be open for extension but closed for modification.
  • Liskov Substitution Principle: Subtypes must be substitutable for their base types.
  • Interface Segregation Principle: A client should not be forced to depend on interfaces it does not use.
  • Dependency Inversion Principle: High-level modules should not depend on low-level modules; both should depend on abstractions.

The intended goal of this design was to make the code more maintainable and clean, with much better option of extension through scalable efforts. Owing to decoupling and modularity, it should also increase its testability.

Benefits of SOLID (at least in theory):

  • Enhances code maintainability and readability.
  • Facilitates code scalability and extension.
  • Reduces code coupling and dependencies.
  • Promotes modular design and easier testing.
  • Fosters adaptability to changing requirements.

Single responsibility principle (SRP)

Each object / class should do one thing, and one thing only. (If something is for everything, it’s good for nothing). Code is more modular and easier to understand, maintain, and extend.

Example:

User class that handles both user authentication and user data storage. Now split these responsibilities into two separate classes: AuthenticationService and UserDataStorageService. Separation of security layer from business logic makes it easier to modify or extend authentication without affecting user data storage and vice versa.

Open / closed principle (OCP)

Objects (classes, entities, modules, functions, etc.) should be extensible but not modifiable. Be able to add new functionality without altering existing code. This is often achieved through the use of interfaces, abstract classes, and polymorphism.

Example:

In report generator, one could introduce a mechanism where new report types can be added without modifying existing code.

Liskov substitution principle (LSP)

Objects should be replaceable by their base types without breaking the program. If a class is a subtype of another class, it should be able to replace the parent class without affecting the program’s behavior. This ensures that inheritance hierarchies are designed in a way that maintains logical consistency.

Example:

In a banking application, there is a base class called Account representing various types of bank accounts. The Account class has a method called withdraw for withdrawing funds. Subclasses represent specific account types, such as SavingsAccount and CheckingAccount. The PO introduces a new requirement that imposes a daily withdrawal limit on savings accounts. Ignoring LSP, he might simply override the withdraw method in the SavingsAccount class to include logic for withdrawal limit. With LSP, an interface Withdrawable, that declares the withdraw method. Both Account and SavingsAccount can then implement or extend this abstraction. SavingsAccount class is Withdrawable according to the requirements (imposes the limits on withdrawal), but it can still be used as a substitute for the base Account: it does not override initial logic of the Account class.

Interface segregation principle (ISP)

Small specific interfaces are better. Clients (classes or modules) should not be forced to implement interfaces they don’t use. Let them use only features they need.

Example:

Internal library of a company consists actually of one big interface. In order to use some needed feature, like export to pdf, a programmer needs to implement all non-abstract method of this interface. If the library has more interfaces with specialized methods, he could only implement the one responsible for pdf printing.

Dependency inversion principle (DIP)

Don’t depend on concrete implementations. Use IoC and DI. 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. Decoupling or loose coupling gives flexibility.

Example:

Database migration to the cloud - an app is dependent on Repository interfaces, not on a concrete implementation of database provider.

YAGNI vs SOLID: a critique of the latter

Strict application of SOLID (like let’s do everything perfect, according to the book, because my guru says so) may not always be suitable for every context.

Eschew programming for theory. Programming is for business.

Pragmatic decision-making, considering project size, team expertise, deadlines, and specific constraints, is crucial to find balance between SOLID theory and practical project goals.

In small or straightforward projects, applying all SOLID principles in 100% might be considered over-engineering. The additional abstraction and structure introduced by SOLID, plus workload needed for it, may be an overkill for projects with simple requirements. Avoid unnecessary complexity, beware of wasted effort. Focus on needs and requirements. Over-engineering for simple projects is a crime.

YAGNI: You Aren’t Gonna Need It!

Deadlines, practical goals, project requirements, concept-proofing and prototyping are often enemies of SOLID. Not to mention that poor communication within a team plus different levels of understanding the concept are frequent reasons of misinterpretations that could lead to suboptimal implementations.

SOLIDification of legacy code or maintenance code is challenging and time-consuming, sometimes not feasible at all or simply not allowed. A pragmatic approach might involve gradually introducing SOLID principles during ongoing development.

Provided that it won’t be counterproductive.