In this post, we will learn SOLID principles in more detail with java example code

SOLID Principles are set of guidelines to help developers to follow and create applications that are module, maintainable,re-usable and easy to modify.

Single Responsibility Principle

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

public class UserService {
    private UserRepository userRepository;
    
    public void createUser(String name, String email) {
        // Validate input data
        
        User user = new User(name, email);
        userRepository.save(user);
    }
}

public class UserRepository {
    public void save(User user) {
        // Save the user to the database
    }
}
  • UserService is responsible for creating a user
  • UserRepository is responsible for saving a user to the database.
  • Each class has only one responsibility.

Open/Closed Principle

A class should be open for extension but closed for modification, which means we should be able to add new functionality to a class without changing its existing code.

Shape class is open for extension and new shapes can be added by creating a new subclass of Shape. However, the Shape class is closed for modification, as the getArea method is implemented in each subclass.

public abstract class Shape {
    public abstract double getArea();
}

public class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    public double getArea() {
        return width * height;
    }
}

public class Circle extends Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

Liskov Substitution Principle

  • The LSP states that a subclass should be able to be substituted for its superclass without affecting the correctness of the program
  • Subtypes must be substitutable for their base types, which means any instance of a parent class should be able to be replaced by an instance of one of its subclasses without causing errors or unexpected behavior
  • Ostrich can be replaced with Bird and we can add more subclasses like Duck (can have swim method) which can replaced.
// Bad example
class Bird {
    void fly() {
        // code to fly
    }
}

class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException();
    }
}

// Good example
interface Bird {
    void fly();
}

class Ostrich implements Bird {
    void fly() {
        // Ostrich cannot fly
    }
}

Interface Segregation Principle

  • A client should not be forced to depend on methods it does not use. This means that interfaces should be tailored to specific use cases, and clients should only implement the methods they need
  • Check below example code for both good and bad.
// Bad example
interface Payment {
    void processPayment();
    void refundPayment();
    void issueRefund();
}

class CreditCardPayment implements Payment {
    void processPayment() {
        // code to process credit card payment
    }
    void refundPayment() {
        // code to refund credit card payment
    }
    void issueRefund() {
        // code to issue a refund
    }
}

// Good example
interface Payment {
    void processPayment();
}

interface Refundable {
    void refundPayment();
}

interface IssuableRefund {
    void issueRefund();
}

class CreditCardPayment implements Payment, Refundable {
    void processPayment() {
        // code to process credit card payment
    }
    void refundPayment() {
        // code to refund credit card payment
    }
}

class Refund implements Refundable, IssuableRefund {
    void refundPayment() {
        // code to refund payment
    }
    void issueRefund() {
        // code to issue a refund
    }
}

Dependency Inversion Principle

  • High-level modules should not depend on low-level modules. Both should depend on abstractions. This means that classes should depend on abstractions (interfaces or abstract classes) rather than concrete implementations.
  • Example code:
// Bad example
class PaymentProcessor {
    void processPayment(CreditCardPayment payment) {
        // code to process credit card payment
    }
}

class CreditCardPayment {
    void processPayment() {
        // code to process credit card payment
    }
}

// Good example
interface Payment {
    void processPayment();
}

class PaymentProcessor {
    void processPayment(Payment payment) {
        payment.processPayment();
    }
}

class CreditCardPayment implements Payment {
    void processPayment() {
        // code to process credit card payment

Thanks for Reading