SOLID Principles

SOLID is an acronym for five design principles introduced by Robert C. Martin (“Uncle Bob”) which aim to make software designs more understandable, flexible, and maintainable.

  1. Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should have only one responsibility or job. This principle encourages high cohesion and reduces coupling, making classes easier to understand, test, and maintain.
  2. Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This principle encourages the use of abstraction and polymorphism to allow for changes in behavior without modifying existing code.
  3. Liskov Substitution Principle (LSP): Subtypes should be substitutable for their base types without altering the correctness of the program. In simpler terms, objects of a superclass should be replaceable with objects of its subclasses without affecting the functionality of the program.
  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. This principle promotes the creation of smaller, specific interfaces tailored to the needs of clients, rather than large, general-purpose interfaces.
  5. Dependency Inversion Principle (DIP): 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. This principle encourages decoupling between modules and promotes flexibility and maintainability.

Understanding and applying these principles can lead to cleaner, more modular, and easier-to-maintain codebases.

Single Responsibility Principle (SRP)

Let’s consider an example of a class that violates SRP and then refactor it to adhere to the principle.

Example violating SRP:

<?php

class User {
    private $username;
    private $email;
    private $password;

    // Constructor
    public function __construct($username, $email, $password) {
        $this->username = $username;
        $this->email = $email;
        $this->password = $password;
    }

    // Method to register a user
    public function register() {
        // Code to insert user data into the database
        echo "User registered successfully.";
    }

    // Method to send a welcome email to the user
    public function sendWelcomeEmail() {
        // Code to send a welcome email to the user
        echo "Welcome email sent.";
    }
}

// Usage
$user = new User("john_doe", "john@example.com", "password123");
$user->register();
$user->sendWelcomeEmail();

?>

In the above example, the User class has two responsibilities:

  1. Registering a user by inserting user data into the database.
  2. Sending a welcome email to the user.

This violates the SRP because if either the registration process or the email sending process needs to be changed, the User class would have to be modified, potentially breaking the principle.

Refactored example adhering to SRP:

<?php

class User {
    private $username;
    private $email;
    private $password;

    // Constructor
    public function __construct($username, $email, $password) {
        $this->username = $username;
        $this->email = $email;
        $this->password = $password;
    }

    // Method to register a user
    public function register() {
        // Code to insert user data into the database
        echo "User registered successfully.";
    }
}

class EmailSender {
    // Method to send a welcome email to the user
    public function sendWelcomeEmail($email) {
        // Code to send a welcome email to the user
        echo "Welcome email sent to $email.";
    }
}

// Usage
$user = new User("john_doe", "john@example.com", "password123");
$user->register();

$emailSender = new EmailSender();
$emailSender->sendWelcomeEmail("john@example.com");

?>

In the refactored example:

  • The User class is responsible only for registering a user.
  • A new class EmailSender is created to handle the responsibility of sending emails.
  • Each class now has a single responsibility, adhering to the Single Responsibility Principle. If either the registration process or the email sending process needs to be changed, only the corresponding class would need to be modified.

I hope it helps to understand SRP, let’s look at Open/Closed Principle (OCP) next.

Open/Closed Principle (OCP)

Suppose we have a Shape class hierarchy, and we want to calculate the total area of various shapes. We can design our code adhering to the OCP as follows:

<?php

// Interface for shapes
interface Shape {
    public function calculateArea();
}

// Circle class implementing Shape interface
class Circle implements Shape {
    private $radius;

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

    public function calculateArea() {
        return pi() * pow($this->radius, 2);
    }
}

// Rectangle class implementing Shape interface
class Rectangle implements Shape {
    private $length;
    private $width;

    public function __construct($length, $width) {
        $this->length = $length;
        $this->width = $width;
    }

    public function calculateArea() {
        return $this->length * $this->width;
    }
}

// AreaCalculator class to calculate total area of shapes
class AreaCalculator {
    public function calculateTotalArea(array $shapes) {
        $totalArea = 0;
        foreach ($shapes as $shape) {
            $totalArea += $shape->calculateArea();
        }
        return $totalArea;
    }
}

