SOLID

img

SOLID Principles

SRP (single-responsibility-principle)

img

Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) is one of the five SOLID principles of object-oriented design. It states that a class should have only one reason to change, meaning it should have only one job or responsibility. This principle helps in creating more maintainable and understandable code by ensuring that each class or module focuses on a single functionality.

Key Benefits:

  1. Maintainability: With SRP, classes are smaller and focused on a single task, making them easier to understand, modify, and extend.
  2. Reusability: Well-defined responsibilities promote code reuse, as each class encapsulates a distinct piece of functionality.
  3. Testability: Testing is simplified because classes have a single focus, reducing the complexity of unit tests.

Example:

Consider a class Invoice that handles both invoice calculations and printing.

Violation of SRP:

public class Invoice {
    public void calculateTotal() {
        // Code to calculate total
    }

    public void printInvoice() {
        // Code to print invoice
    }
}

Adhering to SRP:

public class Invoice {
    public void calculateTotal() {
        // Code to calculate total
    }
}

public class InvoicePrinter {
    public void printInvoice(Invoice invoice) {
        // Code to print invoice
    }
}

In the second example, the Invoice class is only responsible for calculations, while the InvoicePrinter class handles printing. This separation of concerns makes each class more focused and adheres to the SRP.

img


SRP in Product

SRP Product Wise: Focus on core Feature

Keep product focus on the core feature.

img

-- image from whats app scaling article

DIP (dependency inversion principle)

img

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) is one of the five SOLID principles of object-oriented design. It states that high-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces). Additionally, abstractions should not depend on details. Details (concrete implementations) should depend on abstractions (eg. interfaces).

Key Benefits:

  1. Decoupling: Reduces tight coupling between high-level and low-level modules, making the system more modular.
  2. Flexibility: Easier to swap out implementations without affecting high-level modules.
  3. Maintainability: Simplifies changes and enhancements, as changes in low-level modules do not ripple through the entire system.

Example:

Consider a scenario where a CustomerService class depends on a MySQLDatabase class for data storage.

Violation of DIP:

public class MySQLDatabase {
    public void saveData(String data) {
        // Code to save data to MySQL database
    }
}

public class CustomerService {
    private MySQLDatabase database = new MySQLDatabase();

    public void addCustomer(String customerData) {
        database.saveData(customerData);
    }
}

In this case, CustomerService is tightly coupled to MySQLDatabase. If you want to switch to a different database, you must modify CustomerService.

Adhering to DIP:

  1. Define an Abstraction:

    public interface Database {
        void saveData(String data);
    }
    
  2. Implement Concrete Classes:

    public class MySQLDatabase implements Database {
        @Override
        public void saveData(String data) {
            // Code to save data to MySQL database
        }
    }
    
    public class MongoDBDatabase implements Database {
        @Override
        public void saveData(String data) {
            // Code to save data to MongoDB
        }
    }
    
  3. Refactor High-Level Module:

    public class CustomerService {
        private Database database;
    
        public CustomerService(Database database) {
            this.database = database;
        }
    
        public void addCustomer(String customerData) {
            database.saveData(customerData);
        }
    }
    
  4. Usage:

    public class Main {
        public static void main(String[] args) {
            Database mySQLDatabase = new MySQLDatabase();
            CustomerService customerService = new CustomerService(mySQLDatabase);
            customerService.addCustomer("John Doe");
    
            // Switch to MongoDB without changing CustomerService
            Database mongoDBDatabase = new MongoDBDatabase();
            customerService = new CustomerService(mongoDBDatabase);
            customerService.addCustomer("Jane Doe");
        }
    }
    

Summary:

The Dependency Inversion Principle promotes the use of abstractions to decouple high-level modules from low-level implementations. By relying on interfaces or abstract classes, you can swap out concrete implementations with minimal impact on the system, enhancing flexibility, maintainability, and modularity.

To follow this well you should be using interfaces.

You should start by creating an interface and writing out how you want the interface to look like without being bogged down by implementation details yet.

Even when it seems like the interface is not needed yet and there will be just one implementation, creating an interface switches our mind into a different mindset.

Creating an interface forces us to scope down what the object is allowed to do.

Creating an interfaces makes it easy to see what is the object allowed to do.

Creating an interface makes it more likely that some javadoc documentation is written.

Further notes

Also note: Sometimes POJO is all you need.

There is some cost to creation of the interface. Not just in a manner of creating initially (hint: lower this cost by using intellij extract interface functionality to speed up creation of interfaces from classes that already exist). But more so later to hop from interface down to implementation.

Hence, at times it makes sense just to create a data object holding data without making an interface to it for the sake of Keep it super Simple (KISS).

Relationships

LSP-liskov-substitution-principle

img

What is LSP?

The Liskov Substitution Principle is one of the five object-oriented design principles collectively known as SOLID. It was introduced by Barbara Liskov in 1987 during her conference keynote. Essentially, the LSP states:

"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."

In simpler terms, if class B is a subtype of class A, then we should be able to replace A with B without disrupting the behavior of our program.

