[Перевод] Простое объяснение принципов SOLID

z5b2h8gaf28pjknnrjz3tpjlfui.jpeg

Принципы SOLID — это стандарт программирования, который все разработчики должны хорошо понимать, чтобы избегать создания плохой архитектуры. Этот стандарт широко используется в ООП. Если применять его правильно, он делает код более расширяемым, логичным и читабельным. Когда разработчик создаёт приложение, руководствуясь плохой архитектурой, код получается негибким, даже небольшие изменения в нём могут привести к багам. Поэтому нужно следовать принципам SOLID.

На их освоение потребуется какое-то время, но если вы будете писать код в соответствии с этими принципами, то его качество повысится, а вы освоите создание хорошей архитектуры ПО.

Чтобы понять принципы SOLID, нужно чётко понимать, как использовать интерфейсы. Если у вас такого понимания нет, то сначала почитайте документацию.

Я буду объяснять SOLID самым простым способом, так что новичкам легче будет разобраться. Будем рассматривать принципы один за другим.

Принцип единой ответственности (Single Responsibility Principle)

Существует лишь одна причина, приводящая к изменению класса.

Один класс должен решать только какую-то одну задачу. Он может иметь несколько методов, но они должны использоваться лишь для решения общей задачи. Все методы и свойства должны служить единой цели. Если класс имеет несколько назначений, его нужно разделить на отдельные классы.

Рассмотрим пример:

queryDBForOrders($startDate, $endDate);
        
        return $this->format($orders);
    }

    protected function queryDBForOrders($startDate, $endDate)
    {   // If we would update our persistence layer in the future,
        // we would have to do changes here too. <=> reason to change!
        return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get();
    }

    protected function format($orders)
    {   // If we changed the way we want to format the output,
        // we would have to make changes here. <=> reason to change!
        return '

Orders: ' . $orders . '

'; } }


Приведённый здесь класс нарушает принцип единой ответственности. Почему он должен извлекать данные из базы? Это задача для уровня хранения данных, на котором данные сохраняются и извлекаются из хранилища (например, базы данных). Это ответственность не этого класса.

Также данный класс не должен отвечать за формат следующего метода, потому что нам могут понадобиться данные другого формата, например, XML, JSON, HTML и т.д.

Код после рефакторинга будет выглядеть так:

repo = $repo;
                $this->formatter = $formatter;
        }

        public function getOrdersInfo($startDate, $endDate)
        {
                $orders = $this->repo->getOrdersWithDate($startDate, $endDate);

                return $this->formatter->output($orders);
        }
}

namespace Report;

interface OrdersOutPutInterface
{
        public function output($orders);
}

namespace Report;

class HtmlOutput implements OrdersOutPutInterface
{
        public function output($orders)
        {
                return '

Orders: ' . $orders . '

'; } } namespace Report\Repositories; use DB; class OrdersRepository { public function getOrdersWithDate($startDate, $endDate) { return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get(); } }


Принцип открытости/закрытости (Open-closed Principle)


Программные сущности должны быть открыты для расширения, но закрыты для модификации.

Программные сущности (классы, модули, функции и прочее) должны быть расширяемыми без изменения своего содержимого. Если строго соблюдать этот принцип, то можно регулировать поведение кода без изменения самого исходника.

Рассмотрим пример:

width = $width;
        $this->height = $height;
    }
}

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

class AreaCalculator
{
    public function calculate($shape)
    {
        if ($shape instanceof Rectangle) {
            $area = $shape->width * $shape->height;
        } else {
            $area = $shape->radius * $shape->radius * pi();
        }
        
        return $area;
    }
}

$circle = new Circle(5);
$rect = new Rectangle(8,5);
$obj = new AreaCalculator();
echo $obj->calculate($circle);


Если нам нужно вычислить площадь квадрата, то нужно изменить метод вычисления в классе AreaCalculator. Но это нарушит принцип открытости/закрытости, согласно которому мы можем не изменять, а только расширять.

Как же можно решить поставленную задачу? Смотрите:

width = $width;
        $this->height = $height;
    }
    public  function calculateArea(){
        $area = $this->height *  $this->width;
        return $area;
    }
}
  
