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

  • S: Single Responsibility Principle (SRP)
  • O: Open close Principle (OCP)
  • L: Richter’s Substitution Principle (LSP)
  • I: Interface Isolation Principle (ISP)
  • D: Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP)

“A class should be modified for only one reason.” It’s easy to cram a class into a bunch of methods, just as we can only carry one suitcase on an airplane. The problem with this is that such a class is conceptually not highly cohesive, and that leaves plenty of reason to modify it. It’s important to minimize the number of times you need to modify classes. This is because, when there are many methods in a class, modifying one of them makes it difficult to know which dependent modules in the code base will be affected.

Bad:

class UserSettings { private $user; public function __construct($user) { $this->user = $user; } public function changeSettings($settings) { if ($this->verifyCredentials()) { // ... } } private function verifyCredentials() { // ... }}Copy the code

Good:

class UserAuth { private $user; public function __construct($user) { $this->user = $user; } public function verifyCredentials() { // ... } } class UserSettings { private $user; private $auth; public function __construct($user) { $this->user = $user; $this->auth = new UserAuth($user); } public function changeSettings($settings) { if ($this->auth->verifyCredentials()) { // ... }}}Copy the code

Open/Closed Principle (OCP)

As Bertrand Meyer put it, “Software entities (classes, modules, functions, etc.) should be open for extension and closed for modification.” This principle states that users should be allowed to add new functionality without changing existing code.

Bad:

abstract class Adapter
{
    protected $name;

    public function getName()
    {
        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)
    {
        $this->adapter = $adapter;
    }

    public function fetch($url)
    {
        $adapterName = $this->adapter->getName();

        if ($adapterName === 'ajaxAdapter') {
            return $this->makeAjaxCall($url);
        } elseif ($adapterName === 'httpNodeAdapter') {
            return $this->makeHttpCall($url);
        }
    }

    private function makeAjaxCall($url)
    {
        // request and return promise
    }

    private function makeHttpCall($url)
    {
        // request and return promise
    }
}
Copy the code

In the above code, if I add a new xxxAdapter class to the fetch method, I need to modify the class (such as adding an elseif judgment) in the HttpRequester class. And through the following code, can be a good solution to this problem. The following code is a good example of adding new functionality without changing the original code.

Good:

interface Adapter
{
    public function request($url);
}

class AjaxAdapter implements Adapter
{
    public function request($url)
    {
        // request and return promise
    }
}

class NodeAdapter implements Adapter
{
    public function request($url)
    {
        // request and return promise
    }
}

class HttpRequester
{
    private $adapter;

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

    public function fetch($url)
    {
        return $this->adapter->request($url);
    }
}
Copy the code

Liskov Substitution Principle (LSP)

The best way to explain this concept is that if you have a parent and a subclass, the parent and subclass can be interchanged without changing the validity of the original result. This may sound confusing, so let’s look at a classic square-rectangle example. Mathematically, a square is a rectangle, but when your model uses the “IS-A” relationship through inheritance, it’s not right.

Bad:

class Rectangle
{
    protected $width = 0;
    protected $height = 0;

    public function render($area)
    {
        // ...
    }

    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }

    public function getArea()
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function setWidth($width)
    {
        $this->width = $this->height = $width;
    }

    public function setHeight(height)
    {
        $this->width = $this->height = $height;
    }
}

function renderLargeRectangles($rectangles)
{
    foreach ($rectangles as $rectangle) {
        $rectangle->setWidth(4);
        $rectangle->setHeight(5);
        $area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20.
        $rectangle->render($area);
    }
}

$rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($rectangles);
Copy the code

Good:

abstract class Shape
{
    protected $width = 0;
    protected $height = 0;

    abstract public function getArea();

    public function render($area)
    {
        // ...
    }
}

class Rectangle extends Shape
{
    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }

    public function getArea()
    {
        return $this->width * $this->height;
    }
}

class Square extends Shape
{
    private $length = 0;

    public function setLength($length)
    {
        $this->length = $length;
    }

