TechnologyZer
technologyzer.com

Mastering Software Design: SOLID Principle

SOLID principles are a set of five object-oriented design principles that help developers create more maintainable, scalable, and understandable software. Here’s a brief overview of each principle:

SOLID Principle
SOLID Principle

Single Responsibility Principle:

  • A class should have only one reason to change, meaning it should have only one responsibility. This principle suggests that a class should encapsulate only one functionality or behavior.
Without Following SRP:
// This class violates the Single Responsibility Principle
class Employee {
    private String name;
    private double salary;
    
//It carries out two functions: calculating pay and saving data in the database.
    public void calculatePay() {
        // Calculate employee's pay
    }
    
    public void saveEmployeeData() {
        // Save employee's data to a database
    }
}
Following SRP:
// This class handles employee data
class Employee {
    private String name;
    private double salary;
    
    // Constructor, getters, setters
}

// This class handles calculating employee pay
class PayCalculator {
    public void calculatePay(Employee employee) {
        // Calculate employee's pay
    }
}

// This class handles saving employee data
class EmployeeRepository {
    public void saveEmployeeData(Employee employee) {
        // Save employee's data to a database
    }
}

Open/Closed Principle:

  • Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to extend the behavior of a class without modifying its source code.
Without Following OCP
// This class handles employee data
class Employee {
    private String name;
    private double salary;
    
    // Constructor, getters, setters
}

// This class handles calculating employee pay. 
We've introduced a new method, "calculate Bonus Pay,"
 to an existing class, deviating from the open-closed principle.
class PayCalculator {
    public void calculateSalaryPay(Employee employee) {
        // Calculate Monthly Salary pay
    }
    public double calculateBonusPay(Employee employee) {
        // Calculate Bonus pay based on annual salary
    }
}
Following OCP:
// This class handles employee data
class Employee {
    private String name;
    private double salary;
    
    // Constructor, getters, setters
}

// This interface handles calculating employee pay
interface PayCalculator {
    double calculatePay(Employee employee);
}

// This class handles calculating employee's Salary pay
class SalaryCalculator implements PayCalculator {
    public double calculateSalaryPay(Employee employee) {
        // Calculate Monthly Salary pay
    }
}
// This class handles calculating employee's Bonus pay
class BonusPayCalculator implements PayCalculator {
    public double calculateBonusPay(Employee employee) {
        // Calculate Bonus pay based on annual salary
    }
}

Liskov Substitution Principle:

  • Subtypes must be substitutable for their base types without altering the correctness of the program. In other words, objects of a superclass should be replaceable with objects of its subclasses without affecting the behavior of the program.
WithOut Following LSP

  • Employee class with a method performDuties(), and a Manager subclass that overrides this method to provide its specific behavior. However, this still violates the LSP because a Manager instance may not be substitutable for an Employee instance in all contexts. For instance, if there’s a method that expects an Employee object to perform general duties and we pass a Manager object to it, it might lead to unexpected behavior because the Manager object behaves differently

class Employee {
    private String name;
    private double salary;

    public void performDuties() {
        System.out.println("Employee is performing duties.");
    }
}

class Manager extends Employee {
    @Override
    public void performDuties() {
        System.out.println("Manager is managing.");
    }
}
Following LSP:
  • Both RegularEmployee and Manager implement the Employee interface, which defines a performDuties() method. Each class provides its own implementation of the performDuties() method. This adheres to the LSP because substituting a Manager for an Employee or a RegularEmployee for an Employee preserves the expected behavior of the performDuties() method, ensuring that each type of employee can perform its designated duties without unexpected side effects.
interface Employee {
    void performDuties();
}

class RegularEmployee implements Employee {
    @Override
    public void performDuties() {
        System.out.println("Regular employee is performing duties.");
    }
}

class Manager implements Employee {
    @Override
    public void performDuties() {
        System.out.println("Manager is managing.");
    }
}

Interface Segregation Principle:

  • Clients should not be forced to depend on interfaces they don’t use. This principle suggests that you should design small, focused interfaces rather than large, monolithic ones.
Without Following ISP:

In this example, the Employee interface has two methods: performDuties() and manageTeam(). However, not all types of employees should manage a team. RegularEmployee should only perform duties without managing a team, but it’s forced to implement the manageTeam() method, which violates the ISP.

interface Employee {
    void performDuties();
    void manageTeam();
}

class RegularEmployee implements Employee {
    @Override
    public void performDuties() {
        System.out.println("Regular employee is performing duties.");
    }

    @Override
    public void manageTeam() {
        // Regular employees shouldn't manage a team, but we are forced to implement this method
        throw new UnsupportedOperationException("Regular employees cannot manage a team.");
    }
}

class Manager implements Employee {
    @Override
    public void performDuties() {
        System.out.println("Manager is managing.");
    }

    @Override
    public void manageTeam() {
        System.out.println("Manager is managing the team.");
    }
}
Following ISP:

In this revised implementation, we split the Employee interface into two interfaces: Workable for employees who perform duties and Managerial for employees who manage a team. Each class implements only the interface(s) that are relevant to its role. This adheres to the ISP because each class is not forced to depend on methods it doesn’t need, promoting loose coupling and better code organization.

interface Workable {
    void performDuties();
}

interface Managerial {
    void manageTeam();
}

class RegularEmployee implements Workable {
    @Override
    public void performDuties() {
        System.out.println("Regular employee is performing duties.");
    }
}

class Manager implements Workable, Managerial {
    @Override
    public void performDuties() {
        System.out.println("Manager is managing.");
    }

    @Override
    public void manageTeam() {
        System.out.println("Manager is managing the team.");
    }
}

Dependency Inversion Principle:

  • Dependency Inversion Principle (DIP) advocates that high-level modules should not depend on low-level modules, but both should rely on abstractions. This principle encourages the use of interfaces or abstract classes to define the interactions between modules, rather than concrete implementations.
Without Following DIP:

In this example, Manager class directly creates an instance of Employee within its constructor, tightly coupling Manager with Employee. If the organization later introduces different types of employees or changes the behavior of the Employee class, it would require modifications to the Manager class, violating the DIP.

class Employee {
    public void work() {
        System.out.println("Employee is working.");
    }
}

class Manager {
    private Employee employee;

    public Manager() {
        this.employee = new Employee();
    }

    public void supervise() {
        employee.work();
        System.out.println("Manager is supervising.");
    }
}
Following DIP:

In this example, Manager class depends on the Worker interface instead of a concrete Employee class. This allows for flexibility, as different types of workers (e.g., contractors, interns) can implement the Worker interface, and the Manager class can work with any class that implements this interface. This adheres to the Dependency Inversion Principle, promoting loose coupling and easier maintenance and extension of the codebase.

interface Worker {
    void work();
}

class Employee implements Worker {
    @Override
    public void work() {
        System.out.println("Employee is working.");
    }
}

class Manager {
    private Worker worker;

    public Manager(Worker worker) {
        this.worker = worker;
    }

    public void supervise() {
        worker.work();
        System.out.println("Manager is supervising.");
    }
}

In summary, SOLID principles results in software that is more robust, flexible, maintainable, and easier to understand, leading to higher quality products and more satisfied users.

Leave a Comment