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

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

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

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

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


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


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

Егор такой Егор…


Где-то тут уже писали что в последних редакциях дядя Боб делает уточнение что фокус этого принципа на выделении ролей, которые генерят вам требования. "Зона ответственности" в том плане что вашему модулю соответствует один стэкхолдер. И профит от соблюдения этого принципа заключается в том, что нам намного проще отслеживать конфликты в требованиях разных людей, проще предсказывать изменения модулей (когда уже известен некий набор изменений которые были произведены в прошлом), ну и в целом если мы добавим к этому OCP то проще делать более стабильные модули.


Это не про маленькие классы, как пишет Егор. Нет никаких противоречий аля "является ли багфикс причиной для изменения, а оптимизация? а рефакторинг" как пишет Марко.


По факту этот принцип целиком и полностью завязан на то, как ваши модули представляют интересы людей, для которых вы эти модули пишите. И да, это сложно. Есть книжки на эти темы (типа Object design, roles responsibilities and collaborations) и соблюдать этот принцип можно только если вы учитываете поток изменений требований.


А если потока изменений требований нет — можете обратиться к другому принципу — Информационный эксперт. Он проще и не требует анализа потока изменений. Он в купе с protected variations в целом про information hiding и декомпозицию. Так же по сути как и SRP + OCP.

Информационным Экспертом и пользуюсь, в связке с Low Coupling и High Cohesion. Для меня проще оказалось. Вообще мне Ларман нравится, хороший автор, по его книге GRASP и изучил.

Object design, roles responsibilities and collaborations. Автор Ребекка Вилфс-Брок? Читал эту фундаментальную книгу. 1990 год — фактически время первого прихода ООП к популярности. По-моему в ней на примерах (графический редактор и ATM) объясняется как вообще пользоваться ООП, что это такое и как проектировать объекты (метод последовательного приближения с помощью CRC-карт), про SOLID еще никто тогда не слышал (он не был сформулирован), и у г-жи Вилфс-Брок в чести множественное наследование. Не сказал бы, что эта методология сложна, она годная, но теперь есть UML, паттерны и принципы, все таки много с тех пор прошло времени.
но теперь есть UML, паттерны и принципы, все таки много с тех пор прошло времени.

UML было до, паттерны так же (их назвали в 94-ом году но по сути все тоже самое было и до).


Что до "много с тех пор прошло времени" — классы существуют уже лет 50. Структурный дизайн (который вводит понятия low coupling, high cohesion, information hiding (который по сути open/close или protected variations, авторы сами говорят что это одно и то же). Эти понятия появились в 70-х. Но почему-то не особо люди их понимают. И паттерны и прочие UML только хуже делают как по мне в этом плане.

Пост, в целом, правильный, но Unified Modeling Language то чем вам не угодил? Как в принципе он может «делать хуже»?

class diagram смещает внимание с взаимодействия на структуру.

Так диаграмма взаимодействия в uml тоже есть

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

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

Более чем странный аргумент.
Как выше отметили — диаграмм в UML хватит на все случаи жизни — это очень многогранный язык, позволяющий, при правильном инструментарии и навыках, строить сквозные связанные модели ОТ и ДО. Другое дело, что его надо уметь готовить, но это относится вообще ко всему. А если ничего кроме class diagramm не уметь и не использовать, то конечно да.
Я бы сказал так — задача должна выражаться одним предложением, в котором нет сложноподчинённых частей и частицы «и».

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

три «и» :)
«Проверяет соответствие логина и пароля» — одно «и» — надо разделять

проверяет креденшены — не надо.

По факту проверяет соотвествие пароля логину.
«Поиск в интернете»
Последний пример чересчур упрощен, и скорее заведет в тупик новичка, чем ему поможет, на мой взгляд. У 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;
    }
}

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

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

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

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

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

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

А вообще, Роберт Мартин в свое время очень емко его объяснил (в статье оно есть, только в максимально усложненном варианте, зачем-то)
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
А можете обьяснить, в чём принципиальная разница между Принципом подстановки Барбары Лисков и определением полиморфизма? Я сколько не пытался найти, не выходит.

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


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

Полиморфизм — это про возможность изменения реализации.
Принцип подстановки Барбары Лисков — про неизменность и предсказуемость поведения.


Вот пример использования полиморфизма с нарушением принципа подстановки.


class Rectangle{
    protected $height;
    protected $width;

    public function setWidth($width){
        $this->width = $width;
    }

    public function setHeight($height){
        $this->height = $height;
    }

    public function calculateArea():int{
        return $this->width * $this->height;
    }
}

class Square extends Rectangle{
    public function setWidth($width){
        $this->width = $width;
        $this->height = $width;
    }

    public function setHeight($height){
        $this->setWidth($height);
    }
}

function resizeRectangle(Rectangle &$rectangle, $height, $width){
    $rectangle->setWidth($width);
    $rectangle->setHeight($height);
}

$rectangle = new Rectangle();
$square = new Square();
resizeRectangle($rectangle, 2, 3);
resizeRectangle($square, 2, 3);

echo $rectangle->calculateArea();
echo $square->calculateArea();

вывод:


6
9
А можете обьяснить, в чём принципиальная разница между Принципом подстановки Барбары Лисков и определением полиморфизма?

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

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

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

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

Интерпретация не ошибочная, а не полная. Как и с любым «простым» определением, всегда возникает проблема прикручивания его к частным случаям.

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

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

Ну если опираться на оригинальную формулировку, то этот принцип применим вообще только к классам :)


A class should have only one reason to change.
Robert C. Martin

Общая практика показывает, что обычно его применение рассматривают гораздо шире (что, как мне кажется, правильно).
Опять таки тут речь не про определение, которое действительно для всех уровней будет совпадать — просто заменим class на, допустим, entity — а про интерпретацию.


Если для уровня приложения причина "бизнес процесс изменился таким образом" будет вполне корректной, то для более низких уровней ее придется декомпозировать и, на определенном этапе, менять ее суть. Например: Бизнес причина->Архитектурная причина->Техническая причина->Технологическая причина->etc.
И вот тут главное понять, какого рода причина должна учитываться при проектировании конкретной entity конкретного уровня. Обосновывать код метода бизнес причиной — не всегда удачная идея.

НЛО прилетело и опубликовало эту надпись здесь
Мне порой кажется, что в таких темах ни разу не было реальных примеров, потому что в реальности оказывается, что приложение всем этим принципам не соответствует и из реального кода показывать нечего (из-за Legacy кода, криворукости разработчиков, необходимости сделать вотпрямщас или ещё чего-то).
Так и есть. Мне это напоминает собеседования — спрашивают про принципы, паттерны, лучшие практики. А если устроишься и покопаешься в проектах — так там как раз кривой Legacy код с костылями.
Вот я ровно так же себя ощущаю уже много лет. Где то конечно есть мир идеального кода, но почему же я у заказчиков вижу такую лютую жесть, что аж глаза закрыть охота.

Да тот же взять мейлрушный пример первый. Пусть перепишут его на prepared и еще разок покажут.

И еще: универсальные интерфейсы для работы с бд — миф чистой воды.

ЗЫ мне кажется что в погоней за SOLID, попытке завернуть все в ООП без понимания что это, все забывают про KISS, YAGNI. А это очень и очень плохо.
Это я не совсем правильно понял или принцип «LSP» в данном примере суховат?
Особенно неприятно в 2018 году видеть примеры на php без указания типов (возвращаемых значений).
Человечество не придумало примера для Open-closed Principle без использования геометрических фигур.
Этот какое-то проклятие, такое же, как и в статьях про модульные тесты, где проверяется, что 2+2 равно 4.
Принципы SOLID — это стандарт программирования, который все разработчики должны хорошо понимать, чтобы избегать создания плохой архитектуры.

SOLID — это Design. Мне кажется, архитектура приложения лежит на более высоком уровне.
Design — это «проектирование», проектирование архитектуры — его часть, а не другой уровень.
Приведите пример архитектурного решения для принципа «Dependency Inversion».
Использование единого DI-контенейра для иницализации всех зависимостей классов приложения. Это архитектурное решение.
Это не архитектурное решение
А как вы называете решения, от которых зависит реализация минимум половины кода приложения?
зависит реализация минимум половины кода приложения

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

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

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

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

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

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

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

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

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

Для меня как-то DI-контейнер — это способ реализации Dependency Injection. Он пересекается где-то с Inversion of Control и Dependency Inversion Principe, но не одно и то же.

Dependency Injection — частный случай Inversion of control (на ряду с фабриками, сервис локаторами и т.д.) Причем сам по себе DI никакого отношения к DIP не имеет, так как только за счет использования оного у вас не происходит инверсии направления зависимостей.


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

Не уверен, что это именно частный случай. DI может быть, но при этом без IoC, по-моему. Просто зависимости инжектятся клиентом без всякой инверсии.
Просто зависимости инжектятся клиентом без всякой инверсии.