Why is LSP Important?

  • Highlights challenges with inheritance: Inheritance is taught as a core and relatively simple concept. While in reality inheritance is much harder to get right. LSP highlights this fact and points us towards: Favor Composition over Inheritance.
  • Promotes robustness: Adhering to the LSP increases the robustness of the system by ensuring that extending the system via inheritance does not introduce new bugs.
  • Enhances maintainability: Systems that follow LSP are more modular and easier to understand, making maintenance simpler.
  • Improves reusability: By ensuring that subclasses remain substitutable for their base classes, LSP aids in reusing components, which is a hallmark of effective object-oriented design.

Implementing LSP

To comply with the Liskov Substitution Principle, ensure that:

  1. Subtypes enhance, not alter functionalities: Subclasses should implement their superclass methods in a way that does not weaken superclass behavior.
  2. Do not strengthen preconditions: Subclasses should not impose stricter validation or input requirements than their base classes.
  3. Do not weaken postconditions: The outcome and effects of overridden methods in subclasses should meet or exceed what is specified in the base class.
  4. Exceptions are consistent: Subclasses should not introduce new exceptions that can be thrown by methods of the base class, unless those exceptions are equivalents of the base class exceptions.

Example Violation

Consider a class Bird with a method fly(). A subclass Penguin inherits from Bird but cannot fly. If the program attempts to make a Penguin fly, this would violate LSP because Penguin cannot be used as a substitute for Bird in this context.

Remember...

Following the Liskov Substitution Principle helps in creating truly modular systems, where individual components can be replaced and updated without affecting the integrity of the overall system. It's all about keeping your inheritance tree clean and logical.

OCP-open-closed-principle

img

Open/Closed Principle (OCP)

The Open/Closed Principle (OCP) is one of the five SOLID principles of object-oriented design. It states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that the behavior of a module can be extended without modifying its source code, enhancing flexibility and reducing the risk of introducing bugs.

Key Benefits:

  1. Maintainability: By avoiding modifications to existing code, the risk of introducing new bugs is minimized.
  2. Scalability: New functionality can be added with minimal impact on the existing system.
  3. Flexibility: Encourages the use of abstractions, making the codebase more adaptable to changing requirements.

Example:

Consider a class DiscountCalculator that calculates discounts for different customer types.

Violation of OCP:

public class DiscountCalculator {
    public double calculateDiscount(String customerType, double amount) {
        switch (customerType) {
            case "Regular":
                return amount * 0.05;
            case "Premium":
                return amount * 0.10;
            default:
                return 0;
        }
    }
}

To add a new discount type, you must modify the calculateDiscount method, violating OCP.

Adhering to OCP (Using Strategy (Design Pattern) design pattern):

// Step 1: Define an abstract base class
public abstract class DiscountStrategy {
    public abstract double calculateDiscount(double amount);
}

// Step 2: Implement specific discount strategies
public class RegularDiscount extends DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.05;
    }
}

public class PremiumDiscount extends DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.10;
    }
}

// Step 3: Use the strategy in DiscountCalculator
public class DiscountCalculator {
    public double calculateDiscount(DiscountStrategy strategy, double amount) {
        return strategy.calculateDiscount(amount);
    }
}

In the second example, the DiscountCalculator class does not need to be modified to add a new discount type. Instead, new discount strategies can be created by extending the DiscountStrategy class. This adheres to the OCP, making the system more maintainable and scalable.

ISP-interface-segregation-principle

img

Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented design. It states that no client should be forced to depend on methods it does not use. Instead of having a single large interface, ISP recommends breaking down the interface into smaller, more specific ones, so that clients only need to know about the methods that are of interest to them.

Key Benefits:

  1. Decoupling: Reduces the dependencies between classes, making the system more modular and easier to maintain.
  2. Cohesion: Ensures that interfaces are more focused and relevant to the clients that use them.
  3. Flexibility: Simplifies the implementation of classes by avoiding unnecessary methods.

Example:

Consider an interface Worker that combines multiple responsibilities.

Violation of ISP:

public interface Worker {
    void work();
    void eat();
}

A Robot class that implements Worker would have to implement eat method, which is not relevant.

public class Robot implements Worker {
    @Override
    public void work() {
        // Robot working code
    }

    @Override
    public void eat() {
        // Irrelevant for Robot
    }
}

Adhering to ISP: Separate the interface into more specific ones.

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

Now, Robot implements only the relevant interface.

public class Robot implements Workable {
    @Override
    public void work() {
        // Robot working code
    }
}

And a HumanWorker can implement both.

public class HumanWorker implements Workable, Eatable {
    @Override
    public void work() {
        // Human working code
    }

    @Override
    public void eat() {
        // Human eating code
    }
}

Summary:

The Interface Segregation Principle promotes the creation of fine-grained, specific interfaces that align with the actual needs of clients. By following ISP, developers can create more modular, maintainable, and understandable code, avoiding the pitfalls of large, monolithic interfaces that try to do too much.

Note

While SOLID is very important principle it should also be balanced out with YAGNI and at times having a simpler implementation (Keep it super Simple (KISS)) that does not fully adhere to SOLID is the right balance.


Children
  1. DIP (dependency inversion principle)
  2. ISP Interface-Segregation-Principle
  3. Liskov Substitution Principle (LSP)
  4. OCP-open-closed-principle
  5. SRP (single-responsibility-principle)

Backlinks