// Usage
$shapes = [
    new Circle(5),
    new Rectangle(4, 6)
];

$calculator = new AreaCalculator();
$totalArea = $calculator->calculateTotalArea($shapes);
echo "Total area of shapes: " . $totalArea;
?>

In this example:

  • The Shape interface defines a common interface for all shapes, requiring them to implement the calculateArea() method.
  • The Circle and Rectangle classes implement the Shape interface and provide their own implementations of the calculateArea() method.
  • The AreaCalculator class calculates the total area of shapes by iterating over an array of Shape objects and summing up their individual areas.
  • This design adheres to the Open/Closed Principle because you can easily extend the behavior by creating new shapes (e.g., Triangle, Square) without modifying the existing code. You only need to create a new class implementing the Shape interface and provide the calculateArea() method implementation for the new shape. The AreaCalculator class does not need to be modified to accommodate new shapes, making it closed for modification but open for extension.

I hope it helps to understand SRP, let’s look at Liskov Substitution Principle (LSP): next.

Liskov Substitution Principle (LSP)

Suppose we have a Vehicle class and a Car subclass. According to LSP, we should be able to use a Car object wherever a Vehicle object is expected without causing any unexpected behavior. Here’s an example demonstrating LSP in PHP

<?php

// Parent class
class Vehicle {
    protected $speed;

    // Constructor
    public function __construct($speed) {
        $this->speed = $speed;
    }

    // Method to get speed
    public function getSpeed() {
        return $this->speed;
    }
}

// Subclass Car inheriting from Vehicle
class Car extends Vehicle {
    private $model;

    // Constructor
    public function __construct($speed, $model) {
        parent::__construct($speed);
        $this->model = $model;
    }

    // Method to get car model
    public function getModel() {
        return $this->model;
    }
}

// Function to display speed of a vehicle
function displaySpeed(Vehicle $vehicle) {
    echo "Speed of the vehicle: " . $vehicle->getSpeed() . " mph<br>";
}

// Usage
$vehicle = new Vehicle(60);
$car = new Car(70, "Toyota");

displaySpeed($vehicle); // Output: Speed of the vehicle: 60 mph
displaySpeed($car);     // Output: Speed of the vehicle: 70 mph

?>

In this example:

  • The Vehicle class is a parent class that represents a generic vehicle with a speed property and a getSpeed() method.
  • The Car class is a subclass of Vehicle that represents a car. It adds a model property and a getModel() method.
  • The displaySpeed() function accepts a Vehicle object as a parameter and displays its speed.
  • We can pass a Car object to the displaySpeed() function without any issue. This demonstrates LSP because the Car class is substituting the Vehicle class without affecting the correctness of the program. The displaySpeed() function doesn’t need to know the specific type of vehicle it’s dealing with, just that it’s a vehicle.

Interface Segregation Principle (ISP)

Suppose we have a Worker interface representing workers in a company. However, not all workers have the same responsibilities. Some workers only need to work, while others may also have the ability to take breaks. Instead of having a single large interface, we’ll split it into smaller interfaces based on the specific responsibilities.

<?php

// Interface for workers who can work
interface Workable {
    public function work();
}

// Interface for workers who can take breaks
interface Breakable {
    public function takeBreak();
}

// Worker class implementing Workable interface
class Worker implements Workable {
    public function work() {
        echo "Working...";
    }
}

// Manager class implementing both Workable and Breakable interfaces
class Manager implements Workable, Breakable {
    public function work() {
        echo "Managing...";
    }

    public function takeBreak() {
        echo "Taking a break...";
    }
}

// Function to assign work to workers
function assignWork(Workable $worker) {
    $worker->work();
}

// Function to assign breaks to workers
function assignBreak(Breakable $worker) {
    $worker->takeBreak();
}

// Usage
$worker = new Worker();
$manager = new Manager();