Инверсия управления, это когда мы разворачиваем поток управления. То есть у нас не A вызывает B а B вызывает A (don't call us, we call you). Будь то фреймворк который дергает ваши контроллеры, листенеры которые дергает некий диспатчер и т.д. Это краиугольный камень построения реюзабельных решений. Думаю проще буде оставить ссылку на первоисточник.


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

Три варианта кода
class AuthManager 
{
  public function checkCreds(LoginPasswordCreds $creds) : bool
  {
    $repo = new UserRepository();
    $user = $repo->get($creds->login);
    return \password_verify($creds->password, $user->passwordHash)
  }
}

$creds = LoginPasswordCreds::fromGlobals();
$manager = new AuthManager();
if (!$manager->checkCreds($creds)) {
  exit("Auth required");
}
  // main logic

class AuthManager 
{
  public function __construct(UserRepository $repo)
  {
    $this->repo = $repo;
  }
  public function checkCreds(LoginPasswordCreds $creds) : bool
  {
    $user = $this->repo->get($creds->login);
    return \password_verify($creds->password, $user->passwordHash)
  }
}

$creds = LoginPasswordCreds::fromGlobals();
$repo = new UserRepository();
$manager = new AuthManager($repo);
if (!$manager->checkCreds($creds)) {
  exit("Auth required");
}
  // main logic

class AuthManager 
{
  public function __construct(UserRepository $repo)
  {
    $this->repo = $repo;
  }
  public function checkCreds(LoginPasswordCreds $creds) : bool
  {
    $user = $this->repo->get($creds->login);
    return \password_verify($creds->password, $user->passwordHash)
  }
}
$container = new Container();
$container->register(__DIR_ . '/config/container/*');
$creds = $container->get(LoginPasswordCreds::class);
$manager = $container->get(AuthManager::class);
if (!$manager->checkCreds($creds)) {
  exit("Auth required");
}
  // main logic

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

то второй, по-моему, хороший пример DI без IoC.

а где тут DI? Я не наблюдаю его ни во втором ни в третьем примере. В третьем примере могу говорить про сервис локатор, а во втором у вас нет ни DI ни IoC потому что клиентский код собирает свои зависимости.


То что вы композицию агрегацией заменили не говорит о DI.

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

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

И это демонстрация принципа инверсии управления) В обоих случаях для AuthManager мы имеет эту самую инверсию. Во втором варианте за счет фабрики (клиентский код выступает в этой роли) а во втором случае за счет контейнера.

Я всё же отношу второй пример к коду, который демонстрирует исключительно DI — там я не вижу никакой инверсии управления, никакой «динамической линковки», всё «статически линкуется», всё захардкожено, изменение любой сигнатуры требует изменение кода клиента. В третьем, да, уже есть признаки IoC — конструктор является абстрактной точкой расширения потока исполнения, клиент о его сигнатуре ничего не знает, в какой момент происходит его вызов тоже не знает.
изменение кода клиента

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


А с точки зрения "клиентского кода", того что вы указываете, я не вижу ни применения DI ни применения Inversion of control. В третьем варианте можно принятуть IoC за счет использование сервис локатора (не DI, прошу заметить).

Попробую чуть перефразировать....


в ваших примерах можно выделить 2 уровня. Уровень AuthManager и уровень приложения.


В первом примере у нас композиция, нет ни DI ни IoC. С этим мы вроде оба согласны.


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


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


p.s. вообще я нахожу этот спор бессмысленным. Во всей литературе DI это реализация IoC. Они не разделимы. Ну и управление зависимостями это капля в море по сравнению со всем, что дает нам IoC.

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

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

В третьем же варанте появляется магия, без изучения кода обощенного контейнера нельзя сказать в какой момент вызовется код конструктора менеджера, а что перед этим вызовется код конструктора репозитория вообще не очевидно, и даже какой конкретно будет класс нельзя сказать, если UserRepository не final, а имеет нескольких дочерних классов (тут уже DIP попахивает :) )

я не нахожу практической пользы в такой трактовке.

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

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

Просто зависимости инжектятся клиентом без всякой инверсии.

Если зависимости инжектятся клиентом — это и есть dependency inversion :)

В dependency inversion основное — зависимость от абстракций, тут сознательно абстракции типа UserRepositoryInterface или AbstractRepository нет.
В dependency inversion основное — зависимость от абстракций

Это способ реализации конкретно в ООП-языках, а не определение. В общем же в dependency inversion основное — перенос ответственности за конфигурацию сервиса из самого сервиса в клиента.

Вы точно про dependency inversion principle, а не про dependency injection pattern?

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

final class MySqlUserRepository {}

final class MySqlUserService
{
  /** @var MySqlUserRepository */
  private $repo;

  public function __constructor(MySqlUserRepository $repo)
  {
    $this->repo = $repo;
  }
}

никаких интерфейсов или иных абстракций, всё конкретное и финальное, класс MySqlUserService зависит от конкретного класса MySqlUserRepository, только не создаёт их сам, а клиентский код их инжектирует.

только не создаёт их сам, а клиентский код их инжектирует.

Вы опять про "создает". Не важно, кто создает, создавать может кто угодно. Важно, кто определяет эти зависимости. В вашем случае определяет их сервис — они прошиты намертво. По-этому никакого dependency injection тут нет. Тот факт, что репозиторий приходит в конструктор, а не создается там же, ничего не меняет абсолютно.

Вы каким определением пользуетесь?


dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state.[1] Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

сильно от него отличается?

Им и пользуюсь. Обратите внимание — там ничего не сказано про то, кто создает зависимость. Вполне возможно, ее создает сам сервис, но по информации клиента (Passing the service to the client, rather than allowing a client to build or find the service). Это тоже будет DI, в согласии с определением.

Тут не очень привычное определение клиента и сервиса. Сервис тут — это зависимость, а клиент — зависящий. Обычно имеют в виду тройку клиент (в этом определении вообще его нет), сервис (в определении — клиент), зависимость (в определении — сервис). И явно сказано, что основное требование паттерна — это передача готовой зависимости (в определение — сервиса), а не позволение сервису (в определении — клиенту) создавать или находить его.
> И явно сказано, что основное требование паттерна — это передача готовой зависимости (в определение — сервиса), а не позволение сервису (в определении — клиенту) создавать или находить его.

Вы неверно понимаете смысл «build». «build» здесь именно «несет ответственность», потому что в каком-либо другом смысле нельзя получить корректное определение. Сам процесс создания зависимости просто нельзя отнести к какой-то конкретной сущности, ну, то есть, нет никакого способа определить, кто именно «build», если под «build» понимать конструирование.
Вы неверно понимаете «Passing the service to the client» — передаётся уже готовый объект, не какая-то информация, по которой клиент может как-то получить свою зависимость, а готовый конкретный объект зависмости, готовый к использованию (is an object that can be used )
Вы неверно понимаете «Passing the service to the client» — передаётся уже готовый объект, не какая-то информация, по которой клиент может как-то получить свою зависимость

Я говорю, что в данном контексте нету разницы между готовым объектом и информацией, которая достаточна для того, чтобы данный объект сконструировать :)
На самом деле ведь в любом случае передается именно информация, то есть "уже сконструированный объект" — это тоже информация. При чем информация та же самая, что и в случае "информация о конструировании". Это одна и та же информация, которая просто представлена по-разному. Так вот, мой тезис в том, что нам важен именно характер передаваемой информации, но не способ ее представления. Вы можете передать инфу о конструировании, может передать объект в представлении конкретного рантайма (ну, как объект класса в данном языке, с-но), можете передать объект из-под какого-то другого рантайма, например через ffi с оберткой, может передать объект в сериализованном в какой-то бинарный формат виде, можете в виде конфига в той или иной форме. Это абсолютно все неважно.

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

Так вы всегда передаете информацию об объекте. Указатель на него, например. Либо бинарное представление в памяти (если по значению структуру, допустим, передали). И это будет ответственность сервиса правильно проинтерпритировать этот указатель (или данные в памяти) и использовать его корректным образом, как объект.


Передача какой-то другой информации вместо конкретной зависимости

Еще раз, информация во всех случаях одна и та же. Разница в ее представлении. Это как передать один и тот же текст в разной кодировке. Байты разные — текст один и тот же. Почему при передаче текста в одной кодировке, по-вашему, ДИ есть, а в другой кодировке — уже нет? Я за то, чтобы смотреть не на кодировку текста, а на его содержание. Если текст, который вы передали, содержит описание зависимости (в виде указателя, бинарных данных, конфига, не важно) — вы передали зависимость. Если не содержит — не передали.


Просто в вашем понимании паттерн становится бессмысленным абсолютно.

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

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

Как и со строками. Если объекту нужна зависимость в виде utf8 строки, то передача utf8 строки — это DI, а передача строки в какой-то другой кодировке, которую объект, должен будет конвертировать в utf8 — это почти DI, но не совсем.
> Смысл DI в избавлении объекта от лишних ответственности и знаний.

Именно. По-этому не важно, как вы кодируете данные. Данные либо содержат все требуемое для конфигурирования (и тогда объект не несет ответственности) либо не содержат (и тогда объект несет ответственность). Заметьте, кодировка данных совершенно не важна.