class Circle implements  AreaInterface
{
    public  $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }
    
    public  function calculateArea(){
        $area = $this->radius * $this->radius * pi();
        return $area;
    }
}

class AreaCalculator
{
    public function calculate($shape)
    {
        $area = 0;
        $area = $shape->calculateArea();
        return $area;
    }
}

$circle = new Circle(5);
$obj = new AreaCalculator();
echo $obj->calculate($circle);


Теперь можно найти площадь круга, не меняя класс AreaCalculator.

Принцип подстановки Барбары Лисков (Liskov Substitution Principle)


Этот принцип Барбара Лисков предложила в своём докладе «Data abstraction» в 1987-м. А в 1994-м идея была вкратце сформулирована Барбарой и Дженнет Уинг:

Пусть φ (x) — доказуемое свойство объекта x типа T. Тогда φ (y) должно быть верным для объектов y типа S, где S — подтип T.

В удобочитаемой версии повторяется практически всё, что говорил Бертранд Майер, но здесь в качестве базиса взята система типов:

  1. Предварительные условия не могут быть усилены в подтипе.
  2. Постусловия не могут быть ослаблены в подтипе.
  3. Инварианты супертипа могут быть сохранены в подтипе.

Роберт Мартин в 1996-м дал другое определение, более понятное:

Функции, использующие указатели ссылок на базовые классы, должны уметь использовать объекты производных классов, даже не зная об этом.

Попросту говоря: подкласс/производный класс должен быть взаимозаменяем с базовым/родительским классом.

Значит, любая реализация абстракции (интерфейса) должна быть взаимозаменяемой в любом месте, в котором принимается эта абстракция. По сути, когда мы используем в коде интерфейсы, то используем контракт не только по входным данным, принимаемым интерфейсом, но и по выходным данным, возвращаемым разными классами, реализующими этот интерфейс. В обоих случаях данные должны быть одного типа.

В этом коде нарушен обсуждаемый принцип и показан способ исправления:

toArray();
    }
}


Принцип разделения интерфейса (Interface Segregation Principle)

Нельзя заставлять клиента реализовать интерфейс, которым он не пользуется.

Это означает, что нужно разбивать интерфейсы на более мелкие, лучше удовлетворяющие конкретным потребностям клиентов.

Как и в случае с принципом единой ответственности, цель принципа разделения интерфейса заключается в минимизации побочных эффектов и повторов за счёт разделения ПО на независимые части.

Рассмотрим пример:



RobotWorker«у не нужно спать, но класс должен реализовать метод sleep, потому что все методы в интерфейсе абстрактны. Это нарушает принцип разделения. Вот как это можно исправить:



Принцип инверсии зависимостей (Dependency Inversion Principle)

Высокоуровневые модули не должны зависеть от низкоуровневых. Оба вида модулей должны зависеть от абстракций.

Абстракции не должны зависеть от подробностей. Подробности должны зависеть от абстракций.

Проще говоря: зависьте от абстракций, а не от чего-то конкретного.

Применяя этот принцип, одни модули можно легко заменять другими, всего лишь меняя модуль зависимости, и тогда никакие перемены в низкоуровневом модуле не повлияют на высокоуровневый.

Рассмотрим пример:

dbConnection = $dbConnection;
    }
}


Есть распространённое заблуждение, что «инверсия зависимостей» является синонимом «внедрения зависимостей». Но это разные вещи.

В приведённом коде, невзирая на то, что класс MySQLConnection был внедрён в класс PasswordReminder, последний зависит от MySQLConnection. Более высокуровневый PasswordReminder не должен зависеть от более низкуровневого модуля MySQLConnection.

Если нам нужно изменить подключение с MySQLConnection на MongoDBConnection, то придётся менять прописанное в коде внедрение конструктора в класс PasswordReminder.

Класс PasswordReminder должен зависеть от абстракций, а не от чего-то конкретного. Но как это сделать? Рассмотрим пример:

dbConnection =  $dbConnection;
    }
}


Здесь нам нужно изменить подключение с MySQLConnection на MongoDBConnection. Нам не нужно менять внедрение конструктора в класс PasswordReminder, потому что в данном случае класс PasswordReminder зависит только от абстракции.

© Habrahabr.ru