Robert C. Martin grouped these 5 principles under the acronym "SOLID" in the early 2000s. The acronym was first mentioned in a 2004 paper, Design Principles and Design Patterns
Let’s dive deep into each of these
📣 PostHog (Sponsor)
Before we start, special thanks for PostHog for sponsoring this week’s newsletter
PostHog is an open-source, all-in-one suite of product and data tools like product analytics, session replays, A/B testing, and surveys.
Product For Engineers is a free newsletter by PostHog dedicated to helping engineers level up their product skills.
Subscribe for free using button below to get curated advice on building great products, lessons (and mistakes) from building PostHog, and deep dives into the strategies of top startups.
1. Single Responsibility Principle (SRP)
Definition
A class should have only one reason to change, meaning it should have only one responsibility or job.
Why It's Important
If a class has multiple responsibilities, a change in one responsibility might require changes in the other. This can lead to tightly coupled code, making it harder to maintain, test, and understand.
Detailed Example:
Let's consider an example where a User
class handles both authentication and data management.
Initially, it might seem convenient to put everything in one class:
This User
class is now responsible for:
Authentication
Managing user data.
Sending notifications.
If you need to change how notifications are sent, you’d have to modify the User
class, potentially breaking something else.
To follow SRP, you can split this into three different classes:
Now, each class has a single responsibility:
Authenticator
handles authentication.UserDataManager
handles user data.NotificationService
handles notifications.
The code becomes easier to manage, test, and extend.
2. Open/Closed Principle (OCP)
Definition
Software entities like classes, modules, and functions should be open for extension but closed for modification.
Why It's Important
When a class is closed for modification, you reduce the risk of introducing bugs into existing functionality. Instead of modifying existing code, you add new code, which is easier to test and integrate.
Detailed Example
Consider a scenario where we want to calculate the area of different shapes. Initially, you might write something like this:
This approach violates the OCP because every time a new shape is added, you need to modify the AreaCalculator
class, increasing the risk of errors.
A better approach here is
Now, if you want to add a new shape, such as a Triangle
, you simply create a new class extending Shape
:
The AreaCalculator
class doesn’t need to be modified.
3. Liskov Substitution Principle (LSP)
Definition
Subtypes must be substitutable for their base types without altering the correctness of the program.
Why It's Important
If derived classes violate the expectations set by the base class, it can lead to unexpected behavior, making the code less reliable and harder to understand.
Detailed Example
Consider the example of birds. If you have a base class Bird
with a fly
method, you might assume that all birds can fly. However, not all birds can fly, so this assumption is flawed:
If you try to substitute Ostrich
for Bird
, it will break the program:
To adhere to LSP, you can create a more accurate class hierarchy:
Now, substituting a Sparrow
or Ostrich
for Bird
doesn’t violate the expectations set by the base class.
4. Interface Segregation Principle (ISP)
Definition
A client should not be forced to depend on interfaces it does not use.
Why It's Important
Large interfaces are harder to implement and maintain. By splitting them into smaller, more specific interfaces, you make your code more modular, flexible, and easier to understand.
Detailed Example
Let’s say you have a Worker
interface that includes both working and eating behaviors
Now, suppose you have a HumanWorker
and a RobotWorker
. The RobotWorker
doesn’t need to implement the eat
method, but it’s forced to do so:
To follow ISP, you should split the interface:
Now, RobotWorker
only implements what it needs (Workable
), making the code cleaner and more flexible.
5. Dependency Inversion Principle (DIP)
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
Why It's Important
By depending on abstractions, you make your code more modular, testable, and easier to maintain. It allows for easier swapping of implementations without affecting the higher-level logic.
Detailed Example
Consider a scenario where a LightSwitch
class directly depends on a LightBulb
In this case, the LightSwitch
class is tightly coupled to the LightBulb
implementation. If you want to switch to a different type of light, you’d have to modify the LightSwitch
class.
To follow DIP, you can introduce an abstraction (Switchable
interface):
Now, the LightSwitch
class depends on the Switchable
interface, not a specific implementation like LightBulb
. This makes it easy to change or extend functionality without modifying the LightSwitch
class.
For example, you can switch to a Fan
without altering the LightSwitch
This approach adheres to DIP by ensuring that high-level modules depend on abstractions, promoting loose coupling and greater flexibility in the code.
By understanding and applying these SOLID principles, you can significantly improve the design, maintainability, and scalability of your code. Each principle helps in addressing common software design problems, making your code more robust and easier to work with.
If you like this article, please share and repost so it reaches more folks. You can hit the like ❤️ button at the bottom of this email to support.