> если он может эту отвественность и эти знания переложить на клиента?

Ну он не может. Если он не будет знать как интерпретировать полученные данные, то он не сможет их использовать.

> Согласиться с вами в случае передачи объекта, если ссылка или указатель на объект передаётся в разных формах, позволяющих сервису получить доступ к объекту.

Ну вот я передал конфиг в виде жсона, который позволяет получить доступ к объекту. Это DI?

> это почти DI, но не совсем

А почему, собственно? Откуда это условие? Оно выглядит совершенно надуманным и противоречит самой концепции (то, что сервис не отвечает за свои зависимости, а перекладывает ответственность на клиента). Если клиент сделал всю работу по конфигурированию и передал конфигурацию, то формат этой конфигурации какое значение имеет? Вы можете передать в конструктор конкретные объекты в виде зависимостей. Можете передать имена классов. Можете передать ссылку на IoC контейнер, который обратится к конфигу, содержающую эту информацию. Можете передать набор классов для фабрик а параметры к этим фабрикам. Можете передать жсон с описание фабрик и параметров. Какая разница между всеми этими вариантами? Во всех случаях ответственность за выбор конфигурации лежит на клиенте. С-но, некоторые DI фреймворки организованы именно в виде описанных выше схем. И остаются DI.
По-этому не важно, как вы кодируете данные. Данные либо содержат все требуемое для конфигурирования (и тогда объект не несет ответственности) либо не содержат (и тогда объект несет ответственность). Заметьте, кодировка данных совершенно не важна.

Важно. Во-первых, если данные несут всё необходимое, но конфигурирование не выполнено, да и собственно объект для конфигурации не создан, то на сервисе лежит отвественность за создание и конфигурацию, даже если он ничего не решает, а передаёт, например, строку для десериализации в функцию десераилзации объектов как есть. Клиент не выполнил всю работу по конфигурацию, он только собрал и передал необходимую информацию. А наш сервис должен знать как её с помощью собрать и скофигурировать объект, ну и собствнно должен собрать и сконфигурировать. Во-вторых, если сервису нужно что-то декодировать или интерпретировать, то это тоже дополнительная отвественность, не говоря о возможных дополнительных зависмостях типа json-декодера. Его тоже в закодированном виде передавать? ) Получая ссылку на объект стандартными средствами рантайма языка, сервис ничего не декодирует, а просто в нужный момент стандартными средствами языка дергает его метод — кодированием/декодированием занимается рантайм, если ему вообще нужно этим заниматься.


Кстати, а кроме создания объекта обычно его и уничтожать надо. Кто у вас отвественен за уничтожение зависомсти? Тоже сервис?


Ну он не может. Если он не будет знать как интерпретировать полученные данные, то он не сможет их использовать.

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


Ну вот я передал конфиг в виде жсона, который позволяет получить доступ к объекту. Это DI?

Может в каких-то языках да, если этот конфиг позволяет сохранять ссылочную эквивалентность. Наверное, в C++ можно преобразовать указатель в строку типа hex, потом завернуть её в json, потом в сервисе произвести обратное преобразование и получить указатель на объект, который создал и сконфигурировал клиент. Уродливо, но DI, да.


Если клиент сделал всю работу по конфигурированию и передал конфигурацию, то формат этой конфигурации какое значение имеет?

Подготовка информации для конфигурирования и само конфигурирование — это разные процессы. Вот я вам напишу "добавьте в /etc/hosts строку 127.0.0.16 example.com" — это я подготовил информацию и передал вам. А вот когда вы её введёте, то и выпоните конфигурирование.


Можете передать ссылку на IoC контейнер, который обратится к конфигу, содержающую эту информацию.

Это уже не DI, если работа с контейнером не является основной отвествнностью сервиса.


Во всех случаях ответственность за выбор конфигурации лежит на клиенте.

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

> Важно. Во-первых, если данные несут всё необходимое, но конфигурирование не выполнено, да и собственно объект для конфигурации не создан, то на сервисе лежит отвественность за создание и конфигурацию

А это уже неизвестно. Мы можем определить, кто несет ответственность за принятие решения о конфигурации, но нету никакого способа определить, кто конкретно занимается конструированием объекта. Очевидно, что в определении DI (да и вообще в каком-либо определении) не может использоваться факторов, которые принципиально нельзя как-то определить (ну, если мы хотим, чтобы определение было хоть сколько-нибудь корректно).

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

А вот этого мы уже не знаем и никак не узнаем. Может, выполнил, а может — не выполнил.

> Во-вторых, если сервису нужно что-то декодировать или интерпретировать, то это тоже дополнительная отвественность

Но сервис _в любом случае_ эту ответственность несет. Не может не нести.

> А ему и не надо ни интерпретировать их, ни вообще получать.

Тогда он не сможет эту зависимость использовать. С-но, ее можно из сервиса невозбранно удалить.

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

А какая разница, в каком языке?

> «добавьте в /etc/hosts строку 127.0.0.16 example.com» — это я подготовил информацию и передал вам.
> А вот когда вы её введёте, то и выпоните конфигурирование.

Но, погодите, если вы делаете DI по полю класса, то именно это и происходит. Вы выдаете строку «положи в поле Х объект Y», а что с этой строкой делать — уже решает сам сервис. Он может ее, например, проигнорировать :)

> Это уже не DI

Конечно же DI, почему нет?
Забавно вы выбираете цитаты :)
> Во-вторых, если сервису нужно что-то декодировать или интерпретировать, то это тоже дополнительная отвественность

Но сервис _в любом случае_ эту ответственность несет. Не может не нести.

А ответ на самую интересную часть так и не дали:
не говоря о возможных дополнительных зависмостях типа json-декодера.

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

Так же, как и все остальные, в сущности. Разве тут какой-то особенный кейс?
> но нету никакого способа определить, кто конкретно занимается конструированием объекта

Интересует только занимается этим сервис, которому зависимость нужна или нет.

> А вот этого мы уже не знаем и никак не узнаем. Может, выполнил, а может — не выполнил.

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

> Тогда он не сможет эту зависимость использовать. С-но, ее можно из сервиса невозбранно удалить.

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

> А какая разница, в каком языке?

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

> Но, погодите, если вы делаете DI по полю класса, то именно это и происходит. Вы выдаете строку «положи в поле Х объект Y»

Я говорю «ты просил третьим параметром конструктора передать объект Y — вот, передаю». Про поля я ничего не знаю, очень часто они приватные.
Интересует только занимается этим сервис, которому зависимость нужна или нет.

Ну так вы же не можете это определить.


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

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


Сервису не нужна информация о конфигурировании своей зависимости, ему нужна информация о том как её использовать по назначению.

Ну, так я об этом и говорю.


Большая. Многие языки не имеют возможностей как-то создавать ссылки на объекты, кроме её получения при создании объекта.

Ну так какая разница? Очевидно же, что язык на наличие DI влиять не может. Если у вас есть DI в каком-то коде, то любой эквивалентный код в любом другом (или том же языке), тоже будет DI. Верно и обратное — если DI нет, то и после переписывания эквивалентным образом на тот же/другой язык оно не появится.


Я говорю «ты просил третьим параметром конструктора передать объект Y — вот, передаю».

Это вы говорите о DI при помощи конструктора, а если у вас DI по полю, то зависимость инжектится в поле. Или это DI у вас уже не DI, хоть и относится к одной из стандартных реализаций?


Но даже в случае передачи по конструктору, вы говорите: "положи на стек объект Х" при вызове (или что там вместо стека в вашем ЯП). А что там делать с этим объектом на стеке уже решает сам конструктор. Сама по себе, без работы на стороне сервиса, зависимость не заинжектится же. Всегда нужны какие-то дополнительные усилия. Иначе просто ваш параметр будет проигнорирован и все.

> Ну так вы же не можете это определить.

Как автор сервиса или требований к нему я как раз могу. Паттерн DI он на уровне сервиса применяется, что клиенту сервиса нужно под него подстраваться — лишь следствие применения. Как если я применил для класса синглтон, то клиенты вынуждены получать объекты только дозволенным мною способом, каким-нибудь getInstance, клиент даже может не знать синглтон это или фабричный метод.

> Ну так у нас же нет способа узнать, может он или нет, верно?

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

> Ну, так я об этом и говорю.

Как-то вы странно об этом говорите. Я о том, что например сервису нужен только пару методов его зависимоти типа save и load, ему не нужны, например, сигнатура конструктора или сеттеры.

> Если у вас есть DI в каком-то коде, то любой эквивалентный код в любом другом (или том же языке), тоже будет DI.

В большинстве более-менее известных мне языков есть только один (пускай с нюансами) способ передать объект в сервис — передать ссылку на него. В C++ есть (если не путаю) другой — передать значение указателя в какой-то форме и по нему получить доступ к объекту практически эквивлентный основному. Передача данных для конструирования или ещё какого восстановления объекта в остальных этих языках не эквивалента передаче объекта поскольку не обеспечивает сохранения идентичности. Грубо, каждый раз в памяти будут создаваться новые объекты зависимостей не идентичные друг другу. Так что это не эквивалентный код.

