My photo

Nariman Mani, P.Eng., PhD Computer and Software Engineering
Home

    SOLID - 5 Principles of Object Oriented Design

    April 19, 2024

    The SOLID principles are a set of guidelines designed to improve software maintainability and extendability, making it easier for developers to manage and evolve their software systems. Each of the five principles helps manage dependencies in software design, making systems easier to understand, scale, and modify. Here’s a brief introduction to each, along with visual representations to help clarify the concepts."

    Each letter in "SOLID" encapsulates a specific principle designed to encourage best practices in object-oriented programming. These principles help developers avoid common pitfalls such as tightly-coupled code and over-dependency, leading to software that is easier to manage and extend over time. Here’s what each principle stands for:

    1. Single Responsibility Principle (SRP)

    A class should have one, and only one, reason to change. This means that a class should only have one job or responsibility.

    In this diagram, UserService handles user operations, and UserRepository handles database access, demonstrating SRP by separating concerns.

    Here's how you might implement the SRP in Java:

    public class UserService {
        public void createUser(String username) {
            // Logic to create a user
        }
    
        public void updateUser(String username) {
            // Logic to update user information
        }
    }
    
    public class UserRepository {
        public void saveUser(User user) {
            // Save user to database
        }
    
        public void deleteUser(User user) {
            // Delete user from database
        }
    }
    

    2. Open/Closed Principle (OCP)

    Software entities should be open for extension, but closed for modification. This means you should be able to change a class's behavior without modifying its source code, typically using interfaces or abstract classes.

    Shape is an abstract base class with different shapes like Rectangle and Circle extending it. Each can implement calculateArea differently, showing that the system is extensible without modifying existing code.

    abstract class Shape {
        abstract double calculateArea();
    }
    
    class Rectangle extends Shape {
        private double length;
        private double width;
    
        public Rectangle(double length, double width) {
            this.length = length;
            this.width = width;
        }
    
        @Override
        double calculateArea() {
            return length * width;
        }
    }
    
    class Circle extends Shape {
        private double radius;
    
        public Circle(double radius) {
            this.radius = radius;
        }
    
        @Override
        double calculateArea() {
            return Math.PI * radius * radius;
        }
    }
    

    3. Liskov Substitution Principle (LSP)

    Subtypes must be substitutable for their base types. This principle is fundamental for achieving reusable and maintainable object-oriented systems.

    Here, using Bird as a base class for Duck and Ostrich may violate LSP since not all birds can fly (like the ostrich). This indicates a potential redesign might be necessary. To resolve the Liskov Substitution Principle (LSP) violation in the given scenario where Bird is a superclass of both Duck (which can fly) and Ostrich (which cannot fly), we need to redesign the hierarchy to better reflect the abilities of different types of birds. A better design would avoid assuming that all birds can fly, segregating the flying behavior into a separate interface or class structure. Here’s how you might redesign this:

    Proposed Redesign using Interfaces

    We can introduce interfaces to differentiate between birds that can fly and those that cannot. This approach adheres to LSP, as the interfaces will only be implemented by classes that can perform the actions required by the interface.

    In this design:

    • Bird: Remains the base class for all birds, containing behaviors that are common across all birds, such as eating.
    • Flying: An interface that includes flying behavior. Only birds that can fly, like Duck, implement this interface.
    • Duck: Inherits from Bird and implements the Flying interface, indicating it can fly.
    • Ostrich: Inherits from Bird but does not implement Flying, accurately reflecting that ostriches cannot fly.

    This structure ensures that the system remains flexible and scalable, allowing for the easy introduction of other bird types, with or without the ability to fly, without violating LSP. It clearly differentiates the capabilities of different entities based on their real-world characteristics, maintaining logical consistency within the application's design.

    Here's an adjusted Java example for LSP, avoiding the violation:

    class Bird {
        // General bird-related methods
    }
    
    class FlyingBird extends Bird {
        public void fly() {
            // Flying functionality
        }
    }
    
    class Duck extends FlyingBird {
        // Duck-specific functionality
    }
    
    class Ostrich extends Bird {
        // Ostrich-specific functionality
    }
    

    4. Interface Segregation Principle (ISP)

    No client should be forced to depend on methods it does not use. This principle advocates for designing finer and more specific interfaces that are client-specific rather than one general-purpose interface.

    Here, segregating the Worker interface into Workable and Eatable helps ensure that Robot, which does not need to eat, does not have to implement eat().

    5. Dependency Inversion Principle (DIP)

    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.

    DataProcessor uses DataRepository, an abstraction, rather than a specific data source implementation. This decouples the high-level data processing logic from the low-level data access logic.

    These visuals and explanations should help clarify the SOLID principles, making them more accessible and understandable.

    A Java example demonstrating DIP:

    interface DataRepository {
        String getData();
    }
    
    class DataProcessor {
        private DataRepository repository;
    
        public DataProcessor(DataRepository repository) {
            this.repository = repository;
        }
    
        public void processData() {
            String data = repository.getData();
            // Process data
        }
    }
    
    class SQLRepository implements DataRepository {
        public String getData() {
            return "Data from SQL database";
        }
    }
    
    class NoSQLRepository implements DataRepository {
        public String getData() {
            return "Data from NoSQL database";
        }
    }
    

    Benefits of SOLID Principles

    • Enhanced Maintainability: By adhering to these principles, software is generally easier to maintain because changes in one part of the system are less likely to require changes in other parts.
    • Increased Scalability: The principles promote designs that are more modular and decoupled, allowing systems to be scaled up more easily by adding new functionalities without needing to refactor much existing code.
    • Improved Code Quality: SOLID principles lead to cleaner code that is easier to read, understand, and debug. This is particularly beneficial in large projects with multiple developers.
    • Reduced Risk of Bugs: By promoting the use of interfaces and separation of concerns, the principles help prevent bugs that arise from tightly coupled components.
    • Better Testability: Smaller, well-defined classes with limited responsibilities are easier to test in isolation.

    Feasibility of Implementing SOLID Principles

    While the SOLID principles offer numerous benefits, fully implementing them in real-world projects can be challenging:

    • Complexity: Adhering to these principles can sometimes introduce additional complexity in the design, especially in cases where simple tasks become fragmented across multiple classes or modules.
    • Over-engineering: There's a risk of over-engineering solutions by trying too hard to follow these principles, especially for small or less complex projects where simpler designs could suffice.
    • Learning Curve: Developers new to these concepts might find them difficult to understand and apply correctly, which could lead to misapplications that do not yield the intended benefits.
    • Practical Limitations: Sometimes project deadlines, legacy systems, or other practical constraints make it difficult to adhere strictly to these principles.

    In practice, the key is to apply SOLID principles judiciously, understanding when they add value to a project and when they might lead to unnecessary complexity. It’s often about finding the right balance based on the specific requirements and constraints of each project.

2024 All rights reserved.