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.
- 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.
- 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.
- 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.
- 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.
- 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:
- Registering a user by inserting user data into the database.
- 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 thecalculateArea()
method. - The
Circle
andRectangle
classes implement theShape
interface and provide their own implementations of thecalculateArea()
method. - The
AreaCalculator
class calculates the total area of shapes by iterating over an array ofShape
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 theShape
interface and provide thecalculateArea()
method implementation for the new shape. TheAreaCalculator
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 aspeed
property and agetSpeed()
method. - The
Car
class is a subclass ofVehicle
that represents a car. It adds amodel
property and agetModel()
method. - The
displaySpeed()
function accepts aVehicle
object as a parameter and displays its speed. - We can pass a
Car
object to thedisplaySpeed()
function without any issue. This demonstrates LSP because theCar
class is substituting theVehicle
class without affecting the correctness of the program. ThedisplaySpeed()
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, andBreakable
for workers who can take breaks. - The
Worker
class implements theWorkable
interface, indicating that it can work. - The
Manager
class implements both theWorkable
andBreakable
interfaces, indicating that it can both work and take breaks. - The
assignWork()
function accepts objects that implement theWorkable
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 theBreakable
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
andSMSSender
classes implement theNotificationSender
interface, ensuring that they adhere to the contract defined by the interface. - The
Notification
class depends on theNotificationSender
interface rather than concrete implementations of email and SMS senders. This allows theNotification
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!