> Это вы говорите о DI при помощи конструктора, а если у вас DI по полю, то зависимость инжектится в поле. Или это DI у вас уже не DI, хоть и относится к одной из стандартных реализаций?

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

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

Конечно, и наше определение об этом говорит: The service is made part of the client's state. (помня о маппинге терминов)
> Как автор сервиса или требований к нему я как раз могу.

Как?

> Если мы этого кода конфигурирования не написали в сервисе, значит сервис не конфигурирует.

Не факт. может, конфигурирует, а может — нет.

> Я о том, что например сервису нужен только пару методов его зависимоти типа save и load, ему не нужны, например, сигнатура конструктора или сеттеры.

Так и я о том же :)

> Передача данных для конструирования или ещё какого восстановления объекта в остальных этих языках не эквивалента передаче объекта поскольку не обеспечивает сохранения идентичности.

Но это частный случай. Что в случае, если идентичность сохраняется или не требуется? Очевидно, что DI не может зависеть от того, требуется вам идентичность при передаче или нет.

> DI, конечно, просто контракт сервиса другой

И вот тут вы с формальных признаков (где что находится в коде) перешли на «какой в голове у программиста контракт». Как вы это будете определять? В таком определении DI зависит от мнения программиста и один и тот же код — одновременно и DI и не DI?

> Конечно, и наше определение об этом говорит: The service is made part of the client's state.

Ну так если сервис в любом случае выполняет задачи по конфигурированию, то как вы отличаете, объективно, в каких случаях этих задач достаточно, чтобы DI перестало быть DI, а в каких — нет? :)

> Она есть потому что как автору сервиса мне не нужно писать код построения или поиска нужных мне зависимостей.

Как не нужно? Нужно в любом случае. Вам передали объект в конструктор, вы его положили в какое-то поле, вот этот код (кладете параметр конструктора в поле) — это и есть уже «поиск и построение». И ваш сервис принял решение — положить этот объект вот в это поле. Мог проигнорировать, мог положить в другое. Мог положить — и сделать какие-то дополнительные манипуляции — например, кеш или буфер какой-то выделил для работы с этой зависимостью (это ж у вас клиент не будет делать, правда?), мог проверить доступность (вдруг эта зависимость предоставляет интерфейс к какому-то удаленному ресурсу, например?), провел авторизацию (через какую-то другую зависимость, допустим), подтянул какие-то дополнительные данные, требующиеся для работы (например какие-то метаданные, предполагаемые протоколом работы). В какой из описанных моментов DI перестало быть DI?
это и есть уже «поиск и построение»

Инициализация — да, поиск и построение — нет.


И ваш сервис принял решение — положить этот объект вот в это поле.

Это не "принял решение" а просто сохранил ссылку. Предположим что у нас есть возможность делать так:


public function __construct(
    private Dependency $dep
) {}

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


Мог проигнорировать

в этом случае ему не нужна зависимость, и тогда вопрос — зачем оно объявлено как оное?


Суть в том что сервис ваш объявляет лишь какие зависимости ему нужны, что он с ними делать будет это уже его забота.


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


В этом собственно суть инверсии управления.


В какой из описанных моментов DI перестало быть DI?

DI это про то что зависимости передаются извне. Все. Ваш код не должен знать кто и как их вам предоставляет. Точно так же как тот кто передает зависимости не должен знать как эти зависимости будут использованы.


К чему вообще спор и эти пустые философства?

Инициализация — да, поиск и построение — нет.

А как вы их отличаете?


Это не "принял решение" а просто сохранил ссылку.

Так мог и не сохранять. Или сохранить не ссылку. Или еще что :)
Так что именно "принял решение"


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

А если у меня будет прикрыто сахаром, как по конфигурационной строке строится зависсимость, то это снова DI? а если без сахара — то не DI? :)


в этом случае ему не нужна зависимость, и тогда вопрос — зачем оно объявлено как оное?

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


В этом собственно суть инверсии управления.

Так я об этом и говорю. Зависимости сервиса конфигурируются извне (то есть в сервис передается полная информация о конфигурации зависимостей). Что он там потом с этой информацией делает — не важно.


DI это про то что зависимости передаются извне. Все. Ваш код не должен знать кто и как их вам предоставляет.

Ну вот я передал в конструктор строкой имя конкретного класса в качестве зависимости. Код ничего не знает о том, кто и как ему эту зависимость предоставил, все. Он ее просто берет и использует. Это же DI? DI.

> Ну вот я передал в конструктор строкой имя конкретного класса в качестве зависимости. Код ничего не знает о том, кто и как ему эту зависимость предоставил, все. Он ее просто берет и использует. Это же DI? DI.

DI это будет, если сервис не попытается создать или найти объект этого класса, чтобы его в дальнейшем использовать самому. Вот прямо сейчас пишу такой сервис — вполне DI. А если буду создавать объект для своего использования — не DI. А если буду создавать, чтобы вернуть клиенту — DI. Полчаса закончил такой писать.
> А если буду создавать объект для своего использования — не DI

Ну а как узнать-то, создает он или нет?

> А если буду создавать, чтобы вернуть клиенту — DI.

А как вы определите это? В исходниках программы информации о том, кто создает объект, нет. Ничего кроме исходников программы у вас, очевидно, тоже нет.

> Вспомнить, писал я код конфигурации или нет.

Погодите, мы же не про код конфигурации. Мы про создание объекта, разве нет? То, что в DI за конфигурацию сервис не отвечает мы, вроде, оба согласны.

> Как он может конфигурировать, если ни строчки кода про конфигурацию нет?

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

> Как о том же, если предлагаете в сервис передавать, например, имя каталога для сохранения, создавать объект доступа к ФС и конфигурировать его, чтобы он по этому пути сохранял?

> Как о том же, если предлагаете в сервис передавать, например, имя каталога для сохранения, создавать объект доступа к ФС и конфигурировать его, чтобы он по этому пути сохранял?

А имя каталога для сохранения — _это достаточная_ информация для работы сервиса-сохраняльщика? Если достаточная и никакого больше конфигурирования не требуется — то это ДИ, конечно. Но в реальности она достаточна не будет, как мы понимаем.

> Очевидно, что зависит. Определение явно говорит, что передаётся объект

То есть если сервис требует не синглтон, то это уже не ДИ, а если синлготон — то ДИ? Вот так новости!

> Частный случай — это C++, в котором можно передать указатель или ссылку на объект не используя явно эти типы данных

А так же c#, java, javascript, pytho и т.д. и т.д. и т.д., сколько частных случаев развелось :)

> но это должна быть ссылка на тот же объект, который сконфигурирован клиентом

У вас язык вообще может не поддерживать понятие идентичности :) Например, какой-нибудь условный pure language просто вам не позволит отличить два структурно одинаковых объекта :)

> Если оно внутри сервиса (определяется просмотром кода сервиса) — то это не DI.

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

> Это не поиск и построение, это сохранение построенного и, возможно, найденного где-то во внешнем мире и переданного из него черз параметр конструктора, сеттер или свойство.

Ну так и лукап в хеше — это не поиск и построение тогда, нет? Разнница-то в чем?

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

> Сервис может выделять в своём стейте кеш для хранения результатов работы зависимости

Ну я об этом и говорю, он не зависимости говорит: «эй закешируй свои вызовы», а сам смотрит: «ага, у нас тут такой-то сервис, создадим-ка кешик для результатов его работы». Сама зависимость, конечно, не в курсе об этих кешах. Так чем создание такого кеша отличается от любой другой обработки зависимости? Почему можно принять в качестве зависимости некоторый объект и создать для работы с ним другой объект только в том случае, если второй объект — это кеш? Или еще какие-то случаи есть? Когда можно а когда — нет?

> В целом уже попахивает нарушением DI — зачем это делать нашему сервису, если это должен сделать клиент?

Ну, допустим, пользователь вообще о ней не знает, то есть это именно авторизация _сервиса_ на каком-то удаленном ресурсе, не клиента.

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

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

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

Ну вот я передал в сервис ссылку на зависимость (не саму зависимость, ссылку на нее), сервис при попытке обратиться к зависимости теперь выполняет вызовы на основе этой ссылки по определенному протоколу. Почему это не DI?
А как вы их отличаете?

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


Так что именно "принял решение"

принял решение в этом ключе — это если там есть if statement. В любом случае это логика инициализации. Никакого отношения к "построению дерева зависимостей" это не имеет.


то это снова DI? а если без сахара — то не DI? :)

А как это вообще относяится к вопросу? В прочем что есть DI я уже говорил.


обработки, которую делает сервис.

если у вас у сервиса есть некая логика в конструкторе — скорее всего вам надо делать отдельную фабрику.


Чем принятие решения по игнорированию или нет отличается от принятия решений по каким-то другим действиям, связанным с работой с зависимостью?

Ничем. И все это никакого отношения к DI не имеет.


Ну вот я передал в конструктор строкой имя конкретного класса в качестве зависимости.

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


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

