Decoding SOLID Principles: Practical Insights with Real-World Scenarios
The SOLID principles help us build better software by giving us clear guidelines to follow. They make our code easier to understand, fix, and improve over time. By sticking to these principles, we can keep up with the ever-changing world of software development.
SOLID is a set of five key principles for designing software objects. It was created by Robert C. Martin, also known as Uncle Bob, who wrote the book Clean Code.
In this blog, we’ll cover the following:
- Why use SOLID principles?
- Design principles
- Five design principles in detail
- 1. Single Responsibility Principle (SRP)
- 2. Open Closed principle (OCP)
- 3. Liskov Substitution Principle (LSP)
- 4. Interface Segregation Principle (ISP)
- 5. Dependency Inversion Principle (DIP)
If we don’t follow the SOLID principles, we might encounter problems in the code.
- Code gets tangled with lots of parts, making it hard to add new things or fix bugs. This can cause hidden problems.
- It becomes tough to test the code, so every change needs a full check.
- The code repeats itself a lot.
- Fixing one problem creates more errors.
However, if we follow the SOLID principles, we can do the following:
- Reduce the tight coupling of the code to reduce errors.
- Reduce code complexity for future use.
- Produce more flexible, manageable, and understandable software code.
- Produce code that is modular, feature-specific, and fully tested.
Design principles
Let us take a look at how the five design principles are defined.
- Single Responsibility Principle (SRP): Each class should have a single responsibility.
- Open-Closed principle (OCP): Classes should be open for extension but closed for modification.
- Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of its subclasses.
- Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don’t use.
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules.
Let’s look at these five design principles in detail using code examples.
1. Single Responsibility Principle (SRP)
“A class should have only one reason to change.” This implies that any class or component in our code should only have one functionality. Everything in the class should be related to just one goal.
In this example, the Authenticator
class handles user authentication and authorization, adhering to the SRP by having a single responsibility. The NotificationManager
class handles both email and SMS notifications separately, maintaining a single responsibility for each type of notification.
Benefits of following SRP:
- Improved maintainability: Each class is easier to understand and manage because it only has one responsibility.
- Enhanced testability: Smaller, more focused classes are easier to test, resulting in better test coverage.
- Code reusability: Classes with a single responsibility can be reused in a variety of contexts, increasing code reusability.
- Scalability: As your application grows, continuing to SRP ensures that you can add functionality without affecting unused parts of your code.
2. Open Closed Principle (OCP)
“A software artifact should be open for extension but closed for modification.” This means that a system can be improved simply by adding new code instead of changing the code core. This ensures that the core code keeps its unique identity while also being reusable.
In this example, we have content classes for text and images that implement the Content
interface. The ContentManager
class can display any content items without modification, demonstrating the Open/Closed Principle in action.
Benefits of Following OCP:
- Stability: Existing, tested code is not altered, reducing the risk of introducing issues when adding new functionality.
- Scalability: You can quickly add additional functionality to your codebase without changing the essential components.
- Maintainability: Code that adheres to the OCP is easier to maintain and extend over time.
- Reusability: You may utilize existing classes and modules in a variety of settings, enhancing code reuse.
3. Liskov Substitution Principle (LSP)
“Objects of a superclass should be replaceable with objects of its subclasses” means that you should be able to use a subclass wherever you’re using the superclass without altering the behavior expected from the original superclass.
In this example, LSP is applied by ensuring that instances of subclasses (AdminUser
, RegularUser
, GuestUser
) can be used interchangeably with instances of the superclass (User
) in the manageSession
function without changing the expected login and logout behavior.
Benefits of Following LSP:
- Correctness: Subclasses operate consistently with their base class, ensuring the program’s integrity.
- Flexibility: Subclasses can be used interchangeably with their base class, enabling flexible and modular architecture.
- Simplicity: The method helps code readability and understanding by providing a clear inheritance structure.
- Extensibility: New subclasses can be simply added, extending the system’s capability without altering existing code.
4. Interface Segregation Principle (ISP)
“Clients should not be forced to depend on interfaces they don’t use.” It means that when you design interfaces, you should break them into smaller, more specific interfaces so that clients (users of the code) only need to depend on the interfaces that are relevant to them. They shouldn’t have to deal with methods or functionalities they don’t actually use.
In this example, the User
class implements specific interfaces for user actions and emergency contact notifications. This adheres to the Interface Segregation Principle by ensuring that clients only need to depend on interfaces relevant to their functionality.
Benefits of following the ISP:
- Improved readability: Interfaces designed for specific use cases make code clearer and easier to understand.
- Reduced dependencies: Classes only use the methods they require, reducing unnecessary dependencies and making the codebase more modular.
- Ease of maintenance: Changes to a single interface or class are less likely to impact other areas of the code, making maintenance easier.
- Scalability: As your project expands, the ISP allows you to add new features without affecting old code, promoting scalability.
5. Dependency Inversion Principle (DIP)
“High-level modules should not depend on low-level modules.” It means that important parts of a program shouldn’t rely directly on less important parts. Instead, they should both depend on some kind of middleman that helps them communicate. This makes it easier to change or upgrade the less important parts without causing big problems for the important ones.
In this example, the Database
interface and having the DataManager
class depend on the interface rather than a specific database implementation, we adhere to the Dependency Inversion Principle. This allows for flexibility in changing the database type without modifying the DataManager
class.
Benefits of following the DIP:
- Decoupling: High-level modules are separated from low-level modules, making the system more modular and manageable.
- Flexibility: The system becomes more flexible when changing because new export formats can be added or altered without affecting high-level modules.
- Testability: By injecting dependencies, you may quickly swap real implementations with mocks or stubs during testing, allowing you to test high-level modules separately.
The examples above show how each SOLID principle may be utilized in practical circumstances with additional functionality while following the principles of enhanced software design.
For more updates on the latest tools and technologies, follow the Simform Engineering blog.