Как стать автором
Обновить

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

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

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

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

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


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


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


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

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


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

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

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

Думаю, продолжение описание остальных принципов в том же духе было бы полезно для сообщества. Я бы ссылки давал джунам или даже не прошедшим собеседование на джуна :)
НЛО прилетело и опубликовало эту надпись здесь
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 в жизни.

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

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

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

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

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

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

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

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

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


use App\Notificator;
use App\WelcomeNotification;
use App\User;
use App\UserRepository;
// ...

$userRepository->add($user);
$notificator->send(new WelcomeNotification($user));

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


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


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


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

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


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


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

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


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

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


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

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

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

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

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

И вот прикрутили платёжку, транзакции появились, подтверждения — отмены и т.д. Да и мейл теперь мы отсылать не только 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.
А поддержки больше, потому что кода больше (не букав, а объектов и связей).

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


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


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 содержит адаптер к интерфейсу регламентированному в приложении. И про мэйлер знает только нотификатор (ну и нотификации).

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


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

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

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

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

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


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

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


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


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


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

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


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


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

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


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


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


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

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


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

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


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

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


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

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

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

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

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