Published on

SOLID Principles in Object-Oriented Design

Authors
  • avatar
    Name
    Skim
    Twitter

OOP is a useful paradigm for abstraction, but as projects grow in complexity, maintaining and extending the codebase can become challenging due to the abstraction itself. The SOLID principles provide a set of guidelines that help developers create more maintainable, flexible, and scalable software systems. These five design principles were introduced by Robert C. Martin, and they stand for:

  1. Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should have a single responsibility. This principle promotes separation of concerns and helps prevent a class from becoming too complex.
  2. Open-Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This principle encourages adding new functionality through inheritance or interfaces without modifying existing code.
  3. Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, subclasses should be substitutable for their base classes without causing unexpected behavior.
  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use. This principle suggests that smaller, specific interfaces are better than large, monolithic ones, preventing clients from being burdened with unnecessary methods.
  5. Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions. This principle promotes decoupling and flexibility.

Example of ISP

// Violation of ISP
interface Worker {
    void work();
    void eat();
}

class Engineer implements Worker {
    public void work() {
        // engineer-specific work
    }
    public void eat() {
        // engineer-specific eating
    }
}

// Clients forced to implement unnecessary eat() method
class Robot implements Worker {
    public void work() {
        // robot-specific work
    }
    public void eat() {
        // robot doesn't eat, unnecessary implementation
    }

// Adhering to ISP
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Engineer implements Workable, Eatable {
    public void work() {
        // engineer-specific work
    }
    public void eat() {
        // engineer-specific eating
    }
}

class Robot implements Workable {
    public void work() {
        // robot-specific work
    }
}

Example of DIP

# Violation of DIP
class LightBulb:
    def turn_on(self):
        pass

class Switch:
    def __init__(self, bulb):
        self.bulb = bulb

    def operate(self):
        self.bulb.turn_on()

# High-level Switch class depends on low-level LightBulb class
# Changes in LightBulb may affect Switch
bulb = LightBulb()
switch = Switch(bulb)

# Adhering to DIP
class Switchable:
    def turn_on(self):
        pass

class LightBulb(Switchable):
    def turn_on(self):
        # light bulb-specific logic

class Switch:
    def __init__(self, device):
        self.device = device

    def operate(self):
        self.device.turn_on()

# High-level Switch class depends on abstraction (Switchable)
# Changes in LightBulb do not affect Switch
bulb = LightBulb()
switch = Switch(bulb)