> легко, когда я сэчу то что мне пришло в пропертю я ничего не ищу и ничего не строю, я инициализирую инстанс.

Ну окей. Но когда вы будете вызывать метод класса, то искать будете. То есть в итоге вы от этого никуда не денетесь. Можно так же тогда строку с именем класса передать и не сразу найти указатель на объект, а каждый раз искать его.

> принял решение в этом ключе — это если там есть if statement.

Принял решение _в любом_ ключе — это if statement или несколько.

> А как это вообще относяится к вопросу?

Ну совершенно напрямую. Вы, фактически, утверждаете, что «ДИ или не ДИ» зависит не от семантики кода, а от наличия сахара. Разве не так? Это же глупость.

> если у вас у сервиса есть некая логика в конструкторе — скорее всего вам надо делать отдельную фабрику.

Если я в конструктор передал лямбду, которая всю логику содержит, а в самом конструскторе эту лямбду просто вызвал — это ДИ или не ДИ?

>Ничем. И все это никакого отношения к DI не имеет.

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

> Да, ваш класс ничего не знает о том откуда зависимость и что там, главное что бы соблюдался контракт.

То есть передачу по имени вы, в принципе, допускаете как ДИ, хоть и «не совсем ДИ» (кстати, почему?). Просто давайте определимся, потому что вот VolCh говорит, что это не ДИ. Ну просто, чтобы понимать, кто какую позицию отставивает. Я свое понимание указал — ДИ — это когда вы в сервис передаете достаточную информацию для конфигурирования сервиса. Не важно в каком виде — строка с именем класса, указатель на объект, джсон-конфиг, это несущественно. Если же информации не достаточно — то это нарушение ДИ.

Как вы определяете?

> Хотя если он потом инстанцирует зависимость и т.д. это уже не совсем DI

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

Фраза «зависимости сервиса конфигурируется извне» означает, что сервис получает уже сконфигурированную зависимость к моменту когда собирается её использовать, ничего для этого не предпринимая. Если он получает конфигурационную информацию извне и сам на её основе что-то делает, чтобы получить готовую к употреблению зависимость, то он сам и занимается конфигурацией зависимости, и в контексте DI не важно получает конфигурационную информацию извне через какие-то виды инъекций, захардкожена она в нём или читает из конфига — отвественность за конфигурирование (не путать с отвественностью за выбор значений конфигурационных параметров) лежит на нём, а значит это не DI по определению. Какой -то dependency config injection, а не dependency injection если, например, сервис запросов к базе данных получает конфигурацию соединения с базой данных и сам соединение устанавливает, чтобы отправлять запросы. При DI он получит готовый объект (вариант — ресурс, дескриптор) соединения, а не информацию о том как его самоу создать или найти, а потом сконфигурировать.
Если он получает конфигурационную информацию извне и сам на её основе что-то делает, чтобы получить готовую к употреблению зависимость, то он сам и занимается конфигурацией зависимости

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


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

Именно информацию о том, как этот объект найти (адрес в памяти) он и получает.

Не существует никакого способа передать саму зависимость

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


function __construct(Foo $foo) {

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


Наш объект не зависит от жизненного цикла своей зависимости, и ничего об этом не знает. Так же он не знает ничего о том, какие зависимости у реализации Foo, как оно инстанцируется, настраивается и т.д. Мы четко разделили ответственность.


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


Именно информацию о том, как этот объект найти (адрес в памяти) он и получает.

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

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

Объект при вызове конструктора из вашего примера выше:


А если мы передаем имя класса и аргументы

тоже собирает рантайм. И что? Кроме того, если вы говорите:


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

И это хорошо, то почему все сразу становится плохо, когда "вот короч хочу штуку с типом Foo и аргументом Bar"? А если я сделаю отдельные классы для разных вариантов аргумента и скажу "вот короч хочу штуку с типом FooBar" то все опять станет хорошо?


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


Если вы передали в модуль абстрактную фабрику по интерфейсу и он при помощи этой фабрики сделал объект — это DI или нет? если вы передали в модуль лямбду, создающую объект и модуль получил объект, дернув лямбду — это DI или нет? Если лямбда передана текстом и заевалена (например, как в js можно) — это DI или нет? Если лямбда передана текстом на каком-то ЯП, который не является хостом и заевалена модулем интерпретации — это DI или нет? Чем последний случай отличается от передачи имени класса и получения конкретного объекта класса от сервис-локатора? (тогда имя класса — лямбда на некотором не-хост ЯП, а сервис-локатор — интерпретатор этого ЯП)

Не за выбор свойств зависимостей, а за выбор зависимостей. Не Dependency Properties (или State) Injection, а Dependency Injection.

Ребёнок говорит маме: «хочу есть», она ему «на кашу ешь» — это DI. А если она ему «свари манную кашу, только смотри чтоб не пригорела» то это не DI. Во обоих случаях мама решает, что ребёнок будет есть (допустим он абсолютно послушный), но в первом она с него снимает ответственность за готовку, а во втором у него кроме ответственности есть появляется и отвеиственность эту еду приготовить.
тоже собирает рантайм. И что? Кроме того, если вы говорите:

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


Не важно, кто потом этот конкретный Foo конструирует

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


Если вы передали в модуль абстрактную фабрику по интерфейсу и он при помощи этой фабрики сделал объект — это DI или нет?

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


У DI есть вполне четкие критерии — модуль клиент не должен ВООБЩЕ ничего предпринимать для того, что бы работать с зависимостью.


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


И вы можете думать что хотите, я не вижу смысла "размазывать" определения. Иначе они теряют смысл.

> Как?

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

> Не факт. может, конфигурирует, а может — нет.

Как он может конфигурировать, если ни строчки кода про конфигурацию нет?

> Так и я о том же :)

Как о том же, если предлагаете в сервис передавать, например, имя каталога для сохранения, создавать объект доступа к ФС и конфигурировать его, чтобы он по этому пути сохранял?

> Но это частный случай. Что в случае, если идентичность сохраняется или не требуется? Очевидно, что DI не может зависеть от того, требуется вам идентичность при передаче или нет.

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

Очевидно, что зависит. Определение явно говорит, что передаётся объект, а не информация для его конструирования или поиска в каком-то реестре или ещё где. Можно, если язык допускает, каким-то необычным способом передать ссылку или указатель, но это должна быть ссылка на тот же объект, который сконфигурирован клиентом. В частности зависмости могут быть стейтфул и клиент рассчитывает, что состояние сохраняется и передаётся между сервисами. А может не рассчитывает, но зависмость рассчитывает. В общем без идентичности, без передачи объекта по ссылке — это уже не DI в общем случае. В каких-то случаях можно передать по значению (в любом виде) без изменения функциональности, но именно это частные случаи.

> И вот тут вы с формальных признаков (где что находится в коде) перешли на «какой в голове у программиста контракт». Как вы это будете определять? В таком определении DI зависит от мнения программиста и один и тот же код — одновременно и DI и не DI?

Я говорил не про место передачи, а про место конструирования и конфигурирования зависимостей сервисов. Если оно внутри сервиса (определяется просмотром кода сервиса) — то это не DI. Разработчик клиента может не изучать контракт сервиса относительно передачи зависимости, но тогда поведение будет для него неожиданным, если вообще программа скомпилируется или запустится.

> Ну так если сервис в любом случае выполняет задачи по конфигурированию

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

> это и есть уже «поиск и построение»

Это не поиск и построение, это сохранение построенного и, возможно, найденного где-то во внешнем мире и переданного из него черз параметр конструктора, сеттер или свойство.

> кеш или буфер какой-то выделил для работы с этой зависимостью (это ж у вас клиент не будет делать, правда?),

Сервис может выделять в своём стейте кеш для хранения результатов работы зависимости, но не должен выделять кеш для сосбственно зависимости, грубо не должен дергать для неё метода `$this->dependency->setChache($this->dependencyCache);`. Если зависимость предполагает такое, то кеш уже должен быть настроен, к моменту когда сервис узнал о зависимости. Клиент или DI-контейнер решает должен ли быть кеш и какого размера.

> мог проверить доступность

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

> провел авторизацию (через какую-то другую зависимость, допустим)

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

> подтянул какие-то дополнительные данные, требующиеся для работы (например какие-то метаданные, предполагаемые протоколом работы)

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

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

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

нет, не должны.

Тогда не сможете заинжектить.

И что же мне помешает?


1) Можно указать просто класс, а не интерфейс. И инъекция таки возможно по имени класса.


2) Контейнер Laravel, например вообще вот такое умеет:


$value = new SomeDepClass;

$this->app->when(Some::class)
          ->needs('$someDependency')
          ->give($value);

contextual binding называется.
Не зря же нам пых Reflection API предоставляет.

И что же мне помешает?

Тайпчекер.


2) Контейнер Laravel, например вообще вот такое умеет:

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

Делаете "абстракцию" final и будет вам зависимость от "реализации"

Делаете "абстракцию" final и будет вам зависимость от "реализации"

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

PHP — динамический, но единого базового класса нет :) final class SomeClass {} не имеет предков и не может быть расширен, чистая реализация без абстракций.


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

