Reading.CleanArchitecture.SOLID

1. Single Responsibility Principle (SRP)

  • Definition: A class should have only one reason to change.
  • Meaning: Each class should only have one responsibility or functionality. This makes the system easier to maintain and understand.
  • Example:
    • Bad: A Report class that both generates reports and sends them via email.
    • Good: A Report class generates reports, and a EmailSender class handles email sending.

// Bad example

public
c class Invoice {
    public void calculateTotal() {
        // Logic for calculating total
    }

    public void printInvoice() {
        // Logic for printing the invoice
    }

    public void saveToDatabase() {
        // Logic for saving the invoice to the database
    }
}

// Good Example

public class Invoice {
    public void calculateTotal() {
        // Logic for calculating total
    }
}

public class InvoicePrinter {
    public void print(Invoice invoice) {
        // Logic for printing the invoice
    }
}

public class InvoiceRepository {
    public void save(Invoice invoice) {
        // Logic for saving the invoice to the database
    }
}

2. Open/Closed Principle (OCP)

The problem here, that when we change old classes it can bring new bugs, so better option is to inherit and make changes in some new classes.

  • Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.
  • Meaning: You should be able to add new functionality to a class without changing its existing code. This prevents introducing bugs into existing functionality.
  • Example:
    • Use interfaces or abstract classes that can be implemented or extended without modifying the original code.

// Bad example ( if i need to add another shape i will need to change this method )

public class AreaCalculator {
    public double calculateShapeArea(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.radius * circle.radius;
        } else if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.width * rectangle.height;
        }
        return 0;
    }
}

// Good example

public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double calculateArea() {
        return width * height;
    }
}

public class AreaCalculator {
    public double calculateShapeArea(Shape shape) {
        return shape.calculateArea();
    }
}

New shapes can be added without modifying the AreaCalculator.


3. Liskov Substitution Principle (LSP)

  • Definition: Objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.
  • Meaning: Subclasses should behave in a way that does not break the functionality expected of the parent class.
  • Example:
    • Bad: A Bird superclass with a fly() method, but a Penguin subclass overrides it to throw an exception.
    • Good: Design the hierarchy to reflect actual behavior or use composition over inheritance.
public class Bird {
    public void fly() {
        System.out.println("Flying");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}

// Good example

public interface Bird {
    void eat();
}

public interface FlyingBird extends Bird {
    void fly();
}

public class Sparrow implements FlyingBird {
    public void eat() {
        System.out.println("Eating");
    }

    public void fly() {
        System.out.println("Flying");
    }
}

public class Penguin implements Bird {
    public void eat() {
        System.out.println("Eating");
    }
}

4. Interface Segregation Principle (ISP)

  • Definition: A class should not be forced to implement interfaces it does not use.
  • Meaning: Break down large, complex interfaces into smaller, more specific ones that are relevant to particular clients.
  • Example:
    • Bad: A Printer interface with methods print(), scan(), and fax(). A simple printer shouldn’t implement scan() or fax().
    • Good: Separate interfaces like Printable, Scannable, and Faxable.

// Bad example

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

public class Robot implements Worker {
    public void work() {
        System.out.println("Working");
    }

    public void eat() {
        throw new UnsupportedOperationException("Robots don't eat!");
    }
}

// Good example

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class HumanWorker implements Workable, Eatable {
    public void work() {
        System.out.println("Working");
    }

    public void eat() {
        System.out.println("Eating");
    }
}

public class RobotWorker implements Workable {
    public void work() {
        System.out.println("Working");
    }
}

Each class only implements the interfaces it needs.


5. Dependency Inversion Principle (DIP)

  • Definition: 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.
  • Meaning: Decouple higher-level code from lower-level code using interfaces or abstractions.
  • Example:
    • Bad: A Database class tightly coupled to a UserService class.
    • Good: UserService depends on an interface DataStorage, which can have implementations like Database or InMemoryStorage.

// Bad example

public class Database {
    public void save(String data) {
        System.out.println("Saving to database: " + data);
    }
}

public class UserService {
    private Database database;

    public UserService() {
        this.database = new Database();
    }

    public void saveUser(String user) {
        database.save(user);
    }
}

// Good example

public interface DataStorage {
    void save(String data);
}

public class Database implements DataStorage {
    public void save(String data) {
        System.out.println("Saving to database: " + data);
    }
}

public class InMemoryStorage implements DataStorage {
    public void save(String data) {
        System.out.println("Saving to in-memory storage: " + data);
    }
}

public class UserService {
    private DataStorage storage;

    public UserService(DataStorage storage) {
        this.storage = storage;
    }

    public void saveUser(String user) {
        storage.save(user);
    }
}
This entry was posted in Без рубрики. Bookmark the permalink.