SOLID Principles of Classes SOLID



1. S: Single Responsibility Principle (SRP)

2. O: Open/Closed Principle (OCP)

3. L: Liskov Substitution Principle (LSP)

4. I: Interface Segregation Principle (ISP)

##5. D: Dependency Inversion Principle (DIP)

SOLID

SOLID is the easy-to-remember acronym recommended by Michael Feathers, which represents the five most important object-oriented coding design principles named by Robert Martin

  • S: Single Responsibility Principle (SRP)

  • O: Opening and Closing Principle (OCP)

  • L: Liskov Substitution Principle (LSP)

  • I: Interface Isolation Principle (ISP)

  • D: Dependency Inversion Principle (DIP)

1. Single Responsibility Principle

Single Responsibility Principle (SRP)

As stated in Clean Code, "Modifying a class should only for a reason". It's always easy to stuff a class with a bunch of methods, just like we can only carry one suitcase on the plane (stuff everything into the suitcase). The problem with this is that conceptually such a class is not highly cohesive, and leaves a lot of reasons to modify it. It's important to minimize the number of times you need to modify a class. This is because when there are many methods in a class, if you modify one of them, it is difficult to know which dependent modules in the code base will be affected.

Bad:

class UserSettings
{
    private $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
 
    public function changeSettings(array $settings): void
    {
        if ($this->verifyCredentials()) {
            // ...
        }
    }
 
    private function verifyCredentials(): bool
    {
        // ...
    }
}

Good:

class UserAuth
{
    private $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
    
    public function verifyCredentials(): bool
    {
        // ...
    }
}
 
class UserSettings
{
    private $user;
    private $auth;
 
    public function __construct(User $user)
    {
        $this->user = $user;
        $this->auth = new UserAuth($user);
    }
 
    public function changeSettings(array $settings): void
    {
        if ($this->auth->verifyCredentials()) {
            // ...
        }
    }
}

2. Opening and closing principle

Open/Closed Principle (OCP)

As stated by Bertrand Meyer, "Artifacts of software (

classes, modules, functions, etc.) should Open for extension, closed for modification." But what does this mean? This principle generally means that you should allow new functionality to be added without changing existing code

Bad:

abstract class Adapter
{
    protected $name;
 
    public function getName(): string
    {
        return $this->name;
    }
}
 
class AjaxAdapter extends Adapter
{
    public function __construct()
    {
        parent::__construct();
 
        $this->name = 'ajaxAdapter';
    }
}
 
class NodeAdapter extends Adapter
{
    public function __construct()
    {
        parent::__construct();
 
        $this->name = 'nodeAdapter';
    }
}
 
class HttpRequester
{
    private $adapter;
 
    public function __construct(Adapter $adapter)
    {
        $this->adapter = $adapter;
    }
 
    public function fetch(string $url): Promise
    {
        $adapterName = $this->adapter->getName();
 
        if ($adapterName === 'ajaxAdapter') {
            return $this->makeAjaxCall($url);
        } elseif ($adapterName === 'httpNodeAdapter') {
            return $this->makeHttpCall($url);
        }
    }
 
    private function makeAjaxCall(string $url): Promise
    {
        // request and return promise
    }
 
    private function makeHttpCall(string $url): Promise
    {
        // request and return promise
    }
}

Good:

interface Adapter
{
    public function request(string $url): Promise;
}
 
class AjaxAdapter implements Adapter
{
    public function request(string $url): Promise
    {
        // request and return promise
    }
}
 
class NodeAdapter implements Adapter
{
    public function request(string $url): Promise
    {
        // request and return promise
    }
}
 
class HttpRequester
{
    private $adapter;
 
    public function __construct(Adapter $adapter)
    {
        $this->adapter = $adapter;
    }
 
    public function fetch(string $url): Promise
    {
        return $this->adapter->request($url);
    }
}

##3. Liskov Substitution Principle

Liskov Substitution Principle (LSP)

This It's a simple principle, but it uses a difficult-to-understand term. Its formal definition is "If S is a subtype of T, then any object of type T can be replaced by an object of type S without changing the original established properties of the program (checking, executing tasks, etc.) (for example, Using an object of S can replace an object of T)" This definition is more difficult to understand :-).

The best explanation of this concept is: if you have a parent class and a child class, the parent class and the child class can be interchanged without changing the correctness of the original results. This still sounds a little confusing, so let's look at a classic square-rectangle example. Mathematically, a square is a rectangle, but that's not true when your model uses an "is-a" relationship through inheritance.

Bad:

class Rectangle
{
    protected $width = 0;
    protected $height = 0;
 
    public function setWidth(int $width): void
    {
        $this->width = $width;
    }
 