> PHP — динамический, но единого базового класса нет :)

Если язык динамический, то есть базовый тип any. Вот

> final class SomeClass {} не имеет предков и не может быть расширен, чистая реализация без абстракций.

Вы же можете в переменную с типом SomeClass засунуть любую хрень, так? То есть _по факту_ тип этой переменной any. А если вы можете туда засунуть только SomeClass и ничего более (ну пусть рантайм проверяет), то вы не можете выбрать альтернативную реализацию, то есть она намертво прошита.

> Главное, он получает готовую зависимость от клиента, а не конструирует или ищет её сам.

Ну так еще раз, DI не про то, кто и что конструирует. Вы подходите к терминологии чисто формально, типа «если есть строчка кода вида Х в месте Y — это DI, иначе — не DI», но надо смотреть на смысл этой строчки, что она делает. Представьте, что я в конструктор класса Х передаю лямбду, которую Х вызывает и которая, с-но, конструирует внутренности Х. Формально, получается, никакого DI нет, ведь вся конструкция выполняется внутри конструктора. Класс сам себя конструирует, сам все зависимости инициализирует. Но по факту DI есть, потому что вся ответственность за инициализацию лежит не на сервисе а на клиенте, и разница между «передать лямбду, которая будучи запущена внутри сервиса настроит зависимости» и «вызвать эту лямбду в самом клиенте» — вобщем-то отсутствует, это с точки зрения семантики совершенно одно и то же.
В переменную не могу, в параметра метода или функцию могу — рантайм ничего другого не пропустит, будет TypeError

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

В переменную не могу, в параметра метода или функцию могу — рантайм ничего другого не пропустит, будет TypeError

Ну так вы туда ничего не всунете кроме прошитого намертво объекта, то етсь зависимость определяет сам сервис. Где тут DI?


Это будет DI если в конструкторе (имеем в виду инъекцию через конструктор)

То есть если я сделаю метод setup(), который лямбду вызовет — это DI, а если сразу вызову — это не DI? ну серьезно? :) Какой вообще смысл в таком определении, если оно ничего осмысленного не определяет и в зависимости от точки зрения что угодно и является DI и одновременно не является?

Если быть совсем точным, то асбтракция всё же есть — класс это абсракция над инстансами. То есть клиент может выбрать какой инстанс он будет подсосывать. Например, будет ли он шарить один инстанс зависмости между несколькими сервисами или каждому свой давать (например, чтобы в разные соединения с БД у сервисов были).

Если метод setup вызовет клиент, то это DI, а если вы в коонструкторе, то нет :)

Смысл простой — при DI сервис ничего не знает о том как получать свои зависимости, они к нему приходят извне в готовом виде, он только в своём контракте сообщает о готовности их получать (часто через сигнатуру конструктора). Он делает свою работу c помощью своих зависимостей, но не занимается их конструированием, десреиализацией, гидрацией или настройкой.

Это в вашей трактовке DI бессмыслен — дадим сервису что угодно, а он сам догадается как это интрпретировать, сам создаст всё что нужно для его работы. В конце-концов ему лучше знать. Нет, именно для избавления от лишних знаний и нужен DI.
То есть клиент может выбрать какой инстанс он будет подсосывать.

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


Он делает свою работу c помощью своих зависимостей, но не занимается их конструированием, десреиализацией, гидрацией или настройкой.

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


Это в вашей трактовке DI бессмыслен — дадим сервису что угодно, а он сам догадается как это интрпретировать

Вот именно, что не все, что угодно, а полную информацию о конфигурации. Это далеко не все, что угодно :)
Вся ответственность за конфигурирование в данном случае лежит на клиенте. Сервис никакой ответственности не несет. Кстати, работа с IoC контейнерами часто предполагает именно описание зависимостей в виде конфига и определенной поддержки со стороны сервиса для работы с этим контейнером. То есть клиент передает сервису не сами зависимости, а информацию о способе их получения у контейнера. И по-вашему тогда такие контейнеры уже не DI, что абсурдно.

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

Не занимается, если получает ссылку или указатель на объект. Он использует её as is, согласно синтаксису языка, передаёт её рантайму как и получил.


Вот именно, что не все, что угодно, а полную информацию о конфигурации.

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


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

Именно, уже не DI. И не абсурдно, а строко по определению: rather than allowing a client to build or find the service,


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

> Не занимается, если получает ссылку или указатель на объект.

Всмысле не занимается? Занимается. Как же нет? Если у вас есть указатель сам по себе, то вы с ним ничего полезного не сделаете. Объект должен знать, как именно этот указатель на что указывает, как из этого указателя найти вызовы виртуальных методов, как прочитать бинарное представление объекта из памяти и преобразовать его в данные и т.п. вещи. Если он этого ничего не знает — у него не зависимость, а просто 64-битное число. Понимаете? сделать из 64-битного числа что-то полезное ничуть не проще, чем сделать это что-то полезное из куска jsonа. Вы по факту передаете обычный uint и это всегда задача сервиса сделать из этого uint чтото осмысленное.

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

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

> Именно, уже не DI. И не абсурдно, а строко по определению: rather than allowing a client to build or find the service,

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

> то это DI конкретно контейнера, а не остальных «скофигурированных зависимостей». В большинстве случаев — антипаттерн.

Это очень забавно, что у вас антипаттерн ничем не отличается с точки зрения семантики от паттерна. В этом случае вы сейчас и сами понятия «паттерн» и «антипаттерн» сделали эквивалентными и неотличимыми друг от друга :)
> Объект должен знать, как именно этот указатель на что указывает, как из этого указателя найти вызовы виртуальных методов, как прочитать бинарное представление объекта из памяти и преобразовать его в данные и т.п. вещи. Если он этого ничего не знает — у него не зависимость, а просто 64-битное число. Понимаете? сделать из 64-битного числа что-то полезное ничуть не проще, чем сделать это что-то полезное из куска jsonа. Вы по факту передаете обычный uint и это всегда задача сервиса сделать из этого uint чтото осмысленное.

Не должен. Обычно это обязанность языка. Объект говоритт языку «вот мне передали что-то, что должно быть ссылкой на другой объект. Вызови у него метод такой-то и верни результат».

> Объект должен знать, как именно этот указатель на что указывает, как из этого указателя найти вызовы виртуальных методов, как прочитать бинарное представление объекта из памяти и преобразовать его в данные и т.п. вещи. Если он этого ничего не знает — у него не зависимость, а просто 64-битное число. Понимаете? сделать из 64-битного числа что-то полезное ничуть не проще, чем сделать это что-то полезное из куска jsonа. Вы по факту передаете обычный uint и это всегда задача сервиса сделать из этого uint чтото осмысленное.

Не должен. Обычно это обязанность языка. Объект говоритт языку «вот мне передали что-то, что должно быть ссылкой на другой объект. Вызови у него метод такой-то и верни результат».

> Короче ничего не меняется кроме чисто формального размещения пары строк кода.

Это вы считаете, что ничего не меняется. Я так не считаю. Я считаю, что от места расположения чего-то типа new DependencyClass() container->get(«DependencyClass»). меняется многое или
Это вы считаете, что ничего не меняется. Я так не считаю. Я считаю, что от места расположения чего-то типа new DependencyClass() container->get(«DependencyClass»). меняется многое или

Что многое? Смотрите, извне сервиса вы даже не сможете узнать, какой именно у вас кусок кода написан. И при поддержке кода/рефакторинге тоже никакой разницы не заметите. Ее нет именно потому, что заметить ее невозможно.

Она есть потому что как автору сервиса мне не нужно писать код построения или поиска нужных мне зависимостей. Клиент волен использовать любой доступный ему способ, на который мне, как автору сервиса, плевать. У меня к нему только одно требование в данном контексте — предоставить мне полностью сконфигурированную зависимость указанным мною в контракте способом для обеспечения указанного мною же в нём же поведения. Мне, как автору сервиса ничего не нужно знать о том как конструировать и конфигурировать мои зависимости, мне нужно только знать как их непосредственно использовать в нужных мне целях. Мне нужно знать только ту часть полного контракта реально используемых мною зависимостей, которая касается ииспользования зависимости в моих целях. Это мало?

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

> если возвращаться к исходной теме — вы действительно путаете DIP и DI.

Так одно без другого в реальности не бывает.

