Understanding SOLID Principles in Programming

As someone who is learning about these concepts myself, I hope this guide will be helpful for both you and me in understanding these fundamental principles.

What Are SOLID Principles?

SOLID is an acronym that represents five principles of object-oriented programming and design. These principles are intended to make software designs more understandable, flexible, and maintainable. Here’s what SOLID stands for:

  • S - Single Responsibility Principle (SRP)
  • O - Open/Closed Principle (OCP)
  • L - Liskov Substitution Principle (LSP)
  • I - Interface Segregation Principle (ISP)
  • D - Dependency Inversion Principle (DIP)

By adhering to these principles, developers can create software that is easier to manage and extend over time. Below, I’ll explain each principle and provide examples in PHP.

1. Single Responsibility Principle (SRP)

Definition

A class should have only one reason to change. This means that a class should have only one job or responsibility. By ensuring that a class is focused on a single task, you can make your code easier to understand and maintain.

Example in PHP

Consider a simple User class:

<?php

class User {
    private $email;

    public function __construct($email) {
        $this->email = $email;
    }

    public function getEmail() {
        return $this->email;
    }

    public function sendEmail($message) {
        // Logic to send an email
    }
}

?>

In this example, the User class has two responsibilities: storing user information and sending emails. To adhere to SRP, we should separate these responsibilities:

<?php

class User {
    private $email;

    public function __construct($email) {
        $this->email = $email;
    }

    public function getEmail() {
        return $this->email;
    }
}

class EmailService {
    public function sendEmail(User $user, $message) {
        // Logic to send an email
    }
}

?>

Now, the User class is only responsible for user data, while the EmailService handles email sending.

2. Open/Closed Principle (OCP)

Definition

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means you should be able to add new functionality to a class without changing its existing code.

Example in PHP

Imagine you have a PaymentProcessor class that processes payments for different methods:

<?php

class PaymentProcessor {
    public function processPaypalPayment($amount) {
        // Process PayPal payment
    }

    public function processEurobankPayment($amount) {
        // Process Eurobank payment
    }
}

?>

To add more payment methods, you’d have to modify the PaymentProcessor class, violating the OCP. Instead, you can use interfaces to make it open for extension:

<?php

interface PaymentMethod {
    public function processPayment($amount);
}

class PaypalPayment implements PaymentMethod {
    public function processPayment($amount) {
        // Process PayPal payment
    }
}

class EurobankPayment implements PaymentMethod {
    public function processPayment($amount) {
        // Process Eurobank payment
    }
}

class PaymentProcessor {
    public function process(PaymentMethod $paymentMethod, $amount) {
        $paymentMethod->processPayment($amount);
    }
}

?>

Now, to add new payment methods, you simply create new classes that implement the PaymentMethod interface without modifying existing code.

3. Liskov Substitution Principle (LSP)

Definition

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. This means that subclasses should be substitutable for their base classes.

Example in PHP

Consider a base class Bird and a subclass Penguin:

<?php

class Bird {
    public function fly() {
        echo "Flying";
    }
}

class Penguin extends Bird {
    public function fly() {
        throw new Exception("Penguins can't fly");
    }
}

?>

In this example, substituting a Penguin for a Bird breaks the functionality because penguins can’t fly. To adhere to LSP, we can refactor the design:

<?php

abstract class Bird {
    abstract public function move();
}

class FlyingBird extends Bird {
    public function move() {
        echo "Flying";
    }
}

class Penguin extends Bird {
    public function move() {
        echo "Swimming";
    }
}

?>

Now, both FlyingBird and Penguin conform to the Bird interface, and the program will behave correctly when substituting one for the other.

4. Interface Segregation Principle (ISP)

Definition

Clients should not be forced to depend on interfaces they do not use. This means that you should create specific interfaces rather than a large, general-purpose interface.

Example in PHP

Consider a Worker interface with multiple responsibilities:

<?php

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

class HumanWorker implements Worker {
    public function work() {
        // Working
    }

    public function eat() {
        // Eating
    }
}

class RobotWorker implements Worker {
    public function work() {
        // Working
    }

    public function eat() {
        // Robots don't eat
        throw new Exception("Robots don't eat");
    }
}

?>

The RobotWorker class doesn’t need the eat method, violating the ISP. We can fix this by creating more specific interfaces:

<?php

interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

class HumanWorker implements Workable, Eatable {
    public function work() {
        // Working
    }

    public function eat() {
        // Eating
    }
}

class RobotWorker implements Workable {
    public function work() {
        // Working
    }
}

?>

Now, HumanWorker implements both interfaces, while RobotWorker only implements Workable.

5. Dependency Inversion Principle (DIP)

Definition

High-level modules should not depend on low-level modules. Both should depend on abstractions. This principle aims to reduce the dependency between high-level and low-level layers of a system.

Example in PHP

Consider a Keyboard and a Computer class:

<?php

class Keyboard {
    // Keyboard implementation
}

class Computer {
    private $keyboard;

    public function __construct() {
        $this->keyboard = new Keyboard();
    }
}

?>

Here, the Computer class is tightly coupled with the Keyboard class. To follow DIP, we should depend on abstractions:

<?php

interface KeyboardInterface {
    public function type();
}

class Keyboard implements KeyboardInterface {
    public function type() {
        // Typing
    }
}

class Computer {
    private $keyboard;

    public function __construct(KeyboardInterface $keyboard) {
        $this->keyboard = $keyboard;
    }
}

?>

Now, the Computer class depends on the KeyboardInterface, not the concrete Keyboard class, allowing us to inject any implementation of the KeyboardInterface.

This post was part of my learning journey about SOLID principles, and I hope it helps you understand them better too.