    public function setHeight(int $height): void
    {
        $this->height = $height;
    }
 
    public function getArea(): int
    {
        return $this->width * $this->height;
    }
}
 
class Square extends Rectangle
{
    public function setWidth(int $width): void
    {
        $this->width = $this->height = $width;
    }
 
    public function setHeight(int $height): void
    {
        $this->width = $this->height = $height;
    }
}
 
function printArea(Rectangle $rectangle): void
{
    $rectangle->setWidth(4);
    $rectangle->setHeight(5);
 
    // BAD: Will return 25 for Square. Should be 20.
    echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()).PHP_EOL;
}
 
$rectangles = [new Rectangle(), new Square()];
 
foreach ($rectangles as $rectangle) {
    printArea($rectangle);
}

Good:

It is best to treat these two kinds of quadrilaterals separately and use one suitable for both. A more general subtype of the type instead.

Although squares and rectangles look similar, they are different. A square is closer to a rhombus, while a rectangle is closer to a parallelogram. But they are not subtypes. Although similar, squares, rectangles, rhombuses, and parallelograms are all different shapes with their own properties.

interface Shape
{
    public function getArea(): int;
}
 
class Rectangle implements Shape
{
    private $width = 0;
    private $height = 0;
 
    public function __construct(int $width, int $height)
    {
        $this->width = $width;
        $this->height = $height;
    }
 
    public function getArea(): int
    {
        return $this->width * $this->height;
    }
}
 
class Square implements Shape
{
    private $length = 0;
 
    public function __construct(int $length)
    {
        $this->length = $length;
    }
 
    public function getArea(): int
    {
        return $this->length ** 2;
    }
}
 
function printArea(Shape $shape): void
{
    echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL;
}
 
$shapes = [new Rectangle(4, 5), new Square(5)];
 
foreach ($shapes as $shape) {
    printArea($shape);
}

4. Interface Segregation Principle

Interface Segregation Principle (ISP)

Interface Segregation Principle means: "The caller Should not be forced to rely on interfaces that it does not need."

There is a clear example to illustrate this principle. When a class requires a large number of settings, it is convenient not to require the caller to set a large number of options, because usually they do not need all settings. Making settings optional helps us avoid creating "fat interfaces"

Bad:

interface Employee
{
    public function work(): void;
 
    public function eat(): void;
}
 
class HumanEmployee implements Employee
{
    public function work(): void
    {
        // ....working
    }
 
    public function eat(): void
    {
        // ...... eating in lunch break
    }
}
 
class RobotEmployee implements Employee
{
    public function work(): void
    {
        //.... working much more
    }
 
    public function eat(): void
    {
        //.... robot can't eat, but it must implement this method
    }
}

Good:

Not every worker is an employee, but every employee is a worker

interface Workable
{
    public function work(): void;
}
 
interface Feedable
{
    public function eat(): void;
}
 
interface Employee extends Feedable, Workable
{
}
 
class HumanEmployee implements Employee
{
    public function work(): void
    {
        // ....working
    }
 
    public function eat(): void
    {
        //.... eating in lunch break
    }
}
 
// robot can only work
class RobotEmployee implements Workable
{
    public function work(): void
    {
        // ....working
    }
}

5. Dependency Inversion Principle

Dependency Inversion Principle (DIP)

This principle illustrates two basic points:

High-level modules should not depend on low-level modules, they should all depend on abstraction

Abstraction does not Should depend on implementation, implementation should depend on abstraction

This may seem a bit obscure at first, but if you have used PHP frameworks (such as Symfony), you should have seen dependency injection (DI), which is implementation of this concept. Although they are not exactly equivalent concepts, the dependency inversion principle separates the implementation details and creation of higher-order modules from lower-order modules. This can be achieved using dependency injection (DI). The biggest benefit is that it decouples modules from each other. Coupling makes it difficult to refactor and is a very bad development model.

Bad:

class Employee
{
    public function work(): void
    {
        // ....working
    }
}
 
class Robot extends Employee
{
    public function work(): void
    {
        //.... working much more
    }
}
 
class Manager
{
    private $employee;
 
    public function __construct(Employee $employee)
    {
        $this->employee = $employee;
    }
 
    public function manage(): void
    {
        $this->employee->work();
    }
}

Good:

interface Employee
{
    public function work(): void;
}
 
class Human implements Employee
{
    public function work(): void
    {
        // ....working
    }
}
 
class Robot implements Employee
{
    public function work(): void
    {
        //.... working much more
    }
}
 
class Manager
{
    private $employee;
 
    public function __construct(Employee $employee)
    {
        $this->employee = $employee;
    }
 
    public function manage(): void
    {
        $this->employee->work();
    }
}