> Вы можете реализовать DIP без DI (таблица указателей на функции в каком-нибудь Си

Это DI

> Например когда у вас зависимость это конкретная реализация какого-то класса.

Не понял, что вы тут щас подразумевали. В смысле класса а не интерфейса? А какая разница? Или что?
> Так одно без другого в реальности не бывает.

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

Погодите, именно это я и пытался доказать.

Это DI

А где вы там усмотрели именно внедрение зависимостей? Я бы согласился что для DIP нужно соблюдение Inversion of control, но DI лишь один из способов добиться IoC.


А какая разница? Или что?

Разница в направлении зависимостей. Предположим у нас есть модуль A который зависит от чего-то из модуля B. То есть направление зависимости у нас A -> B. А что бы возник принцип инверсии зависимостей, у нас B должен стать зависимым либо от A (A <- B), за счет добавления интерфейса в модуль A, либо, мы должны ввести третий модуль C, который будет содержать в себе абстракцию (A -> C <- B).


Если же у вас просто есть зависимость от класса — нет никакой инверсии зависимостей.


На всякий случай уточню, что мы должны стараться всегда выбирать направления зависимостей от менее стабильного модуля к более стабильному (Stable dependency principle от того же дяди Боба). И DIP тут является одним из вариантов как мы можем достичь этого.

А где вы там усмотрели именно внедрение зависимостей?

Вы серьезно? Это же классическое DI в самой чистой и незамутненной форме, которое всегда приводится в качестве примера. Ну, когда вы по интерфейсу передаете аргумент, а потом дергаете его методы через таблицу виртуальных ф-й.


Разница в направлении зависимостей. Предположим у нас есть модуль A который зависит от чего-то из модуля B. То есть направление зависимости у нас A -> B. А что бы возник принцип инверсии зависимостей, у нас B должен стать зависимым либо от A (A <- B). А что бы возник принцип инверсии зависимостей, у нас B должен стать зависимым либо от A (A <- B), за счет добавления интерфейса в модуль A

Не-не, в модуль А никто интерфейс совать не будет, т.к. это невозможно, с-но и перенаправление зависимости тоже невозможно. Ну то есть если А зависит от В, то В никогда уже зависеть от А не будет, как интерфейсы не насовывай. Интерфейс будет в отдельном модуле С и А с В будут зависеть от С, как в вашем "или", но только без "или" :)


А вообще разделение на модули тут не совсем правильно, правильнее говорить, что у нас есть клиент, сервис и интерфейс. Каждый из них может быть в каком угодно модуле, но DIP не о том, как зависят друг от друга эти модули (как угодно могут) а именно о том, как зависят друг от друга клиент, сервер и интерфейс. Они хоть в одном модуле могут все быть, физически.


Если же у вас просто есть зависимость от класса — нет никакой инверсии зависимостей.

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

> Ну то есть если А зависит от В, то В никогда уже зависеть от А не будет, как интерфейсы не насовывай.

Засунуть в А интерфейс, который для общения с А будет реализовывать В. А зависит от B, делаем интерфейс А.B, который реализует В и вот уже А не зависит от В, а Б зависит от А (А.В), при условии что А знает только о А.В получая В в качестве реализации А.В откуда-то извне.

> А если вы внедрение зависимости добавите, то чтобы оно могло получиться вам придется выделить ваш класс в модуль С, в результате чего В и А будут зависеть от С, но не друг от друга. То есть получаете ровно то же самое, что и в случае с интерфейсом.

Не придётся. Для внедрения зависимостей не нужно ничего выделять, изменяется только место инстанцирования/поиска зависимости — внутри сервиса или снаружи.
А зависит от B, делаем интерфейс А.B, который реализует В

Ну так если у вас А зависит от B и С зависит от B, то вы сделаете две копии интерфейсов (A.B и C.B)? Какое-то странное у вас решение и уж точно не то, что подразумевается под DI по дефолту.


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

Если вы ничего не выделите — то нечего будет внедрять. Ни снаружи ни внутри. Где тогда внедрение-то?

DIP не про DRY точно.


Внедрять зависимость. Было:


class A {
  constructor() {
    this.b = new B();
  }
}

стало


class A {
  constructor(B b) {
    this.b =b;
  }
}

Никакого выделения

нет. У вас A зависит от B. То что вы не сами создаете инстанс а получаете его извне не устраняет зависимости A от B. Инверсии направления зависимостей не происходит. Что нужно сделать — либо разместить в модуле A интерфейс, который будет имплементить B, и тогда уже B будет зависеть от A. Происходит инверсия. Другой вариант — введение модуля C, от которого будут зависеть и A и B.


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

нет. У вас A зависит от B. То что вы не сами создаете инстанс а получаете его извне не устраняет зависимости A от B.

Важно не кто создает сервис, а кто его конфигурирует, то есть кто определяет зависимости сервиса. А — клиент, Б — сервис, С — зависимости сервиса. Если конкретная зависимость С выбирается самим сервисом Б — это "прямая зависимость", а если С выбирается клиентом сервиса — это и есть dependency inversion. На примере двух модулей инверсии зависимостей не бывает, всегда нужен, как минимум, клиент, сервис и зависимость сервиса. Если вы ограничитесь сервисом и зависимостью то разворачивать будет нечего.


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

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

Если контракт Б оставляет клиенту единственную возможность «выбрать» С, будь то модификатор типа final или «песен ещё ненаписанных....», то это что?
Если контракт Б оставляет клиенту единственную возможность «выбрать» С, будь то модификатор типа final или «песен ещё ненаписанных....», то это что?

Это значит, что никакой возможности выбирать у клиента нет и выбирает, де-факто, сам сервис, разве нет? В чем смысл вопроса?

это прямая зависимость или инвертированная?

Конечно, прямая. Сервис сам свои зависимости выбирает. Инвертированная она будет тогда, когда выбирать будет клиент.

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


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

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

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

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


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();
Наверно для того чтобы в одном месте были общие действия для каждого подсчета, однако в примере это не показано.
Стоит отметить, что SOLID принципы делают разработку более долгой и сложной. Требуется создавать множество интерфейсов и абстрактных классов, что может показаться избыточным и ненужным.
Но при доработке или расширении вашего приложения вы поймете для чего все это было нужно.

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

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

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

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


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

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

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

Типа этого?


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();        
    }
};
Не это. Для упрощения можно убрать метод в базовом классе и оставить только в наследниках. Каждый наследник считает что-то свое и это нужно закэшировать. Причем некоторые наследники могут остаться не закэшированными.

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

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

Согласен что пример можно упросить. Базовый класс можно не учитывать.
декоратор?
Это решение примерно как прокси. Если нужно задекорировать модель с данными то все усложняется.
Нужно кэшивароние — прокси.
Нужен lazy load — прокси.
Нужен контроль доступа — проски.

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

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


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


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


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


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


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

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

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


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

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

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

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


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

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


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

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

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


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


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

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

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


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

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


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

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


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

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


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

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

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

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


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

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


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

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

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

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

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


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


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

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

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


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


Та же история с TDD где фокус уходит на тестирование нежели на дизайн как изначально подразумевалось, с DDD где смысл в уменьшении стоимости перевода из требований в код и про сам процесс изучения предметной области в процессе разработки. BDD, где все свели к When I click button вместо того, что бы использовать этот подход для сбора и формализации требований. Ну и ООП где идея распределенной системы обменивающейся сообщениями (что-то типа actor model) заменила себя на процедурщину и классы.

Получается, что объяснение SOLID не может быть простым, так?

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


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

Формально? Ну, допустим, LSP, OС и DI «вытекают» из теории типов (хотя, последнее может и нет).
В то время как SR и IS — вещи скорее интуитивные, как мне кажется.
Может. Только нужно как в Алисе. Сначала съесть пудинг, а потом разрезать. Т.е. вы читаете что такое SOLID и начинаете делать проект. Когда вы начинаете понимать, что уже по ушу завязли в грязных хаках и уже чувствуете стремитльное падение в кроличью нору вы останавливаетесь и зовете гуру. Он смотрит ваш код и тыкая пальцем в нужные места говорит, что ты сделал так, а может быть нужно было вот так? И вот тогда приходит просветление.
зовете гуру

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


И вот тогда приходит просветление.

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

