Разбираемся с SOLID: Инверсия зависимостей

    Давайте глянем на определение принципа инверсии зависимостей из википедии:


    Принцип инверсии зависимостей (англ. dependency inversion principle, DIP) — важный принцип объектно-ориентированного программирования, используемый для уменьшения связанности в компьютерных программах. Входит в пятёрку принципов SOLID.

    Формулировка:

    A. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
    B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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


    Модули


    модуль — логически взаимосвязанная совокупность функциональных элементов.

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


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


    Граф зависимостей

    Здесь стрелочки между модулями показывают кто что использует. Соответственно эти же стрелочки будут показывать нам направления зависимостей между нашими модулями.


    И вот пришло время добавить "еще одну кнопочку". И мы понимаем что функционал этой кнопки реализован в модуле E. Мы не раздумывая полезли добавлять то что нам надо, и нам пришлось поменять интерфейс взаимодействия с нашим модулем.


    Мы уже хотели закрыть задачу, закоммитить код… но мы же что-то поменяли… пойдем смотреть не сломали ли мы кого. И тут оказывается что из-за наших изменений сломался модуль B. Окей. Починили. А вдруг кто-то кто использует модуль B тоже сломался? И в правду! Модуль A тоже отвалился. Чиним… Коммитимся, пушим. Хорошо если есть тесты, тогда о проблемы мы узнаем быстро и быстро сможем исправить. Но давайте посмотрим правде в глаза, мало кто пишет тесты.


    А еще коллеге вашему прилетел баг от тестировщика, мол модуль C сломался. Оказалось что он по неосторожности завязался на ваш модуль E, а вам об этом не сказал. Да еще и модуль этот состоит из кучи файлов, и всем от модуля E что-то нужно. И вот теперь и он лазает по своей части графа зависимостей (потому что ему проще в нем ориентироваться чем вам, не ваша же часть системы) и проклинает вас.


    Изменения в графе зависимостей

    На рисунке выше, оранжевый кружочек обозначает модуль, который мы хотели поправить. А красные — которые пришлось поправить. И не факт что каждый кружок — один класс. Это могут быть целые компоненты. И хорошо если модулей у нас не сильно много и они не сильно пересекаются между собой. А что если у нас каждый кружочек был бы связан с каждым? Это ж чинить все на любой чих. И в итоге простая задача "кнопочку добавить" превращается в рефакторинг куска системы. Как быть?


    Интерфейсы и позднее связывание


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

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


    Мы вспоминаем о требовании, но не бросать же нам все? Давайте все же сначала закончим с регистрацией пользователя, а уж потом будем разбираться как чего делать. Надо все же последовательно подходить к работе. А потому вместо того чтобы гуглить "как правильно хэшировать пароль" или разбираться как это делать в нашем фреймворке, давайте сделаем интерфейс PasswordEncoder. Сделав это, мы создадим "контракт". Мол всякий кто решится реализовать этот интерфейс, обязан предоставить надежное и безопасное хэширование пароля. Сам же интерфейс будет до безумия простым:


    interface PasswordEncoder
    {
        public function encode(string $password): string;
    }

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


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


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


    Инверсия зависимостей


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


    Для этого в своем модуле C он создал интерфейс, и написал простенький адаптер, который принимает зависимость из нужного модуля и предоставляет доступ только к нужному методу. Теперь если вы что-то поправите — исправить "поломку" можно будет в одном месте.


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


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


    Граф зависимостей с инверсией

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


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


    Не все зависимости стоят того, чтобы их инвертировать


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


    К примеру, если нам понадобится логгер, мы всегда сможем использовать интерфейс PSR\Logger поскольку он стандартизирован, а такие вещи крайне редко меняются. Затем мы сможем выбрать любой логгер реализующий этот интерфейс на наш вкус:


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

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


    Изоляция


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


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


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


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


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


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


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

    Поделиться публикацией

    Комментарии 21

      0
      Добавлю, пожалуй, что не обязательно именно вводить интерфейсы (или абстрактные классы) как сущность языка. В формулировке принципа DI абстракция это не обязательно интерфейс или абстрактный класс (их в конкретном языке может вообще не быть, например, в JS или PHP4). Это может быть какая-то реальная абстракция :), существующая только в голове разработчика.

      Связывание между конкретными сущностями приложения может быть жёстким статическим, границы модулей расплывчатыми, абстракция от которой всё зависит не то, что явно в коде не выражена, но даже не документирована, но можно считать, что реализация соответствует DI, если разработчик имел в голове эту абстракцию, если эти жёсткие статические зависимости не текут.
        +2
        реализация соответствует DI

        DIP, конечно
          0
          не обязательно именно вводить интерфейсы

          Именно. Нунжно просто добавить тип. А класс это или интерфейс — это уже детали. Вот только в этом случае имеет смысл отказываться от типичного суффикса Interface чуть что.


          Например был у нас класс Notificator. Мы с ним все сделали и закрыли зависимость. А потом мы захотели другую реализацию этого адаптера. Большинство разработчиков не раздумывая введут интерфейс NotificatorInterface и быстренько заменят его повсюду где он используется в нашем модуле.


          Итог — git коммит будет содержать изменения в много-много файлов. Тогда как можно было сделать Notificator интерфейсом, сделать… DefaultNotificator класс например. В этом случае git коммит содержал бы только два файла. Интерфейс и реализация.


          например, в JS или PHP4

          В JS интерфейсом будет служить то, что выплевывает наружу модуль. В PHP4 да, там особо не разгуляешься. Он чисто процедурный и модульность не умеет. Но можно классы использовать как модули.


          если разработчик имел в голове эту абстракцию, если эти жёсткие статические зависимости не текут.

          тут еще стоит учитывать и сегрегацию интерфейсов (могу об этом попробовать написать) и open/close принцип. Многие например пренебрегают сегрегацией интерфейсов, а ведь эта штука в добавок к DIP реально снижает шансы накосячить.

            +2
            В принципе все принципы SOLID нужно использовать совместно и гармонично, не допуская перегиба в сторону только одного или нескольких. Но на практике хорошо если следуют хотя бы SRP :(

            Думаю, продолжение описание остальных принципов в том же духе было бы полезно для сообщества. Я бы ссылки давал джунам или даже не прошедшим собеседование на джуна :)
          0
          > Мы должны стараться делать модули как можно более изолированными и самодостаточными.
          Хорошо, но вот вопрос. Если (из приведенной иллюстрации) модуль B зависит от модуля D, тогда принцип «как можно более» просто требует от нас отсутствия модулей 2 и 3 уровней — то есть, тогда придется оставить только один уровень модулей. Иначе говоря.

          Компонент A использует Symfony/Finder. Компонент B использует его же. Но компоненты A и B друг о друге даже не подозревают. Вот это вот «как можно более» разве не требует переписать компоненты A и B так, чтобы (кстати, чтобы — в данном контексте пишется слитно) они не использовали Finder? Или как? Я не понимаю, где пределы принципа «как можно более».
            +2
            DIP требует переписать A, B, а также и Symfony/Finder (или написать обёртку для него) так, чтобы A и B зависели от какой-то абстракции (в данном случае, наверное, интерфейса типа FileFinderInterface), а Symfony/Finder (или обёртка) её реализовывали. Например, чтобы без всяких проблем модули A и B могли переключиться с локально примонтированной ФС к какому-то удалённому хранилищу, возможно вообще традиционную иерархическую файловую систему лишь эмулирующему для не очень продвинутых клиентов, а по факту хранящему наборы данных в графовой СУБД. Причём могли это сделать независимо друг от друга. Компоненты A и B тогда не будут зависеть от Symfony/Finder, они будут зависеть только от интерфейса FileFinderInterface. Сам интерфейс не должен зависеть от деталей Symfony/Finder, а это детали Symfony/Finder должны зависеть от него.

            DIP не продвигает идею набора одноуровневых модулей, ни от чего не зависящих. DIP продвигает идею зависимости от абстракций, которые модули верхнего уровня используют, а нижнего — реализуют. Ни те, ни другие в идеале не должны знать друг о друге вообще ничего, только об абстракциях. Хороший пример с PSR/Log как раз — одна группа разработчиков придумала абстракцию, я её использую в своих модулях для записи логов, а, вы, например, пишите модуль логирования, реализующий эту абстракцию для логирования в какое-то экзотическое хранилище. Мне, как разработчику модуля, даже название вашего модуля знать не надо в общем случае, ссылку на него я возьму в каком-нибудь контейнере, или получу параметром в методе, или ещё где. Причём я даже не буду знать как класс вашего модуля называется, мне только нужно знать, что он реализует PSR/Log, а кто, куда и как логирует реально — не нужные мне детали, решения о которых принимает разработчик приложения в целом, а то и вообще не разработчик, а эксплуататор (читай — админ или девопс). А вы тем более не будете знать, что мой модуль использует ваш модуль для логирования (ну если не встроете в него какиой-то шпион). Вот это реальный DIP в жизни.

              0
              Теперь я понял термин «уменьшение связанности». В английской версии, кстати, звучит достаточно адекватно, а в русском варианте термин вводит в заблуждение. Но это лично мое мнение. Спасибо за ответ. Хотя, если честно, все изложенное более-менее не открытие для меня. Если долго работаешь с качественным кодом (фреймворки, сторонние библиотеки), рано или поздно вырабатывается вкус и начинаешь подражать тому, с чем работаешь.
                +2
                Различные паттерны проектирования это, прежде всего, язык для коммуникаций. Для хорошего опытного разработчика изучение этого языка не должно быть открытием типа «вот, оказывается, как надо делать!», а должно быть открытием типа «вот, оказывается, как называется то, что я давно делаю!» :)
            0
            Так же, если вы посмотрите на картинку выше, вы можете заметить, что поскольку реализация адаптеров лежит в модуле E, теперь этот модуль вынужден реализовывать интерфейсы из других модулей. Тем самым мы инвертировали направление стрелочки, указывающей зависимость. Мы инвертировали зависимости.

            Что делать, если модуль E сторонний?
              +1

              Часто в этой ситуации делают "мосты". Это какой-то промежуточный модуль, который будет содержать нужные нам адаптеры. Он как бы в нашей власти, и мы можем делать с ним все что захотим. И то что он будет напрямую зависеть от стороннего модуля — ну так для этого он и нужен.

                +1
                  +1

                  Да, Гексагональная архитектура это яркий пример крайностей как при помощи инверсии зависимости изолировать приложение от инфраструктуры и посторонних вещей.

              0
              Модули самодостаточными… не лазить же МейлерБандлу/Сервису в базу за данными?! Таки передавать на конструктор/метод должен верхний модуль в момент билда экземпляра, как… напрямую или адаптер — не важно.

              Если структура данных часто меняется (>100 миграций за пол года, при релизах в 2 недели)… крути интерфейсы, крути адаптеры — всё равно всю цепочку менять придётся.

              Я к чему… не спорю со сказанным в статье, всё это добавляет надёжности, но и время на поддержку будет отнимать не мало. И тут может возникнуть конфликт с требованиями бизнеса, не редко выбирается динамика, а надёжность потом… если взлетит этот бизнес.

              Или я упускаю чего?
                0
                Модули самодостаточными… не лазить же МейлерБандлу/Сервису в базу за данными?!

                Когда "мэйлер бандлу" (причем тут бандлы к слову?) надо лазать в базу что бы собрать письмо — тут речи не может идти о самодостаточности. Посмотрите код:


                use App\Notificator;
                use App\WelcomeNotification;
                use App\User;
                use App\UserRepository;
                // ...
                
                $userRepository->add($user);
                $notificator->send(new WelcomeNotification($user));

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


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


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


                всё равно всю цепочку менять придётся.

                В простонародье это называется "лазанья код". ну то есть как спагетти, только хуже. Мол мы меняем один слой а "прорезать" приходится все.


                Если у вас возникают такие ситуации — мне было бы интересно услышать конкретные примеры. Разбор таких случаев намного полезнее статей вроде моей.


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

                вот как раз нет. Надежности это не добавляет. А время на поддержку должно сохранять. Если не сохраняет — значит что-то пошло не так и надо эту ситуацию проанализировать и сделать для себя выводы.


                И тут может возникнуть конфликт с требованиями бизнеса, не редко выбирается динамика

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


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

                  0
                  Под «надёжней» имелось ввиду качество кода. А поддержки больше, потому что кода больше (не букав, а объектов и связей).

                  По поводу примера:

                  Представим что мейл был не «приветствие» а оповещение о покупке.

                  До изменений, все продажи происходили по телефону и мейл отсылался продавцом по клику в виде «Спасибо, мы рады вашим $!».

                  И вот прикрутили платёжку, транзакции появились, подтверждения — отмены и т.д. Да и мейл теперь мы отсылать не только html, но и text хотим… При том, тхт вариант может в корне отличатся от html версии.

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

                  namespace App\Controller;
                  
                  use NotificationModule\Service\Notificator;
                  use NotificationModule\Entity\TransactionNotification;
                  
                  use Database\Entity\User;
                  use Database\Entity\Transaction;
                  use Database\Repository\UserRepository;
                  
                  // ...
                  $userRepository->add($user);
                  $notificator->notify(new TransactionNotification($user, $transaction));
                  


                  namespace NotificationModule\Service;
                  
                  use NotificationModule\Entity\PaymentNotificationInterface;
                  
                  function notify(PaymentNotificationInterface $notification): void
                  {
                     $this->getMailerService()
                             ->addHtmlMimeType( (new TransactionMailHtmlView)->render( $notification->getHtmlData() ) )
                             ->addTextlMimeType( (new TransactionMailTextView)->render( $notification->getTextData() ) )
                             ->send(
                                  [
                                       $notification->getSellerEmail(), 
                                       $notification->getClientEmail()
                                  ]
                             );
                  }
                  


                  При этом send() это еще не отсылка по SMTP, а лишь еще один адаптер к stpm, mailgun, slack, jabber и т.д. драйверам.

                  Связь модулей Doctrine->App->NotificationModule и возможно Mailer.

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

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

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


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


                    class HtmlNotificationSender implements Notificator
                    {
                          private $next;
                          private $mailer;
                          public function __construct(Notification $notificator, Mailer $mailer)
                          {
                                $this->next = $notificator
                                $this->mailer = $mailer;
                          }
                          public function send(Notification $notification)
                          {
                               $notification->send($this->mailer); // передаем все что надо сервису что бы он сам себя отправил
                          }
                    }

                    Что-то типа такого. И да, количество изменений теперь будет значительно меньше. И принцип единой ответственности у нас сохраняется (нотификатор — он как хороший менеджер все делигирует, нотификации знают как себя собрать и делигируют отправку мэйлеру).


                    Но то что мы экономим сейчас, кладётся в тех. долг с х2.

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


                    Да и если честно, допустим эти загоны увеличивают время написания кода на 10%. В среднем разработчик работающий над сложной системой именно кодит хорошо если 60% времени. То есть в сумме мы тратим лишних пол часа в день, а команда может экономить час в день. И в итоге выходит профит. Естественно что если профита в действия разработчика нет — то не надо этого делать.


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


                    Связь модулей Doctrine->App->NotificationModule и возможно Mailer.

                    А какой из ваших модулей знает что-то о доктрине? У меня к примеру о доктрине знают только репозитории и больше никто. А репозитории имплементят мои интерфейсы (а не экстендятся от EntityRepository). И приложение от NotificationModule не зависит, NotificationModule содержит адаптер к интерфейсу регламентированному в приложении. И про мэйлер знает только нотификатор (ну и нотификации).

                      +1
                      А поддержки больше, потому что кода больше (не букав, а объектов и связей).


                      Связь связи рознь. Большое количество связей с чётко определённой ответственностью легче в поддержке, чем одна связь «с богом».
                  0
                  Может я вас неправильно понял, но по-моему вы пытаетесь решать организационные проблемы архитектурными методами. Т.е. уже на стадии написания нужно четко понимать что ты пишешь — одноразовый метод, который будет использоваться только в твоей зоне ответственности, или же ты пишешь что-то что может быть использовано как API другими людьми. В последнем случае нужна изоляция и понимание того, что нельзя просто менять интерфейс. И не важно как именно будет реализована заморозка интерфейса модуля — вынесением в интерфейс, пометкой рядом с методом, что его нельзя менять, вынесением метода в API или микросервис. Если не будет этого понимания, то через месяц ответственный за интерфейс все равно найдет способ что-то сломать.

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

                  Вторая проблема — нельзя просто взять и получить то что хочешь из зависимостей, их может быть так много, а обработка так сложна, что придется лепить класс-агрегатор, который бы получал часть источников данных и передавался бы нашему классу. Иначе наш класс раздуется до размеров бога. Агрегаторов может быть несколько, и каждый нужно писать с оглядкой на то, что в один день кто-то глядя в services.yml подумает, что это именно то что ему нужно. Т.е. просто из-за дробления на более мелкие модули мы несем издержки на огораживание или же наоборот одружелюбнивание этих модулей.

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

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


                    В последнем случае нужна изоляция и понимание того, что нельзя просто менять интерфейс.

                    А давайте подумаем, по каким причинам может меняться интерфейс?


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


                    • поменялась инфраструктура, и для выполнения задачи нужно больше данных. Вчера мне предложили пример с сервисом, строящим маршрут по карте. Мол нужно было из пункта A в пункт Б добраться. И мы сделали интерфейс допустим, а сервис, который мы используем, хочет не только это, но еще и какие-то дополнительные данные, предпочтения, стратегии какие-нибудь и тд. Это обычно легко запихнуть в адаптер к зависимости. Просто потому что все эти дополнительные данные — это чисто детали реализации сервиса, который мы используем для построения маршрута.


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

                    Если вы можете предложить еще вариантов, будет интереснее.


                    В честности человека в чатике сильно волновал вопрос, что при таком раскладе интерфейс используемый и реализация могут ооочень сильно различаться. Но в этом собственно и суть, спрятать вообще все что нужно чтобы сделать дела в какой-то объект, переложить на него полностью ответственность за конкретную задачу. Аля мы наняли строителей сделать ремонт, а они начинают нам говорить что-то вроде "а как же так ты не купил мешки для строительного мусора!" А ты такой, я вообще-то вам деньги плачу чтобы не париться об этом.


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

                    писать быстрее, а поддерживать — тяжелее. Написание кода — это незначительная часть расходов на разработку. Поддерживать дробленые архитектуры сложнее только если у нас дробление это произошло как-то неправильно. Мы не пытались скрывать детали, у нас нарушаются принципы вроде single responsibility или interface segregation, мы не пытались делать информационных экспертов, мы забили на закон деметры и вообще о нем не знаем...


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


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


                    Вторая проблема — нельзя просто взять и получить то что хочешь из зависимостей, их может быть так много

                    Это как? не нарушает ли тогда тот, кто использует слишком много зависимостей (30 например) принцип единой ответственности?


                    Агрегаторов может быть несколько, и каждый нужно писать с оглядкой на то, что в один день кто-то глядя в services.yml подумает, что это именно то что ему нужно.

                    А причем тут services.yml? Вы в нем ищите сервисы?


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

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


                    p.s. PerlPower, отвечал вам, промазал веткой.

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

                      Вроде в русском языке это называют связанность и связность или зацепленность и прочность. Английские coupling и cohesion в общем :)
                        0

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

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

                    Самое читаемое