Ответы пользователя по тегу SOLID
  • Как реализуется принцип открытости/закрытости в случае "ветвления" расширений в Java?

    Fesor
    @Fesor
    Full-stack developer (Symfony, Angular)
    основной — наследование.


    Наследование типов а не классов. То есть через интерфейсы.

    В Java нет множественного наследования.


    В Java есть возможность делать композицию типов путем имплементации множества интерфейсов. А с учетом того что в Java8 у вас теперь есть возможность делать "дефолтные" имплементации методов в интерфейсах - у вас есть "правильное" наследование.

    Через некоторое время, этот класс захотят расширить по OCP — унаследуются, и сделают класс Бармен.


    А как потом быть если мы захотим сделать робота бармена? Я это к тому что "человек" не всегда будет являться базовым типом "бармена".

    В целом Dmitry Roo вам верно сказал. "Бармен" это профессия. Свойство человека. У человека может быть много профессий:

    class Human
    {
        Profession[] professions;
    }


    Таким образом мы можем крутить и вертеть как хотим.
    Ответ написан
    Комментировать
  • Как разобраться с инверсией зависимостей?

    Fesor
    @Fesor
    Full-stack developer (Symfony, Angular)
    Что такое адаптер?


    Смотрите. Есть у вас например micro USB кабель. И есть дырка в новом макбуке - Usb type c. Друг в друга они, как вы понимаете, не втыкаются. И можно взять адаптер microUSB -> USB type-c.

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

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

    За счет этого достигается независимость.

    Что значит "принимает зависимость"?


    Предположим у нас есть метод для смены пароля юзера. Что бы это безопасно провернуть, нам надо пароль захэшировать и хранить в захэшированном виде. То есть нам нужна какая-то штука, которая будет эти пароли хэшировать. Мы о ней знать ничего не хотим, хотим что б нам ее давали:

    public function changePassword(string $password, PasswordEncoder $encoder)
    {
        $this->password = $encoder->encode($password);      
    }


    Это зависимость нашего метода. Он зависит от него. Диалог между объектами можно представить себе такой:

    - Слыш, поменяй пароль на этот
    - Оке, только хэшер паролей мне дай, мне очень надо
    - А какой тебе?
    - Да любой с этим типом
    - Ну ок. На вот пароль и хэшер. Делай дела.

    Что такое вообще эта зависимость?


    Зависимости - это все что мы используем чтобы сделать дела. Это не только библиотечки, но и просто классы, функци и т.д. Весь "сторонний" код с точки зрения нашего кода. И самое важное в том, что "нашим" кодом является тот, над которым мы работаем в данный момент времени, а не все что мы написали. Даже функции, которые есть в языке программирования из коробки являются зависимостями. Вот только от них вам не деться никуда особо, а потому с ними замарачиваться не стоит. Или если есть долговременная поддержка у библиотеки и она устаялась - тоже можно просто использовать. А вот если это поделка на гитхабе с 10-ю звездочками и там до сих пор нет ни одного релиза - но она вам вот очень нужна, возможные поломки в ней (а они рано или поздно будут) стоит "закрыть" адаптером что бы потом поменять на что-то получше или обновить без боли.
    Ответ написан
    Комментировать
  • Как лучше разбить логику?

    Fesor
    @Fesor
    Full-stack developer (Symfony, Angular)
    Пытаюсь понять SRP


    Давайте думать. Вот есть у вас класс который:

    - занимается аутентификацией пользователя
    - занимается регистрацией пользователя
    - занимается восстановлением пароля

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

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

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

    p.s. маленький вброс: https://gist.github.com/fesor/33f041e3f362beff8d0e...
    Ответ написан
    1 комментарий
  • Как использовать Принцип подстановки Барбары Лисков применительно к PHP?

    Fesor
    @Fesor
    Full-stack developer (Symfony, Angular)
    В PHP отсутствует Double Dispatching и перегрузка методов


    Double Dispatch в PHP:

    class Foo {
        // ...
        public function makeSomeStuff(Bar $bar)
        {
             $bar->doStuff($this->someData); // double dispatch!
        }
    }


    Перегрузка методов:

    class Foo {
        public function foo() {}
    }
    
    class Bar extends Foo {
        public function foo() {} // перегружен!
    }


    и в ответ на отличающуюся от базового класса/интерфейса сигнатуру он вывалится с ошибкой


    то что вы хотели сделать называется ad-hoc полиморфизм, и он есть из коробки в любом языке программирования с динамической типизацией. Достаточно просто не указывать явно сигнатуру, все довольно просто) Ну и да, минус этого то что это не явно и в рантайме. Для языков со статической типизацией явная "перегрузка" нужна только для того что бы компилятор мог построить таблицы диспетчеризации вызовов.

    Но решить проблему как-то нужно


    Перегрузка методов в наследниках с изменением сигнатуры это как раз таки нарушение принципа подстановки барбары лисков (LSP для сокращения).

    > Сервисы должны имплементировать какие-то общие методы, но помимо этого у них есть и специфичные методы.

    выносите "общие методы" в отдельный сервис и шарьте его как зависимость. Тогда у всех сервисов будут только специфичные методы и тогда будет достигаться принцип единой ответственности (который характеризуется как "у каждого объекта должна быть только одна причина для возможных изменений").

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

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


    Значит полиморфизм в нашем случае пошел погулять. Есть куча решений данной проблемы, в частности Chain of responsibility.

    > Visitor. Предполагает наличие в каждом из хендлеров методов типа handleService1(Service1 $service) и handleService2(Service2 $service), при этом один из методов остается пустым

    зачем так усложнять то? У вас должен быть снаружи только один публичный метод а внутри уже реализация сама разберется. Ну то есть если хотите - можете внутри сделать два приватных метода но это так же странно.

    > Массив маппинга, который говорит, какой хендлер может обрабатывать какой из сервисов.

    Опять же излишнее усложнение.

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

    Дальнейшие варианты возможны только после того, как вы опишите на высоком уровне что вам нужно сделать. Ну то есть не то к чему вы пришли а почему вы к этому пришли и какая задача стояла изначально.
    Ответ написан
    8 комментариев
  • Как не нарушать SOLID?

    Fesor
    @Fesor
    Full-stack developer (Symfony, Angular)
    вы путаете инверсию контроля и инверсию зависимости. Давайте по порядку кратенько.

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

    Зачем нам в принципе контроллер? Что он делает? Для упрощения не будет воспринимать контроллер как "один объект" и вместо этого представим себе его как целый слой. Так же заменим слово "модель" словом "приложение".

    Задача контроллера - принять и обработать запрос и выдать ответ. По сути в контексте WEB наш HTTP запрос и ответ это представление, которое хочет получить клиент (браузер, мобильное приложение, SPA, что угодно). HTTP - это интерфейс пользователя (UI) для нашего web-приложения.

    Например что бы независеть от реализации клиента и что бы было удобно мы передаем даты в формате iso 8601 (пример: 2016-07-14T19:40:12Z). Это удобно что бы быть независимым от реализации клиента или сервера. Но это не удобно для нашего приложения. В приложении скорее всего нам удобнее всего работать с объектом типа DateTime. То есть приложение использует абсолютно другое представление.

    Мы могли бы прямо в приложении конвертить DateTime в iso 8601 но тогда мы делаем наше приложение привязанным к одному конкретному представлению, которое хочет получить клиент. К примеру по каким-нибудь причинам известным только темным богам, вам вдруг понадобится быстро прикрутить интеграцию с другим сервисом и те же данные гонять уже в RFC2822. И стало быть уже приложению нужно париться о еще одном представлении.

    Мы могли бы сделать какие-то адаптеры у приложения, и дергать их в зависимости от потребностей, но тогда опять же наше приложение все еще знает о представлении, которое ему собственно не нужно. То есть у нас есть зависимость приложения от его UI что... похоже на "не лучшую идею". И тут на помощь приходит Inversion of Control.

    Что такое Inversion of Control

    Тут название само говорит за себя. Допустим у нас был объект A который дергал объект B, причем объект A по сути и не должен ничего знать об объекте B потому то это не его дело. Принцип инверсии контроля говорит нам о том, что в таких ситуациях именно B должно вызывать A, таким образом меняя направление потока управления. Это позволяет нам уменьшить связанность и повысить зацепление компонентов нашей системы. Так же сделав это у нас может появиться объект C который так же будет дергать объект A. Если говорить о UI - мы просто можем сделать несколько реализаций UI.

    То есть если еще упростить - фреймворк должен дергать ваш код, а не код дергать код фреймворка. Тем самым мы снижаем связанность одного от другого.

    Роутер и контроллеры как реализация UI

    Что бы отвязать приложение от логики формирования представления, вынесем это все в отдельный "слой" и назовем этот слой - контроллеры. Точнее это будет как цепочка адаптеров. Один адаптер (фронт-контроллер по сути) получает Request и делает какие-нибудь вещи с ним. Например проверяет можем ли мы вообще делать подобный запрос. Другой адаптер вызывает роутер и выясняет какой дальше адаптер вызвать. Если следующий адаптер не вызван - надо вернуть 404-ую ошибку. Если же все пошло хорошо - мы вызываем еще один адаптер, который уже будет конвертировать HTTP запрос в какое-то действие приложения (вызов метода приложения по сути).

    Так а инверсия зависимости это что?

    Инверсия зависимости - очень похожа на инверсию контроля но действует чуть по другому. Проще всего будет вглянуть на картинку:

    Dependency_inversion.png

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

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

    Нужно ли соблюдать принцип инверсии зависимости в случае контроллеров?

    Нет. Контроллеру нужна конкретная реализация какой-то части нашего приложения (ибо приложение главнее UI-ки), иначе в них нет особо смысла. И наше приложение вообще не должно париться о том что есть какие-то там контроллеры.

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

    Это уже вопрос реализации IoC. Конкретно вы хотите получить что-то вроде Dependency Injection. Вы можете забрать зависимости из аргументов метода экшена. или аргументов конструктора контроллера.... или просто использовать контейнер зависимостей внутри контроллера.... это совершенно не важно. Контроллеры это то место где высокая связанность на компоненты фреймворка более чем допустимы.

    С другой стороны у вас теперь роутинг совмещает обязанность маршрутизации и разруливания зависимостей. Сами понимаете что это как-то нарушает прицип единой ответственности. Этим может заниматься Controller Resolver какой-нибудь.
    Ответ написан
    2 комментария
  • Проектирование веб-программы, правильно ли я сделал?

    Fesor
    @Fesor
    Full-stack developer (Symfony, Angular)
    AbstractTask нужен только для DRY. В вашем случае он пока лишний, чуть что добавите позже. Даже больше, можно пока даже интерфейс отдельно не делать. Если в будущем нам понадобится добавить еще одну реализацию скажем... Task-а или TaskManager-а, выделение инетфейса класса вам сделает любая нормальная IDE. Назовете этот интерфейс TaskManager и будет у вас реализация оного ScheduledTaskManager и DBTaskManager... для примера. А клиентский код такие штуки не затронет вовсе.

    Советую вооружиться PhpSpec и таким образом проверять насколько удобно вы спроектировали API ваших сервисов.
    Ответ написан
    1 комментарий