Ну вот как раз отправку на отдых мы не хотим делать для всех. Собственно разделяем интерфейсы для того, чтобы у робота не было отправки на отдых, чтобы он про отдых вообще не знал. И передавать скопом мы можем только инстансы WorkAbleInterface и работать с ними только исходя из этого. Да, в PHP можно (не уверен, что можно в C#/Java/...) проверить дополнительно, а не реализует ли инстанс WorkAbleInterface ещё и SleepAbleInterface, но такая проверка в общем случае говорит о том, что мы очень сильно что-то делаем не так — не должен какой-то модуль рассчитывать на то, что SleepAbleInterface как бы является расширением WorkAbleInterface, если в коде они не связаны явно через extends, но при этом по факту нет инстансов SleepAbleInterface, не реализующих WorkAbleInterface. Если это просто «случайно» так получилось, то нормально, а если есть явные или неявные требования или допущения, что каждый инстанс SleepAbleInterface реализует WorkAbleInterface, значит мы SIP применили не к месту, надо было применять расширение интерфейса, а не разделение.
НЛО прилетело и опубликовало эту надпись здесь
Обширна тема, которую врятли можно освоить за одну статью. Может порекомендуете ещё чтение по данной теме? Лучше книги.
Мне одному мозолят глаза public атрибуты в примерах? Инкапсуляция, наследование и полиморфизм до SOLID обычно продаются. Если уж давать примеры, то сразу по всем канонам, ничего бы не усложнилось сильно.
public атрибуты инкапсуляции не нарушают.

А как же доступ к атрибутам класса через методы этого класса.

Делайте геттеры и сеттеры если хотите, но к инкапсуляции это особого отношения не имеет, это сокрытие деталей реализации.
Да хотябы вот даже определение:
Encapsulation is one of the fundamentals of OOP (object-oriented programming). It refers to the bundling of data with the methods that operate on that data.[5] Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties' direct access to them. Publicly accessible methods are generally provided in the class (so-called getters and setters) to access the values, and other client classes call these methods to retrieve and modify the values within the object.
en.wikipedia.org/wiki/Encapsulation_(computer_programming)
Так перечитайте то, что скопировали «Encapsulation is used to hide the values». Инкапсуляция — это не сокрытие, а объединение данных с методами. Она может использоваться для сокрытия, а может не использоваться.
preventing unauthorized parties' direct access to them
«Она может использоваться для сокрытия, а может не использоваться.» — только вот использовать её без сокрытия не имеет смысла, и в большинстве случаем их и не разделяют, а если решили разделить, то надо описывать это. В противном случае, привыкнув писать публичные атрибуты и взявшись за что-то более менее сложное, вы и итоге получите бесконечные неуверенности в том, что же у вас там в этом публичном атрибуте хранится.
В большинстве случае публичные атрибуты — зло, исключения составляют случаи когда речь идет о совсем простых классых типа
class Point {
    public int x;
    public int y;
}

, да вот только в PHP нет типов и туда можно запихать хоть строку например которая вообще не приводится к числу. Методы ограничивают доступ к данным класса и позволяют фильтровать вход. А если будете писать в такой манере, делая публичными методы, то ваш код ничем не будет отличаться от использования массивов.
class Point {
    private $x;
    private $y;

    public function getX(): int
    {
        return $this->x;
    }

    public function setX(int $x): void
    {
        $this->x = $x;
    }

    public function getY(): int
    {
        return $this->y;
    }

    public function setY(int $y): void
    {
        $this->y = $y;
    }
}


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

Авторы (навскидку) python, javascript, php (4) решили, что и без сокрытия имеет смысл объединять данные и методы, их обрабатывающие.


то ваш код ничем не будет отличаться от использования массивов.

Будет. К массивам нельзя методы прикрутить, конструкторы, использовать в тайпхинтинге нормально и в инстансоф


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

Именно. Если делаю публичные геттеры-сеттеры, то только когда понимаю зачем я их делаю. И обычно для меня такие "тупые" геттеры-сеттеры маркер плохого проектирования: если они ничего не делают, кроме сокрытия ради сокрытия, то нет смысла их использовать, это нарушение KISS и YAGNI.

Авторы (навскидку) python, javascript, php (4) решили, что и без сокрытия имеет смысл объединять данные и методы, их обрабатывающие.

PHP то вы зачем сюда приплели, у него с инструментами сокрытия все впорядке.
Будет. К массивам нельзя методы прикрутить, конструкторы, использовать в тайпхинтинге нормально и в инстансоф

Что мешает запихать методы в массив.
Именно. Если делаю публичные геттеры-сеттеры, то только когда понимаю зачем я их делаю. И обычно для меня такие «тупые» геттеры-сеттеры маркер плохого проектирования: если они ничего не делают, кроме сокрытия ради сокрытия, то нет смысла их использовать, это нарушение KISS и YAGNI.

Все с точность до наоборот, они позволяют в момент ограничить входные данные, повзволяют быть уверенным в том, что на входе именно то что ожидаешь, повзволяют расширять код в будущем (к слову о плохой архитектуре). Не раз приходилось сталкиваться с ситуацией, когда было крайне необходимо добавить дополнительную обработку в setter, отловить входной параметр при бебаге, или преобразовать данные при входе/выходе. KISS и YAGNI тут вообще не причем, генерация методов идет на уровне IDE и ничего не усложняет.
> PHP то вы зачем сюда приплели, у него с инструментами сокрытия все впорядке.

В 4-й версии модификаторов доступа не было, все члены класса были публичными.

> Что мешает запихать методы в массив.

К чему вы будете $this привязывать?

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

«Позволяют» != «делают». Максимум через тайпхинт (если разрешено и сделали) отлавливают тип, но в PHP тайпхинтинг и для свойств завезли.

> KISS и YAGNI тут вообще не причем, генерация методов идет на уровне IDE и ничего не усложняет.

Очень причём. Введение «тупых» акцессоров без преобразований и проверок просто чтобы было нарушает KISS, а «потом легче будет добавить преобразования и проверки» нарушает YAGNI. А кроме написания кода, его ещё и читать надо. А практика показывает, что в 99% случаев геттеры/сеттеры так и остаются тупыми, по факту не делая никакого сокрытия, но понимание кода усложняя.
В 4-й версии модификаторов доступа не было, все члены класса были публичными.

Ну вы вспомнили. Даже 5 уже устарел. Меня смущает упоминание 4 версии. Если у вас старая школа, я вас не переубежу.
К чему вы будете $this привязывать?

Так, тут вообще зарулили не туда, изначально суть была вообще в другом. ООП позволяет хорошо организовать, массивы позволяют хранить все те же переменные и методы, но без всякой структуры. Ограничение доступа — один из инструментов сохранения этой структуры.
Можно конечно поизвращаться:
<?php
$a = [
    'value' => 1,
];
$a['this'] = function () use ($a) {
    return $a;
};
var_dump($a['this']()['value']);

но в PHP тайпхинтинг и для свойств завезли

Серьезно? Похоже, что еще только в проекте. Если разработчик не использует типы, то это уже на его совести.
А практика показывает, что в 99% случаев геттеры/сеттеры так и остаются тупыми, по факту не делая никакого сокрытия, но понимание кода усложняя.

Зато в том 1% случаев вы выиграете гораздо больше чем потеряете, если не будете рефакторить код по всему проекту переводя атрибут на метод, или разыскивая где-эже этот атрибут изменился.
Если у вас старая школа, я вас не переубежу.

Это не старая школа, а классические определения, не привязанные к языку или семейству языков типа "C++ образных".


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

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


Серьезно? Похоже, что еще только в проекте.

Status: Implemented (in PHP 7.4)


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

Или на совести тех, кто принимал соглашения и кодировании, вібирал версию языка и т. п.


Зато в том 1% случаев вы выиграете гораздо больше чем потеряете, если не будете рефакторить код по всему проекту переводя атрибут на метод, или разыскивая где-эже этот атрибут изменился.

Количественные оценки этого "гораздо больше" есть? Они учитывают возможность использования магического метода __set(), если вот прямо срочно нужно?


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

Это не старая школа, а классические определения, не привязанные к языку или семейству языков типа «C++ образных».

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

Но ООП без сокрытия это сущий ад.
Status: Implemented (in PHP 7.4)

Но релиз-то еще только в конце 2019 и оно еще может быть перенесен. А пока повсеместно начнут использовать, так появится еще куча кода в старом стиле.
Количественные оценки этого «гораздо больше» есть? Они учитывают возможность использования магического метода __set(), если вот прямо срочно нужно?

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

А вот тут мы идем как раз по типичной дорожке к запутанному коду. Magic методы вообще губительны как для проекта, так и для ООП. В этом случае предполагается, что вы знаете код, и как там работают эти магические методы. Об автокомплите тоже можно забыть, так как «Метод __set() будет выполнен при записи данных в недоступные свойства. », т.е. придется сначала убарть атрибут, чтобы заработал методы, а значит пропадет автокомплит. Ну и искать где он там меняется все же сложнее, чем искать вызовы метода.
Но ООП без сокрытия это сущий ад.

JavaScript и Python показывают, что писать на ООП без сокрытия членов класса вполне возможно. И как-то особо никто не жалуется на отсутствие модификаторов доступа.
С другой стороны, если где-то кто "заприватит" нужное мне поле в PHP, то я всегда через Reflection или Closure могу влезть, и вот это точно полный ад будет при отладке.


Но релиз-то еще только в конце 2019

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


Об автокомплите тоже можно забыть

phpdoc @property специально для этого


Ну и искать где он там меняется все же сложнее, чем искать вызовы метода.

В чём бОльшая сложность?

К слову, C++ тоже не всё хорошо с сокрытием. Идиома PIMPL не на пустом месте возникла.
В противном случае, привыкнув писать публичные атрибуты и взявшись за что-то более менее сложное, вы и итоге получите бесконечные неуверенности в том, что же у вас там в этом публичном атрибуте хранится.

А каким образом автозамена данных атрибутов на пару геттер/сеттер меня избавит от такой неуверенности?


Не раз приходилось сталкиваться с ситуацией, когда было крайне необходимо добавить дополнительную обработку в setter, отловить входной параметр при бебаге, или преобразовать данные при входе/выходе.

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


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

Именно. Не $contract->setStatus(Contract::STATUS_APPROVED), возможно бросающее исключение при попытке нарушить флоу, а возможно нет, а потом где-то проверку $contract->getStatus() !== Contract::STATUS_APPROVED, а просто $contract->approve() и проверка $contract->isApproved()

Зарегистрируйтесь на Хабре, чтобы оставить комментарий