SOLID Principles with Examples
Dr Adarsh Anand
BACKGROUND
The SOLID principles are a set of guidelines for writing maintainable and extensible
object-oriented code. While their specific origins can be traced back to the early days of
object-oriented programming(OOP), they gained widespread recognition in the late 1990s
or early 2000s thanks to the work of Robert C. Martin.
He is credited with introducing and popularizing the SOLID principles because of his
significant contributions to Software Design and Robert C. Martin
(also called ‘Uncle Bob’)
Object-Oriented Design(OOD). He has been a prominent figure in the software
development community for decades, writing extensively about these principles in his
books and articles, making them widely known and understood.
Today, the SOLID principles continue to be applied and discussed, shaping the way
developers approach object-oriented design(OOD)
DESIGN PHASE OF THE SDLC
The design phase is a crucial stage in the software development life cycle (SDLC). This where the blueprint or
architecture for the software is created. This includes:
• Defining the overall structure: How the software will be organized into components, modules, and subsystems.
• Designing the user interface: Creating wireframes, mockups, and prototypes to visualize the user experience.
• Specifying data structures: Determining how data will be stored and managed within the software.
• Identifying dependencies: Understanding the relationships between different components and systems.
This is a critical stage as a well-designed software architecture provides a solid foundation for efficient and
effective development, and can mitigate risks by identifying potential issues early in the process.
Moreover, a well-designed software is easier to maintain, update, and extend, and a good design can contribute
to higher quality software by ensuring that it meets the specified requirements.
OBJECT-ORIENTED DESIGN
Object-Oriented Design (OOD) is a software design approach that models real-world entities as objects,
each with its own properties (data) and behaviors (methods). These objects interact with each other to
perform tasks.
OOD provides a framework for organizing software into a more modular and maintainable structure. The
SOLID principles, in turn, offer guidelines for designing classes and objects in an OOD context.
By following the SOLID principles, developers can create OOD systems that are:
• Modular: Easier to understand, maintain, and test.
• Extensible: Adaptable to changing requirements.
• Reusable: Components can be reused in different projects.
• Maintainable: Changes can be made without introducing significant risks.
In essence, OOD and SOLID principles work together to create robust, flexible,
and maintainable software systems.
SOLID PRINCIPLES
S Single Responsibility Principle (SRP) : A class should have a single,
well-defined responsibility.
O Open-Closed Principle (OCP) : Entities should be open for extension
but closed for modification.
L Liskov Substitution Principle (LSP) : Subclasses should not violate the
contract of their superclass.
I Interface Segregation Principle (ISP) : Large interfaces should be broken
down into smaller & specific ones.
D Dependency Inversion Principle (DIP) : Dependencies should be based on
abstractions, not on implementations.
SINGLE RESPONSIBILITY PRINCIPLE (SRP)
• This principle states that “A class should have only one reason to change” which means every class should have
a single responsibility or single job or single purpose. In other words, a class should have only one job or purpose
within the software system.
• When a class as only one responsibility , it becomes easier to cane and test. If a class as multiple responsibilities,
changing one responsibility may impact others and more testing efforts will be required.
Let’s understand Single Responsibility Principle using a Real World Example:
Imagine a baker who is responsible for baking bread. The baker’s role is to focus on the task of baking
bread, ensuring that the bread is of high quality, properly baked, and meets the bakery’s standards.
However, if the baker is also responsible for managing the inventory, ordering supplies, serving
customers, and cleaning the bakery, this would violate the SRP.
Each of these tasks represents a separate responsibility, and by combining them, the baker’s focus and
effectiveness in baking bread could be compromised.
To adhere to the SRP, the bakery could assign different roles to different individuals or teams. For
example, there could be a separate person or team responsible for managing the inventory, another
for ordering supplies, another for serving customers, and another for cleaning the bakery.
IMPLEMENTATION IN CODING:
Let us consider a class that manages both user information and file
operations. This class violates SRP because it has multiple
responsibilities:
Here, the User class has two responsibilities:
[Link] manages user information (e.g., name, email).
[Link] handles file operations (saving user info to a file).
User
User info File
Operation
Extra Responsibility
REFACTORED CODE USING SRP:
In this refactored code, we have two classes:
[Link] User class, which only manages user-related information.
[Link] FileManager class, which handles file-related operations.
Each class now has a single responsibility, and changes to file
management (e.g., switching to a database) won't affect the User
class. This leads to a more modular, maintainable system.
User info
User File
Operation
IMPORTANCE OF SINGLE RESPONSIBILITY PRINCIPLE
•Code Maintenance:
When classes are small and focused on one responsibility, it becomes easier to maintain them. If a
change is needed, the changes are localized, reducing the risk of unintended side effects.
•Code Reusability:
Classes with a single responsibility are more reusable because they are focused on one thing. They can be
reused in different contexts without requiring modifications.
•Testability:
Classes that follow SRP are easier to test. You can write focused unit tests for each responsibility, which
makes the testing process simpler and more effective.
•Separation of Concerns:
SRP promotes separation of concerns, meaning that different parts of the system manage different
concerns. This reduces the risk of complex interdependencies between different parts of the system
ADVANTAGES OF SINGLE RESPONSIBILITY PRINCIPLE
•Improved Code Readability: Classes that adhere to SRP are easier to read and understand because they have a
well-defined purpose.
•Easier to Debug: When each class handles only one task, it's easier to identify where a bug might be. You can
quickly trace which class is responsible for a particular issue.
•Reduced Code Coupling: By separating responsibilities, you reduce the dependency between different parts of the
code. This makes the system more modular and less prone to ripple effects when changes are made.
•Scalability and Extensibility: It’s easier to extend the system by adding new features or modifying existing ones
because each responsibility is isolated. Changes in one part of the code do not affect other parts unnecessarily.
•Enhanced Collaboration: In larger teams, the SRP encourages better collaboration. Since different team members
can work on different responsibilities without stepping on each other’s toes, it speeds up development.
OPEN/CLOSED PRINCIPLE (OCP)
• The Open/Closed Principle (OCP) is one of the five SOLID principles of object-oriented design,
introduced by Bertrand Meyer and popularized by Robert C. Martin (Uncle Bob). It states that:
“Software entities (such as classes, modules, functions, etc.) should be open for extension, but closed for
modification.”
1. Open for extension: This means you should be able to extend the behaviour of a class or module
without modifying its existing code. In other words, you should be able to add new functionality or
behaviours by adding new code.
2. Closed for modification: Once a class or module is written, tested, and put into production, it
shouldn't be altered or modified. This protects the existing behaviour of the system from unintended
changes and helps ensure stability.
IMPORTANCE OF OCP:
• Maintainability: If your system follows the OCP, adding new features becomes
easier without risking changes to existing, stable code.
• Flexibility: Software designed under this principle is more flexible to changes and
can adapt to future requirements without much refactoring.
• Reusability: By separating the parts of the system that are subject to frequent
changes, developers can reuse the existing code without worrying about side
effects.
HOW TO ACHIEVE OCP:
1. Abstraction and Polymorphism:
• OCP leverages abstraction (e.g., interfaces or abstract classes) to define common behaviour for a set
of classes. By creating new implementations (i.e., extending the abstraction), you can add new
features without modifying the original code.
• Polymorphism allows different classes to be treated through a common interface or base class,
enabling extensibility. For example, a system that processes different kinds of Payments (credit card,
cash, etc.) can be extended by adding new payment methods without changing the
payment-processing code.
2. Inheritance:
Using inheritance, developers can create a base class (or abstract class) that provides general behaviour,
while subclasses extend or override this behaviour without touching the base class. This achieves
"open for extension" because new behaviour is added by creating new subclasses.
3. Interfaces or Contracts:
Interfaces or abstract classes define contracts that new classes can implement. This ensures that
future extensions follow a predefined structure, making the system predictable and scalable.
4. Dependency Injection:
OCP often works in combination with Dependency Injection (DI) to decouple the system’s
components, allowing new behaviours to be introduced without altering the core logic. DI lets you
inject new functionality into a system at runtime, making it highly extensible.
5. Encapsulation:
OCP encourages encapsulation by restricting direct modifications to existing classes. By
encapsulating behaviour inside classes and exposing only the necessary interface, you can
extend the system by adding new classes or modules without altering the internals of the existing
ones.
REAL-WORLD EXAMPLE – RESTAURANT
Imagine you are managing a restaurant, and you have a software system that
calculates the price of various dishes. Initially, the restaurant offers only two
types of dishes: pizzas and burgers
However, if the restaurant wants to add a new item, say pasta, you would need
to modify the calculate_price function by adding another condition.
This violates the OCP because everytime a new dish is introduced, the existing
calculate_price function must be modified. This makes the
system fragile and difficult to maintain as the menu grows.
To follow the Open/Closed Principle, we need to design the system in a way that allows new types of dishes
to be added without modifying the existing code. We can achieve this by using polymorphism and
inheritance.
Step 1: Create a base class Dish that
defines an abstract calculate_price method
Step 2: Create separate classes for each dish, inheriting from Dish and
implementing their own calculate_price method
In this design, if the restaurant decides to add more dishes (like salads, desserts, or drinks), the system allows for easy
extension without modifying the existing structure(CLOSED FOR MODIFICATION). The original classes and
methods remain unchanged while still allowing new functionality to be added(OPEN FOR EXTENSION). . This makes
the system more scalable, maintainable, and less error-prone.
BENEFITS OF THE OPEN/CLOSED PRINCIPLE
The Open/Closed Principle encourages you to design software that is robust and adaptable by
allowing new features to be added without altering existing code, thus reducing bugs and
improving maintainability over time. It helps in writing code that evolves easily with new
requirements while preserving the integrity of existing functionality.
Extensibility without Minimal risk of Maintainability
modification bugs and flexibility
OCP allows systems to grow by Existing, tested, and stable Systems built using OCP are
adding new features or code is not altered when new easier to maintain and
behaviors through extension. features are added. adapt to new requirements,
making them more
future-proof.
LISKOV SUBSTITUTION PRINCIPLE
Barbara Liskov introduced the Liskov Substitution Principle in 1987, Car:
Engine ✔
marking a pivotal moment in programming. Move ✔
LSP states that subclasses must enhance, not disrupt, the behavior of Fly ❌
their superclasses for effective software design.
In the reaIm of software development, the Liskov Substitution Vehicle:
Principle serves as a cornerstone for robust design and maintainable Engine()
Cycle: Move()
code. Airplane:
Engine Fly()
Engine
❌ ✔
This principle emphasizes behavior compatibility; subclasses must Move ✔ Move ✔
Fly ❌ Fly ✔
honor the expectations set by their parent classes. Furthermore,
clear definitions of covariant return types and invariants ensure that
derived classes enhance rather than hinder functionality.
IMPORTANCE IN OBIECT-ORIENTED
PROGRAMMING
Object -oriented programming heavily relies on the Liskov Substitution Principle to
ensure that objects of a superclass can be replaced with objects of its subclasses
without affecting the program's functionality. Adhering to this principle enhances code
reliability, reduces bugs, and promotes code reusability. Understanding its importance
is crucial for developing systems that are flexible and easy to maintain, which is
essential in professional software engineering.
In simpler terms, if a program is designed to work with a superclass object, it should
also work with any object of a subclass without any modifications.
We can understand this with the help of the adjacent example.
PROPER IMPLEMENTATION OF LSP:
• To effectively implement the Liskov Substitution Principle, clear contracts for classes should be prioritized,
while ensuring that subclasses adhere to these agreements. Code reusability should be promoted while
maintaining behavioral consistency.
Rigorous testing and validation of subclasses are essential to confirm their proper functionality in place of
the superclass.
• Thorough testing and validation are critical components in applying the Liskov Substitution Principle.
Employ unit tests to evaluate subclass behavior, ensuring they meet superclass expectations. Incorporate
integration tests to observe interactions between subclasses and other system parts. Utilizing automated
testing frameworks can streamline this process, reducing human error and
fostering consistent verification.
This proactive approach reinforces the integrity and reliability of your codebase. By following these best
practices, professionals can enhance system reliability and minimize unexpected behaviors in software
applications.
CONCLUSION AND KEY TAKEAWAYS
In summary, mastering the Liskov Substitution Principle is crucial for developing scalable and resilient
software. By embracing modular designs, leveraging Al, and utilizing contemporary architectural
practices, developers can ensure compliance with LSP. This not only enhances code quality but also
promotes a sustainable development environment. Let us commit to these foundational principles as
we shape the future of software engineering.
INTERFACE SEGREGATION PRINCIPLE
•The Interface Segregation Principle is part of the SOLID
design principles. It states that “Clients should not be forced
to depend on interfaces they do not use.”
•Here our main goal is to focus on avoiding fat interface and
give preference to many small client-specific interfaces. We
should prefer many client interfaces rather than one general
interface and each interface should have a specific
responsibility.
•A large interface might force implementing classes to include
unused methods, leading to unnecessary code or a violation
of Liskov Substitution Principle.
THE FAT INTERFACE PROBLEM
• Tight Coupling: Large interfaces lead to tight coupling between
classes, making it difficult to change one class without affecting
others.
• Rigidity: Bloated interfaces make it hard to add new functionality or
modify existing code without breaking other parts of the system.
• Fragility: Changes to a large interface can have unexpected effects
on other parts of the system, leading to fragility.
• Limited Scalability: Fat interfaces hinder scalability, making it
challenging to add new features or components.
• Maintenance headache: Large interfaces become a maintenance
nightmare, as small changes can have far-reaching and
unpredictable.
UNDERSTANDING ISP WITH AN EXAMPLE
A REAL LIFE SITUATION
• Imagine a restaurant that offers both vegetarian and non-vegetarian dishes. A
vegetarian customer must scroll through a long menu with both veg and non-veg
items. The customer has to go through irrelevant information (non-veg items), Non-Ve
which can be overwhelming and inefficient. g
Desserts Veg
APPLYING ISP IN THIS SITUATION
• In this case, a customer should have a menu card that only includes vegetarian Menu
items, and not the other items that he doesn’t consume. Here the menu should be
different for different types of customers.
• By applying ISP, we segregate the menu into smaller, more specific categories,
ensuring that vegetarian customers only see what’s relevant to them. This
improves user experience and reduces unnecessary complexity.
USES OF ISP:
•Prevents Unnecessary Code: Clients (classes) don’t need to implement methods they don’t use, reducing code bloat.
•Enhances Flexibility: Smaller, specific interfaces allow for more flexible designs that can be extended or modified without
affecting unrelated parts of the code.
•Improves Maintainability: Changes to one interface don’t impact clients that don’t use that interface, making the system
easier to maintain.
•Encourages Modularity: Breaking down large interfaces into smaller, focused ones helps design modular systems, which
are easier to test and refactor.
•Increases Code Reusability: Well-segregated interfaces can be reused across different parts of the system without forcing
unnecessary dependencies.
•Simplifies Client Implementation: Clients only need to know about the methods they require, making their
implementation simpler and more intuitive.
•Supports the Single Responsibility Principle: Since each interface focuses on a specific set of related actions, the system
naturally adheres to the single responsibility principle.
CONCLUSION
• We've learned that:
• ISP helps us avoid fat interfaces and their associated problems
• Segregated interfaces lead to looser coupling, increased flexibility, and improved maintainability
• ISP enables scalability and makes software design more efficient
• We will:
• Apply ISP in our software design to improve maintainability and scalability
• Refactor existing fat interfaces to segregated ones
• Embrace the power of ISP for better software architecture
In conclusion: Segregated interfaces are like separated roads - they
may seem longer but they lead to a smoother journey.
DEPENDENCY INVERSION PRINCIPLE (DIP)
High-Level Module High-Level Module
Abstraction Layer
Low-Level Module Low-Level Module
INTRODUCTION
The Dependency Inversion Principle (DIP) states : “How two modules should depend on each other”.
Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts:
High-level modules should not Abstractions should not depend on
depend on low-level modules. Both details. Details should depend on
should depend on abstractions. abstractions.
In simpler terms, this means that your code should be designed in a way that high-level
components are not directly coupled to low-level components. Instead, they should both
depend on abstract interfaces.
EXPLANATION OF THE DEPENDENCY INVERSION
DIP promotes loose coupling, flexibility and maintainability. It also promotes the use of interfaces and abstract
classes.
Abstractions High-Level Modules Low-Level Modules
Abstractions are interfaces or High-level modules are Low-level modules are
abstract classes that define a responsible for business logic responsible for details like
contract for how objects and complex operations. data access or specific
interacts implementations
Loose Coupling Flexibility Maintainability
Code is loosely coupled when Flexibility means the system is easy Maintainability refers to how
modules are independent of each to adapt new requirements or easy it is to fix bugs, add new
other and can be changed changes in the environment without features, or make changes to
without affecting other parts of extensive refactoring. the codebase
the system.
IMPLEMENTING THE DEPENDENCY INVERSION
Implementing DIP requires careful consideration of how dependencies are managed and how abstractions
are used.
1 Define Interfaces
Create interfaces that define the contracts
between modules.
2 Implement Concrete Classes
Implement concrete classes that fulfill the
interface contracts
3 Inject Dependencies
Inject the concrete classes into the
high-level modules through constructor
injection or dependency injection
There are many ways to implement the Dependency Inversion Principle, and these examples illustrate some
common approaches.
Example 1: Using a Database Interface
A high-level module interacts with a database through an interface, allowing for easy
switching of database implementations.
Example 2: Using a Logging Interface
Different logging libraries can be used without changing the core logic of the application by
using a logging interface.
Example 3: Using a Dependency Injection Framework
Dependency injection frameworks simplify the process of injecting dependencies, promoting
loose coupling and testability.
REAL-WORLD EXAMPLE
• A real-world analogy for the Dependency Inversion Principle can be
drawn from a smartphone. The phone itself acts as a high-level
module, while apps are low-level modules.
High-Level Module Smartphone
Apps (e.g., Camera, Messaging,
Low-Level Modules
Navigation)
The phone's operating system
provides an abstract interface
Abstraction
for apps to interact with
hardware resources.
BENEFITS OF THE DEPENDENCY INVERSION
The Dependency Inversion Principle offers several benefits, such as improved code maintainability,
reduced complexity, and increased testability.
Improved New implementations can be added without modifying the high-level module. High-level
Maintainability modules remain unchanged when low-level modules are updated or swapped.
Reduced Simpler codebase, making it easier to understand and manage. Encourages the creation of
Complexity reusable components that can be utilized across different projects.
Increased
Testability Easier to write unit tests for individual components without complex dependencies.
CONCLUSION
Why should we go for Dependency Inversion?
• Embracing Dependency Inversion enables the construction of loosely coupled components leading to
simpler testing and replacement of modules without causing disruptions to the entire system.
• The Dependency Inversion Principle (DIP) is a powerful tool for building maintainable, extensible, and
reusable software systems.
Its promotes loose Use interfaces and Inject dependencies to Use dependency injection
coupling, flexibility & abstract classes to decouple modules and frameworks to simplify the
testability. define contracts. promote reusability. process.
Thank
You