assignWork($worker); // Worker works
echo "<br>";
assignWork($manager); // Manager works
echo "<br>";
assignBreak($manager); // Manager takes a break

?>

In this example:

  • We have two interfaces: Workable for workers who can work, and Breakable for workers who can take breaks.
  • The Worker class implements the Workable interface, indicating that it can work.
  • The Manager class implements both the Workable and Breakable interfaces, indicating that it can both work and take breaks.
  • The assignWork() function accepts objects that implement the Workable interface, allowing workers to be assigned work without needing to know about their ability to take breaks.
  • Similarly, the assignBreak() function accepts objects that implement the Breakable interface, allowing workers to be assigned breaks without needing to know about their ability to work.

This adheres to the Interface Segregation Principle by ensuring that clients (functions assignWork() and assignBreak()) depend only on the interfaces they use, without being forced to depend on unnecessary methods.

Dependency Inversion Principle (DIP)

Let’s illustrate the DIP with an example in PHP:

Suppose we have a Notification class that sends notifications to users via different channels such as email and SMS. We’ll start with a simple implementation that violates the DIP, then refactor it to adhere to the principle.

Example violating DIP:

<?php

// High-level module (Notification) depends on low-level modules (EmailSender and SMSSender)
class Notification {
    public function sendEmailNotification($to, $message) {
        $emailSender = new EmailSender();
        $emailSender->send($to, $message);
    }

    public function sendSMSNotification($to, $message) {
        $smsSender = new SMSSender();
        $smsSender->send($to, $message);
    }
}

// Low-level module for sending emails
class EmailSender {
    public function send($to, $message) {
        echo "Sending email to $to: $message<br>";
    }
}

// Low-level module for sending SMS
class SMSSender {
    public function send($to, $message) {
        echo "Sending SMS to $to: $message<br>";
    }
}

// Usage
$notification = new Notification();
$notification->sendEmailNotification("john@example.com", "Hello, John!");
$notification->sendSMSNotification("+123456789", "Hello, there!");

?>

Refactored example adhering to DIP

<?php

// Abstraction for notification senders
interface NotificationSender {
    public function send($to, $message);
}

// Email sender implementing NotificationSender interface
class EmailSender implements NotificationSender {
    public function send($to, $message) {
        echo "Sending email to $to: $message<br>";
    }
}

// SMS sender implementing NotificationSender interface
class SMSSender implements NotificationSender {
    public function send($to, $message) {
        echo "Sending SMS to $to: $message<br>";
    }
}

// High-level module (Notification) depends on abstraction (NotificationSender)
class Notification {
    private $sender;

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

    public function sendNotification($to, $message) {
        $this->sender->send($to, $message);
    }
}

// Usage
$emailSender = new EmailSender();
$smsSender = new SMSSender();

$emailNotification = new Notification($emailSender);
$smsNotification = new Notification($smsSender);

$emailNotification->sendNotification("john@example.com", "Hello, John!");
$smsNotification->sendNotification("+123456789", "Hello, there!");

?>

In the refactored example:

  • We introduce an NotificationSender interface representing the abstraction for notification senders.
  • Both EmailSender and SMSSender classes implement the NotificationSender interface, ensuring that they adhere to the contract defined by the interface.
  • The Notification class depends on the NotificationSender interface rather than concrete implementations of email and SMS senders. This allows the Notification class to be decoupled from specific sender implementations, adhering to the Dependency Inversion Principle.
  • Now, we can easily extend our system by adding new notification sender classes without modifying existing code. This promotes flexibility and maintainability in our codebase.

We do follow different ways of writing code, but there are some rules as you read in this post, can for sure make a difference!

Author: Danyal
I'm skilled programmer with expertise in Vue.js/Nux.js for front-end development and PHP Laravel for back-end development. I excel in building APIs and services, and also have experience in web server setup & maintenance. My versatile skill set allows you to develop and maintain web applications effectively, from the user interface to the server-side functionality. I love coding with never ending learning attitude, thanks for visiting danya.dk