    public function getArea()
    {
        return pow($this->length, 2);
    }
}

function renderLargeRectangles($rectangles)
{
    foreach ($rectangles as $rectangle) {
        if ($rectangle instanceof Square) {
            $rectangle->setLength(5);
        } elseif ($rectangle instanceof Rectangle) {
            $rectangle->setWidth(4);
            $rectangle->setHeight(5);
        }

        $area = $rectangle->getArea(); 
        $rectangle->render($area);
    }
}

$shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($shapes);
Copy the code

Interface Isolation Principle

Interface isolation principle: “Clients should not be forced to implement interfaces they do not need.”

There is a clear example to illustrate this principle. When a class requires a large number of Settings, clients are not required to set a large number of options for convenience, as they usually do not need all Settings. Making Settings optional helps us avoid creating “fat interfaces”

Bad:

interface Employee { public function work(); public function eat(); } class Human implements Employee { public function work() { // .... working } public function eat() { // ...... eating in lunch break } } class Robot implements Employee { public function work() { //.... working much more } public function eat() { //.... robot can't eat, but it must implement this method } }Copy the code

In the code above, the Robot class does not need the eat() method, but implements the Emplyee interface, so it can only implement all methods, which allows Robot to implement methods it does not need. So the Emplyee interface should be split here. The correct code is as follows:

Good:

interface Workable { public function work(); } interface Feedable { public function eat(); } interface Employee extends Feedable, Workable { } class Human implements Employee { public function work() { // .... working } public function eat() { //.... eating in lunch break } } // robot can only work class Robot implements Workable { public function work() { // .... working } }Copy the code

Dependency Inversion Principle (DIP)

This principle makes two basic points:

  • Higher-order modules should not depend on lower-order modules, they should all depend on abstractions
  • Abstraction should not depend on implementation; implementation should depend on abstraction

This one may seem a bit arcane at first, but if you’ve ever used a PHP framework such as Symfony, you’ve probably seen dependency injection (DI) implemented this concept. Although they are not completely interlinked concepts, the dependency inversion principle separates implementation details and creation of higher-order modules from lower-order modules. This can be done using dependency injection (DI). The further benefit is that it decouples modules from each other. Coupling makes it hard to refactor, and it’s a very bad development pattern.

Bad:

class Employee
{
    public function work()
    {
        // ....working
    }
}

class Robot extends Employee
{
    public function work()
    {
        //.... working much more
    }
}

class Manager
{
    private $employee;

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

    public function manage()
    {
        $this->employee->work();
    }
}
Copy the code

Good:

interface Employee
{
    public function work();
}

class Human implements Employee
{
    public function work()
    {
        // ....working
    }
}

class Robot implements Employee
{
    public function work()
    {
        //.... working much more
    }
}

class Manager
{
    private $employee;

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

    public function manage()
    {
        $this->employee->work();
    }
}
Copy the code

Don’t write duplicate code (DRY)

This principle should be familiar to you.

Do your best to avoid copying code, it is a very bad practice, copying code usually means that when you need to change some logic, you need to change more than one.

Bad:

function showDeveloperList($developers) { foreach ($developers as $developer) { $expectedSalary = $developer->calculateExpectedSalary(); $experience = $developer->getExperience(); $githubLink = $developer->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } } function showManagerList($managers) { foreach ($managers as $manager) { $expectedSalary = $manager->calculateExpectedSalary(); $experience = $manager->getExperience(); $githubLink = $manager->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); }}Copy the code

Good:

function showList($employees) { foreach ($employees as $employee) { $expectedSalary = $employee->calculateExpectedSalary(); $experience = $employee->getExperience(); $githubLink = $employee->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); }}Copy the code

Very good:

function showList($employees) { foreach ($employees as $employee) { render([ $employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink() ]); }}Copy the code

Postscript:

While OOP design should follow these principles, actual code design should be simple, simple, simple. Make trade-offs according to the situation in the actual code. Sticking to principles instead of paying attention to the actual situation can make your code difficult to understand!