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

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

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

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

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

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

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

<?php
namespace Demo;
use DB;

class OrdersReport
{
    public function getOrdersInfo($startDate, $endDate)
    {
        $orders = $this->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 '<h1>Orders: ' . $orders . '</h1>';
    }
}

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

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

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

<?php
namespace Report;
use Report\Repositories\OrdersRepository;

class OrdersReport
{
        protected $repo;
        protected $formatter;

        public function __construct(OrdersRepository $repo, OrdersOutPutInterface $formatter)
        {
                $this->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 '<h1>Orders: ' . $orders . '</h1>';
        }

}

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)


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

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

<?php
class Rectangle
{
    public $width;
    public $height;
    public function __construct($width, $height)
    {
        $this->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. Но это нарушит принцип открытости/закрытости, согласно которому мы можем не изменять, а только расширять.

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

<?php
interface AreaInterface
{
    public  function calculateArea();
}

class Rectangle implements AreaInterface
{
    public $width;
    public $height;

    public function __construct($width, $height)
    {
        $this->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-м дал другое определение, более понятное:
Функции, использующие указатели ссылок на базовые классы, должны уметь использовать объекты производных классов, даже не зная об этом.
Попросту говоря: подкласс/производный класс должен быть взаимозаменяем с базовым/родительским классом.

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

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

<?php
interface LessonRepositoryInterface
{
    /**
     * Fetch all records.
     *
     * @return array
     */
    public function getAll();
}

class FileLessonRepository implements LessonRepositoryInterface
{
    public function getAll()
    {
        // return through file system
        return [];
    }
}

class DbLessonRepository implements LessonRepositoryInterface
{
    public function getAll()
    {
        /*
            Violates LSP because:
              - the return type is different
              - the consumer of this subclass and FileLessonRepository won't work identically
         */
        // return Lesson::all();

        // to fix this
        return Lesson::all()->toArray();
    }
}

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

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

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

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

<?php
interface workerInterface
{
    public  function work();
    public  function  sleep();
}

class HumanWorker implements workerInterface
{
    public  function work()
    {
        var_dump('works');
    }

    public  function  sleep()
    {
        var_dump('sleep');
    }
}

class RobotWorker implements workerInterface
{
    public  function work()
    {
        var_dump('works');
    }

    public  function sleep()
    {
        // No need
    }
}

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

<?php
interface WorkAbleInterface
{
    public  function work();
}

interface SleepAbleInterface
{
    public  function  sleep();
}

class HumanWorker implements WorkAbleInterface, SleepAbleInterface
{
    public  function work()
    {
        var_dump('works');
    }
    
    public  function  sleep()
    {
        var_dump('sleep');
    }
}

class RobotWorker implements WorkAbleInterface
{
    public  function work()
    {
        var_dump('works');
    }
}

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

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

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

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

<?php
class MySQLConnection
{
   /**
   * db connection
   */
   public function connect()
   {
      var_dump('MYSQL Connection');
   }
}

class PasswordReminder
{    
    /**
     * @var MySQLConnection
     */
     private $dbConnection;
     

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

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

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

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

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

<?php
interface ConnectionInterface
{
    public function connect();
}

class DbConnection implements ConnectionInterface
{

    /**
     * db connection
     */
    public function connect()
    {
        var_dump('MYSQL Connection');
    }
}

class PasswordReminder
{
    /**
     * @var ConnectionInterface
     */

    private $dbConnection;

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

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

Комментарии (66)


  1. PashaNedved
    31.05.2018 00:27
    +1

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

    Вопрос на миллион, насколько «общей» должна быть эта задача?


    1. AstarothAst
      31.05.2018 09:41

      А ответ на миллион заключается в том, что в реальной жизни какой-нибудь сервис авторизации выполняет кучу совершенно разных действий, но при этом внезапно начинает соответствовать SOLID, как только появляется формулировка «он решает задачу авторизации пользователей»! С формальной точки зрения все зависит от того, как дробишь задачи, как группируешь. С практической нужно для начала подумать чего мы SOLIDом добиваемся, а дальше уже смотреть в конкретный случай — добились? не добились?


      1. PashaNedved
        31.05.2018 15:57
        +1

        Формально, первый пример в статье соответствует SRP — предоставляет отчет по заказам. А практически SRP предлагает нам стать Вангой 99lvl. Я ничего не имею против принципа SRP при создании фреймворка, например, но когда дело доходит до предметной области, то приходит осознание непонимания этого принципа и как ему следовать не нарушая идей ООП.


        1. Fesor
          01.06.2018 11:05
          +1

          нам стать Вангой 99lvl.

          Интересный факт. В книжке Дяди Боба, где он SOLID расписывает, вся эта тема идет после главы о рефакторинге и постоянном улучшении кода.


          Это я к чему, попробуйте подумать о solid с другой стороны. Когда у вас код уже написан и вы вносите очередные правки. Все еще ли с этими правками код соблюдает solid или нам надо разбить этот класс и повыносить часть логики?


          Пытаться upfront сделать все по solid невозможно. Ведь даже если вы сможете в 100% случаев все делать соблюдая SRP то с OCP уже все намного сложнее.


    1. tangro
      31.05.2018 11:12
      +1

      Я бы сказал так — задача должна выражаться одним предложением, в котором нет сложноподчинённых частей и частицы «и».


      1. Dreyk
        31.05.2018 11:44

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


        1. VolCh
          31.05.2018 12:38
          +2

          три «и» :)


      1. Valle
        31.05.2018 19:15
        +1

        «Поиск в интернете»


  1. xvladimirov
    31.05.2018 00:56
    +1

    Последний пример чересчур упрощен, и скорее заведет в тупик новичка, чем ему поможет, на мой взгляд. У MySQLConnection и MongoDBConnection по определению будут разные интерфейсы, по этому лучше было бы сделать что-то вроде

    <?php
    interface UserRepository {
     // ...
    }
    
    class MySqlUserRepository implements UserRepository { /* реализация методов с учетом специфики mysql */ }
    class MongoUserRepository implements UserRepository { /* реализация методов с учетом специфики mongodb */ }
    
    class PasswordReminder
    {
        /**
         * @var UserRepository
         */
    
        private $repository;
    
        public  function __construct(UserRepository $repository)
        {
            $this->repository =  $repository;
        }
    }
    


    1. s_suhanov
      31.05.2018 13:56

      Но в последнем примере как раз это и написано. Просто пропущен код для class MongoDBConnection.


      1. xvladimirov
        31.05.2018 14:57
        +1

        Вы прочитали мой комментарий, прежде чем на него ответить?
        Я говорю о том, что это неудачный пример по тому, что в реальной жизни невозможно сделать общий интерфейс для MongoDBConnection и MysqlConnection ввиду разной специфики реляционных и документ-ориентированных баз.
        Если новичок попытается сделать что-то по конкретно этому примеру, его может поставить это в тупик.


        1. s_suhanov
          31.05.2018 15:03

          Вы слишком привязались к деталям, а на самом деле имели ввиду то же, что и автор: для разных видов БД можно сделать общий интерфейс и объявить в нашем PasswordReminder поле с типом этого интерфейса. А реализация у каждого конечно же своя. Просто у вас это Repository, у автора — Connection, но суть тут не в деталях, а в подходе. :)


  1. kiwokr
    31.05.2018 01:35

    Если честно статья каких миллион, уж извините. https://metanit.com/sharp/patterns/5.1.php
    Не сочтите за рекламу, но куда проще и доступнее объясняется.


    1. GrWizard
      31.05.2018 10:24

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


  1. rjhdby
    31.05.2018 02:23

    Стиль изложения такой, что начинающие не поймут, а те, кто поймет — тем оно уже и так известно и понятно.

    И немного комментариев по совсем уж в глаза бросившемуся:
    1. Если уж перевод, тем паче для новичков, то стоит и комментарии в коде перевести, так как они являются неотъемлемой частью объяснения.
    2. Принцип единственной ответственности относится не только к классам.
    3. Очередная статья, где НЕ смогли объяснить «Принцип подстановки Барбары Лисков».


    1. SH42913
      31.05.2018 10:25

      Подкинешь статью, где нормально разъясняется «Принцип подстановки Барбары Лисков»?


      1. rjhdby
        31.05.2018 11:44

        Да в той же вики вполне доступно описано.

        А вообще, Роберт Мартин в свое время очень емко его объяснил (в статье оно есть, только в максимально усложненном варианте, зачем-то)

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


  1. x-foby
    31.05.2018 07:34

    Господа, принцип ЕДИНСТВЕННОЙ ответственности, а не единой. Это так-то немного разные вещи, чуть менее, чем совсем.


    1. Dywar
      31.05.2018 19:37

      Мартин в своей новой книге дал более понятное объяснение этому принципу.
      Имеется ввиду пользователь/группа пользователей/актор, тот кто может инициировать изменение.

      «Программное обеспечение изменяется для удовлетворения нужд пользователей и заинтересованных лиц. Пользователи и заинтересованные лица как раз и есть та самая «причина для изменения», о которой говорит принцип.»


      1. QtRoS
        01.06.2018 20:46

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


  1. brnovk
    31.05.2018 08:47
    +1

    Каждый раз при прочтении подобных статей, натыкаюсь на одну и ту же ошибку, которая, видимо, никого кроме меня не раздражает. Почему примеры кода настолько надуманные?

    1. Пример с Order — ни в одном рабочем приложении не будет такого кода, ибо отчёты отталкиваются от бизнес логики и практически не встречаются в единственном числе. Почти всегда их несколько. Хоть сколько-нибудь опытный программист сразу мысленно пытается выделить общий функционал у различных отчётов, затем в силу знаний и навыков уже реализует какие-либо шаблоны проектирования, вспомогательные методы, хитрую ооп-иерархию и т.д. В приведённом примере используется различная 'кастомизация' вывода и источника данных, замечательно, но тип отчёта остаётся всё тем-же и к расширению не располагает.
    2. Пример с Area — у кого, в здравом уме, придёт в голову писать ООП-иерархию для вычисления площади геометрических фигур? Среднестатистический разработчик напишет 2-3 вспомогательных метода в утилитарном классе приложения, наподобие RectangleArea, CircleArea, понятные любому другому среднестатистическому программисту. Без этих ненужных усложнений.
    3. Пример с HumanWorker, RobotWorker — вообще без комментариев.
    4. Пример с Connection и Password, в сравнении с остальными, уже лучше, но всё равно кажется неестественным — пароль это сущность (чаще даже не сущность, а поле) пользовательского аккаунта, отдельный класс для нотификации выглядит не самым естественным образом.

    В вашей компании 3.5 тысячи сотрудников. Неужели не удалось найти хоть пару разработчиков, в практике работы которых, встречались примеры получше?


    1. Alozar
      31.05.2018 10:43

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


      1. Femistoklov
        01.06.2018 13:01

        Так и есть. Мне это напоминает собеседования — спрашивают про принципы, паттерны, лучшие практики. А если устроишься и покопаешься в проектах — так там как раз кривой Legacy код с костылями.


  1. nepster-web
    31.05.2018 08:50

    Это я не совсем правильно понял или принцип «LSP» в данном примере суховат?
    Особенно неприятно в 2018 году видеть примеры на php без указания типов (возвращаемых значений).


  1. Jenly
    31.05.2018 11:31
    -1

    Человечество не придумало примера для Open-closed Principle без использования геометрических фигур.


    1. MrDaedra
      01.06.2018 09:44
      +1

      Этот какое-то проклятие, такое же, как и в статьях про модульные тесты, где проверяется, что 2+2 равно 4.


  1. aosja
    31.05.2018 11:42

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

    SOLID — это Design. Мне кажется, архитектура приложения лежит на более высоком уровне.


    1. VolCh
      31.05.2018 12:45

      Design — это «проектирование», проектирование архитектуры — его часть, а не другой уровень.


      1. aosja
        31.05.2018 14:36

        Приведите пример архитектурного решения для принципа «Dependency Inversion».


        1. VolCh
          31.05.2018 17:43

          Использование единого DI-контенейра для иницализации всех зависимостей классов приложения. Это архитектурное решение.


          1. aosja
            31.05.2018 17:51
            +1

            Это не архитектурное решение


            1. VolCh
              31.05.2018 18:02

              А как вы называете решения, от которых зависит реализация минимум половины кода приложения?


              1. aosja
                31.05.2018 18:13

                зависит реализация минимум половины кода приложения

                Вы серьезно? Я могу поменять один DI контейнер на другой и это вряд ли повлечет за собой изменение половины кода. Только сам код конфигурациии DI, а это далеко не половина.
                Примеры архитектурных решений: нужна секьюрити/не нужна, используем БД/документную/реляционную/не используем, eventual-consistency/transactional-consistency, и т.п.


                1. VolCh
                  01.06.2018 03:53

                  Использование DI вообще и контейнера в частности — это архитектурное решение. Какой контейнер конкретно — деталь реализации.


                  1. aosja
                    01.06.2018 09:03

                    Архитектурные решения обусловлены функциональными и не функциональными требованиями.
                    Будет использоваться DI или нет — это выбор программиста, а не решение, которое принимает архитектор. Это, — использовать DI, как руки мыть перед едой, какая еще архитекрута...


                    1. shuron
                      01.06.2018 13:57

                      Это вы из какой-то методики цитируете или сами опредилили так? Например жесткие роли.


                    1. Fesor
                      01.06.2018 17:18

                      это выбор программиста, а не решение, которое принимает архитектор.

                      кто такой архитектор? Зачем он нужен? в чем его функция?


          1. Fesor
            01.06.2018 17:16

            DI контейнер это про ивенсию управления, а не инверсию зависимостей же.


        1. Fesor
          01.06.2018 17:16

          архитектура портов и адаптеров (hexagonal которая) полностью построена на идее dependency inversion.


          SOLID это принципы, набор ограничений. Архитектура приложения это совокупность всех ограничений, которые мы накладываем на систему. Все это является частью процесса проектирования. Как выбор ограничений так и их применение.


    1. rjhdby
      31.05.2018 14:49

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


      1. aosja
        31.05.2018 15:04

        SOLID —… это набор рекомендаций

        Да, я в курсе. Но все же, не просто рекомендаций на любой случай жизни, а для конкретной области. Возможно, я не так выразился первый раз… Я считаю, что SOLID, это принципы дизайна кода (который я просто назвал дизайном), но не архитектурные принципы.


  1. DistortNeo
    31.05.2018 12:35

    А зачем городить такую монструозную конструкцию:


    class AreaCalculator
    {
        public function calculate($shape)
        {
            $area = 0;
            $area = $shape->calculateArea();
            return $area;
        }
    }
    
    $circle = new Circle(5);
    $obj = new AreaCalculator();
    echo $obj->calculate($circle);

    Когда гораздо проще писать так:


    $circle = new Circle(5);
    echo $shape->calculateArea();


  1. evgwed
    31.05.2018 12:51

    Стоит отметить, что SOLID принципы делают разработку более долгой и сложной. Требуется создавать множество интерфейсов и абстрактных классов, что может показаться избыточным и ненужным.
    Но при доработке или расширении вашего приложения вы поймете для чего все это было нужно.

    Вывод: SOLID принципы хороши в больших и продолжительных проектах, на коротких проектах это долго и дорого.


    1. DistortNeo
      31.05.2018 13:03
      +1

      Дело даже не в SOLID, а в тестируемости кода. Код, написаный по SOLID, тестировать легко. Код, который не следует SOLID, тестировать сложно.

      Если у вас есть необходимость тестировать код (или даже использовать TDD), то вам придётся следовать принципам SOLID, иначе вы попросту не сможете писать тесты.

      То есть всё зависит от того, нужно ли писать тесты или нет. На коротких и неподдерживаемых проектах тесты действительно замедляют разработку. На серьёзных же проектах без тестов никуда: выигрыш времени на старте приведёт к значительно большим потерям в будущем.


      1. VolCh
        31.05.2018 17:46

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


      1. evgwed
        01.06.2018 10:45

        По SOLID можно писать без тестов, но вот тесты нельзя писать по SOLID.
        Для того, чтобы тесты работали не обязательно следовать каждому принципу, до большинства вещей доходят при расширении приложения.


        1. DistortNeo
          01.06.2018 10:58

          Написание хороших тестов автоматически принуждает к использованию SOLID, даже если программист вообще не знает, что это такое.


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

          Принцип KISS тоже никто не отменял.


  1. springimport
    31.05.2018 16:22

    А нет какого-то принципа для решения следующей проблемы: имеется базовый класс с наследниками. Нужно сделать кэширование для некоторых методов.
    Можно решить это добавлением кэширования в объект с которым работают классы. Но нужна реализация над классами.


    1. DistortNeo
      31.05.2018 16:36

      Типа этого?


      class Base
      {
      public:
          virtual int CalculateSomething();
      };
      
      class Derived: public Base
      {
      public:
          int CalculateSomething() override { ... }
      };
      
      class Cached: public Base
      {
      private:
          std::unique_ptr<Base> base;
          std::optional<int> cachedValue;
      
      public:
          Cached(std::unique_ptr<Base> base): base(std::move(base)) {}
          int CalculateSomething()
          {
              if (!cachedValue) { cachedValue = base->CalculateSomething(); }
              return cachedValue.value();        
          }
      };


      1. springimport
        31.05.2018 16:52

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

        Можно, конечно, написать для каждого прокси, но это не элегантное решение.


        1. DistortNeo
          31.05.2018 17:12

          Для упрощения можно убрать метод в базовом классе и оставить только в наследниках.

          Тогда причём здесь вообще базовый класс?


          1. springimport
            31.05.2018 17:40

            Согласен что пример можно упросить. Базовый класс можно не учитывать.


    1. VolCh
      31.05.2018 17:47

      декоратор?


      1. springimport
        31.05.2018 18:14

        Это решение примерно как прокси. Если нужно задекорировать модель с данными то все усложняется.


    1. Dywar
      31.05.2018 19:22

      Нужно кэшивароние — прокси.
      Нужен lazy load — прокси.
      Нужен контроль доступа — проски.

      Нужно имитировать/эмулировать/создать динамическое наследование — декоратор.


  1. greabock
    31.05.2018 20:34
    +2

    Я не знаю откуда столько плюсов. Примеры очень плохие.


    S
    У нас был килограмм гуано. После "рефакторинга", мы получили четыре куска гуано по 250 грамм.


    O
    Показан пример (довольно слабый) с полиморфизмом.
    Не показан пример с наследованием.


    L
    ОЧЕНЬ слабый пример. Который просто говорит о соблюдении интерфейсов, и вообще не затрагивает тот момент, что принцип подстановки при указании возвращаемых значений в PHP работает неправильно. Не разобраны примеры с усилением предусловий и ослаблением постусловий.


    I
    Ну а где пример с композицией интерфейсов?


    D
    Ну тут ладно, тут попадание.
    Почему инверсия зависимостей идет бок о бок с внедрением зависимостей?
    Ну потому, что последнее невозможно без соблюдения первого.


    1. greabock
      31.05.2018 20:42

      при указании возвращаемых значений

      при указании типов входящих значений, простите


      примеры с усилением предусловий и ослаблением постусловий

      примеры с ослаблением предусловий и усилением постусловий, конечно же. Опечатался.


    1. Leoon
      01.06.2018 06:53

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


      1. VolCh
        01.06.2018 07:32

        Или знают, но считают, что теперь ещё больше людей вокруг будут знать.


    1. Fesor
      01.06.2018 17:19

      Не показан пример с наследованием.

      а зачем его показывать? composition over inheritance и все такое. OCP это НЕ про наследование. Это про точки расширения. Точки расширения за счет того что у вас класс не final это так себе решение.


      что принцип подстановки при указании возвращаемых значений в PHP работает неправильно.

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


      Ну потому, что последнее невозможно без соблюдения первого.

      наоборот, инверсия зависимостей требует наличия инверсии управления (DI это как раз реализация второго).


      1. BradMaslov
        02.06.2018 01:19

        Тема SOLID, к сожалению, осталась не раскрытой. Нужны примеры по лучше.
        Сразу вспоминаю учебу в университете. Тогда его SOLID это не называли (по крайней мере я об этом не слышал тогда) но в книгах по проектированию можно было встретить каждый из этих принципов, который "рекламируют" сейчас как SOLID.
        Тогда мой неокрепший ум, начитавшись таких принципов, сразу ринулся распылятся паттернами и принципами направо и налево. Продвигание ООП преподавателями — крепко осело в моем мозгу — "Наследование, полиморфизм, инкапсуляция"...


        Я к тому, что новичкам это конечно полезно знать, но это все таки слишком сложная тема, что бы так вот просто (и местами плохо) её изложить. Понимание и целесообразность SOLID и принципов ООП без опыта не придет. Хоть 1000 статей на эту тему сделать — толку мало будет.


        Проблема в том, что на сегодняшний день куча технологий и архитектурных решений продвигаются как панацея. Люди продвигают то, что знают или видели. Но только обладая опытом и поддавая все критике можно сбалансировать все эти знания. Вот этот вот последний абзац, нужно прилагать в конце каждой статьи. Что бы наивный неокрепший ум знал, что юзать можно, но ооочень осторожно) А то хуже будет.


        1. Fesor
          02.06.2018 19:08

          Нужны примеры по лучше.

          примеры по solid надо рассматривать исключительно в динамике. Я не представляю как принцип описываемый фразой "единая причина для изменений" можно продемонстрировать без этих самых изменений.


          можно было встретить каждый из этих принципов

          package principle появились в 98-ом вроде, а то и раньше. LSP и OCP — 88-ой. DIP — в целом существовал очень давно. И все эти штуки основаны либо на логике хоара (60-ые) либо на принципах структурного дизайна (1970).


          Продвигание ООП преподавателями — крепко осело в моем мозгу — "Наследование, полиморфизм, инкапсуляция"...

          В моей памяти как-то особо упор на наследовании делали. От инкапсуляции только data hiding, и то вскользь. Да и полиморфизм только в том контексте который родился из примеров наследования. Я могу объяснить это тем, что это проще показать чем саму идею. Это просто фичи языка. А про связанность, cohesion, information hiding… все это как-то опускается.


          но это все таки слишком сложная тема, что бы так вот просто (и местами плохо) её изложить.

          тема не сложная, просто это к вопросу о последовательном обучении.


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

          очень важный момент что как панацею эти решения продвигают немного другие люди, нежели те кому эти технологии приписывают. А тут нужно развивать скил критического мышления и анализа поступающей информации. Особенно если тебе сыпят маркетинговыми терминами или категоричными заявлениями. Увы этому в школах/универах почти не учат.


      1. greabock
        02.06.2018 13:27

        OCP это НЕ про наследование

        и НЕ про композицию. А потому, примеры нужны разные.


        не умеет в ковариантность/контрвариантность для пре/пост кондишенов

        таки с постусловиями там всё норм.


        dependency inversion — вообще не интересует, каким образом поставляются зависимости, ты можешь делать это хоть в императивном стиле, хоть в декларативном через механизм DI. Ничего инверсия зависимостей не требует.


        1. Fesor
          02.06.2018 18:24

          таки с постусловиями там всё норм.

          в каком месте?


  1. evseev
    02.06.2018 12:49

    По-моему эта статья объясняет только одно. Не стоит слепо следовать ни одному из принципов. Какими-бы замечательными они не казались. Возьмем пример из «Принцип разделения интерфейса». Мы замечательно разделили интерфейс. Разве не чудесно? С первого взгляда да. Но давайте пойдем чуть-чуть дальше. Мы ведь теперь хотим работать с этими объектами. Мы создаем некоторое количество воркеров обоих типов. И даже складываем их в какую-то структуру данных. И теперь хотим каким-то образом их запустить, а потом отправить на отдых. Но просто шарахнуть циклом мы не можем. Объекты вроде-бы и очень похожие, но вот методы не совпадают. Т.е. нам нужна дополнительная проверка. Если мы их куда-то передаем, то там нужно учитывать эту разность. Я просто предчувствую как обрадуется этому факту наши коллеги. В этом случае объектов очень мало. А представим, что это реальный проект и таких объектов 2-3 десятка. И, скажем, 3 из них еще нигде не встречаются. Это новая ветвь развития рабочих. Вы все чудно отладили и отдали заказчикам. И вот в один прекрасный момент оказывается, что один из заказчиков таки запустил эту новинку. Но их пока мало. Поэтому все работает пока мы не доходим до нового образца. И вот именно тогда все вдруг улетает к чертовой бабушке. Мы перезапускаем процесс и опять все хорошо. И все хорошо часами, сутками, неделями. И… вот опять все рухнуло. И ведь проект собирается, и выглядит все замечательно, но заказчик почему-то не радуется, а больше волком смотрит. Но зато все по SOLID.


    1. Fesor
      02.06.2018 18:26

      Но просто шарахнуть циклом мы не можем.

      мораль — никто опять ничего не понял.


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


      А то что вы описали это не про SOLID. Это про то, как люди думаю что понимают в чем смысл SOLID а на самом деле занимаются фигней.