Comments 201
Он может иметь несколько методов, но они должны использоваться лишь для решения общей задачи
Вопрос на миллион, насколько «общей» должна быть эта задача?
нам стать Вангой 99lvl.
Интересный факт. В книжке Дяди Боба, где он SOLID расписывает, вся эта тема идет после главы о рефакторинге и постоянном улучшении кода.
Это я к чему, попробуйте подумать о solid с другой стороны. Когда у вас код уже написан и вы вносите очередные правки. Все еще ли с этими правками код соблюдает solid или нам надо разбить этот класс и повыносить часть логики?
Пытаться upfront сделать все по solid невозможно. Ведь даже если вы сможете в 100% случаев все делать соблюдая SRP то с OCP уже все намного сложнее.
А вот что пишет про SRP Егор Бугаенко.
Егор такой Егор…
Где-то тут уже писали что в последних редакциях дядя Боб делает уточнение что фокус этого принципа на выделении ролей, которые генерят вам требования. "Зона ответственности" в том плане что вашему модулю соответствует один стэкхолдер. И профит от соблюдения этого принципа заключается в том, что нам намного проще отслеживать конфликты в требованиях разных людей, проще предсказывать изменения модулей (когда уже известен некий набор изменений которые были произведены в прошлом), ну и в целом если мы добавим к этому OCP то проще делать более стабильные модули.
Это не про маленькие классы, как пишет Егор. Нет никаких противоречий аля "является ли багфикс причиной для изменения, а оптимизация? а рефакторинг" как пишет Марко.
По факту этот принцип целиком и полностью завязан на то, как ваши модули представляют интересы людей, для которых вы эти модули пишите. И да, это сложно. Есть книжки на эти темы (типа Object design, roles responsibilities and collaborations) и соблюдать этот принцип можно только если вы учитываете поток изменений требований.
А если потока изменений требований нет — можете обратиться к другому принципу — Информационный эксперт. Он проще и не требует анализа потока изменений. Он в купе с protected variations в целом про information hiding и декомпозицию. Так же по сути как и SRP + OCP.
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 только хуже делают как по мне в этом плане.
class diagram смещает внимание с взаимодействия на структуру.
Так диаграмма взаимодействия в uml тоже есть
Как выше отметили — диаграмм в UML хватит на все случаи жизни — это очень многогранный язык, позволяющий, при правильном инструментарии и навыках, строить сквозные связанные модели ОТ и ДО. Другое дело, что его надо уметь готовить, но это относится вообще ко всему. А если ничего кроме class diagramm не уметь и не использовать, то конечно да.
<?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 конкретного уровня. Обосновывать код метода бизнес причиной — не всегда удачная идея.
Да тот же взять мейлрушный пример первый. Пусть перепишут его на prepared и еще разок покажут.
И еще: универсальные интерфейсы для работы с бд — миф чистой воды.
ЗЫ мне кажется что в погоней за SOLID, попытке завернуть все в ООП без понимания что это, все забывают про KISS, YAGNI. А это очень и очень плохо.
Особенно неприятно в 2018 году видеть примеры на php без указания типов (возвращаемых значений).
Принципы SOLID — это стандарт программирования, который все разработчики должны хорошо понимать, чтобы избегать создания плохой архитектуры.
SOLID — это Design. Мне кажется, архитектура приложения лежит на более высоком уровне.
зависит реализация минимум половины кода приложения
Вы серьезно? Я могу поменять один DI контейнер на другой и это вряд ли повлечет за собой изменение половины кода. Только сам код конфигурациии DI, а это далеко не половина.
Примеры архитектурных решений: нужна секьюрити/не нужна, используем БД/документную/реляционную/не используем, eventual-consistency/transactional-consistency, и т.п.
Архитектурные решения обусловлены функциональными и не функциональными требованиями.
Будет использоваться DI или нет — это выбор программиста, а не решение, которое принимает архитектор. Это, — использовать DI, как руки мыть перед едой, какая еще архитекрута...
Это вы из какой-то методики цитируете или сами опредилили так? Например жесткие роли.
это выбор программиста, а не решение, которое принимает архитектор.
кто такой архитектор? Зачем он нужен? в чем его функция?
— обеспечить простое изолированное юнит-тестирование каждого класса
— обеспечить замену реализаций абстракций без изменения исходного кода
Их можно реализовать и другими способами, например, добавлением в тулинг разработки тестового фреймворка, позволяющего мокать произвольные классы и методы, а также использованием сервис-локатора. Но архитектор (или разработчик исполняющий его роль) подумал и решил использовать DI-контейнер с текстовыми конфигами.
Ну и обычно рядовой программист на проекте не свободен в выборе использовать DI или нет, использовать для него контенйер или нет. На этапе ревью могут отклонить патч, вводящий DI-контейнер в проект, по причине «лишние абстрации, лишние зависимости проекта в целом», а вводящий DI в целом как «дублирование кода» или «нарушение KISS». Ну и наоборот, отклонить по причине «инициализация зависимостей класса в асмом классе».
DI контейнер это про ивенсию управления, а не инверсию зависимостей же.
Dependency Injection — частный случай Inversion of control (на ряду с фабриками, сервис локаторами и т.д.) Причем сам по себе DI никакого отношения к DIP не имеет, так как только за счет использования оного у вас не происходит инверсии направления зависимостей.
А вот если вы сделаете интерфейсик, на который будет завязан ваш код, и этот интерфейсик будет принадлежать самому стабильному модулю из двух (наш и тот где реализация), тогда направление зависимостей изменяется (реализация теперь зависит от интерфейса а не наш код от реализации).
Просто зависимости инжектятся клиентом без всякой инверсии.
Инверсия управления, это когда мы разворачиваем поток управления. То есть у нас не 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.
P.S. Наверное надо было как-то акцентировать внимание, что DI я применил исключительно к коду AuthManager, а дальше просто вариации как можно собственно инъекцию произвести без очень умных фреймворков.
в его контракт входит, что его зависимости должны быть ему предоставлены извне
И это демонстрация принципа инверсии управления) В обоих случаях для AuthManager
мы имеет эту самую инверсию. Во втором варианте за счет фабрики (клиентский код выступает в этой роли) а во втором случае за счет контейнера.
изменение кода клиента
проблема в том что вы сами изолировали пример только в рамках 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 основное — зависимость от абстракций
Это способ реализации конкретно в ООП-языках, а не определение. В общем же в dependency inversion основное — перенос ответственности за конфигурацию сервиса из самого сервиса в клиента.
А вы можете привести пример одного без другого? Чтобы инжектить в клиенте зависимости, они в сервисе должны быть связаны по интерфейсу. А если зависимости в сервисе связаны по интерфейсу, то сервис сам не может свои зависимости сконфигурировать. То есть это с практической точки зрения тождественные понятия.
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» — передаётся уже готовый объект, не какая-то информация, по которой клиент может как-то получить свою зависимость
Я говорю, что в данном контексте нету разницы между готовым объектом и информацией, которая достаточна для того, чтобы данный объект сконструировать :)
На самом деле ведь в любом случае передается именно информация, то есть "уже сконструированный объект" — это тоже информация. При чем информация та же самая, что и в случае "информация о конструировании". Это одна и та же информация, которая просто представлена по-разному. Так вот, мой тезис в том, что нам важен именно характер передаваемой информации, но не способ ее представления. Вы можете передать инфу о конструировании, может передать объект в представлении конкретного рантайма (ну, как объект класса в данном языке, с-но), можете передать объект из-под какого-то другого рантайма, например через ffi с оберткой, может передать объект в сериализованном в какой-то бинарный формат виде, можете в виде конфига в той или иной форме. Это абсолютно все неважно.
Объект, в который идёт инъекция, вообще не должен нести ответственности за конструирование или поиск своих зависимостей ни по своей информации, ни по переданной извне, ни как-то ещё.
Так вы всегда передаете информацию об объекте. Указатель на него, например. Либо бинарное представление в памяти (если по значению структуру, допустим, передали). И это будет ответственность сервиса правильно проинтерпритировать этот указатель (или данные в памяти) и использовать его корректным образом, как объект.
Передача какой-то другой информации вместо конкретной зависимости
Еще раз, информация во всех случаях одна и та же. Разница в ее представлении. Это как передать один и тот же текст в разной кодировке. Байты разные — текст один и тот же. Почему при передаче текста в одной кодировке, по-вашему, ДИ есть, а в другой кодировке — уже нет? Я за то, чтобы смотреть не на кодировку текста, а на его содержание. Если текст, который вы передали, содержит описание зависимости (в виде указателя, бинарных данных, конфига, не важно) — вы передали зависимость. Если не содержит — не передали.
Просто в вашем понимании паттерн становится бессмысленным абсолютно.
Ну и информация обычно немного разная. Объект, переданный по ссылке — будет одним и тем же объектом для клиента, он может передавать его куда угодно и будет уверен, что один объект, один его стейт будет шарится межу всеми сервисами, куда он его передал, а не воссоздаваться. Согласиться с вами в случае передачи объекта, если ссылка или указатель на объект передаётся в разных формах, позволяющих сервису получить доступ к объекту. А если передается информация о том как создать сервису свою зависимость или где-то найти её, то это уже не DI.
Как и со строками. Если объекту нужна зависимость в виде utf8 строки, то передача utf8 строки — это DI, а передача строки в какой-то другой кодировке, которую объект, должен будет конвертировать в utf8 — это почти 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 за конфигурацию сервис не отвечает мы, вроде, оба согласны.
> Как он может конфигурировать, если ни строчки кода про конфигурацию нет?
Весь код конфигурации за пределами сервиса же, мы вроде об этом договорились. Сервис принимает внешнюю конфигурацию.
> Как о том же, если предлагаете в сервис передавать, например, имя каталога для сохранения, создавать объект доступа к ФС и конфигурировать его, чтобы он по этому пути сохранял?
> Как о том же, если предлагаете в сервис передавать, например, имя каталога для сохранения, создавать объект доступа к ФС и конфигурировать его, чтобы он по этому пути сохранял?
А имя каталога для сохранения — _это достаточная_ информация для работы сервиса-сохраняльщика? Если достаточная и никакого больше конфигурирования не требуется — то это ДИ, конечно. Но в реальности она достаточна не будет, как мы понимаем.
> Очевидно, что зависит. Определение явно говорит, что передаётся объект
То есть если сервис требует не синглтон, то это уже не ДИ, а если синлготон — то ДИ? Вот так новости!
> Частный случай — это 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 или нет? Чем последний случай отличается от передачи имени класса и получения конкретного объекта класса от сервис-локатора? (тогда имя класса — лямбда на некотором не-хост ЯП, а сервис-локатор — интерпретатор этого ЯП)
Ребёнок говорит маме: «хочу есть», она ему «на кашу ешь» — это 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 паттерн. Главное, он получает готовую зависимость от клиента, а не конструирует или ищет её сам.
Если язык динамический, то есть базовый тип any. Вот
> final class SomeClass {} не имеет предков и не может быть расширен, чистая реализация без абстракций.
Вы же можете в переменную с типом SomeClass засунуть любую хрень, так? То есть _по факту_ тип этой переменной any. А если вы можете туда засунуть только SomeClass и ничего более (ну пусть рантайм проверяет), то вы не можете выбрать альтернативную реализацию, то есть она намертво прошита.
> Главное, он получает готовую зависимость от клиента, а не конструирует или ищет её сам.
Ну так еще раз, DI не про то, кто и что конструирует. Вы подходите к терминологии чисто формально, типа «если есть строчка кода вида Х в месте Y — это DI, иначе — не DI», но надо смотреть на смысл этой строчки, что она делает. Представьте, что я в конструктор класса Х передаю лямбду, которую Х вызывает и которая, с-но, конструирует внутренности Х. Формально, получается, никакого DI нет, ведь вся конструкция выполняется внутри конструктора. Класс сам себя конструирует, сам все зависимости инициализирует. Но по факту DI есть, потому что вся ответственность за инициализацию лежит не на сервисе а на клиенте, и разница между «передать лямбду, которая будучи запущена внутри сервиса настроит зависимости» и «вызвать эту лямбду в самом клиенте» — вобщем-то отсутствует, это с точки зрения семантики совершенно одно и то же.
Это вы какой-то сервис-локатор с одним сервисом или фабрику описываете, это немного другие паттерны. Это будет 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 чтото осмысленное.
Не должен. Обычно это обязанность языка. Объект говоритт языку «вот мне передали что-то, что должно быть ссылкой на другой объект. Вызови у него метод такой-то и верни результат».
> Короче ничего не меняется кроме чисто формального размещения пары строк кода.
Это вы считаете, что ничего не меняется. Я так не считаю. Я считаю, что от места расположения чего-то типа new DependencyClass() container->get(«DependencyClass»). меняется многое или
Это вы считаете, что ничего не меняется. Я так не считаю. Я считаю, что от места расположения чего-то типа new DependencyClass() container->get(«DependencyClass»). меняется многое или
Что многое? Смотрите, извне сервиса вы даже не сможете узнать, какой именно у вас кусок кода написан. И при поддержке кода/рефакторинге тоже никакой разницы не заметите. Ее нет именно потому, что заметить ее невозможно.
если возвращаться к исходной теме — вы действительно путаете DIP и DI. Вы можете реализовать DIP без DI (таблица указателей на функции в каком-нибудь Си, или промежуточный модуль который предоставляет абстракцию остальным и какой-то другой модуль, который является реализацией этой абстракции), а так же вы можете практиковать DI без соблюдения DIP. Например когда у вас зависимость это конкретная реализация какого-то класса.
Так одно без другого в реальности не бывает.
> Вы можете реализовать 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 по дефолту.
Для внедрения зависимостей не нужно ничего выделять, изменяется только место инстанцирования/поиска зависимости — внутри сервиса или снаружи.
Если вы ничего не выделите — то нечего будет внедрять. Ни снаружи ни внутри. Где тогда внедрение-то?
нет. У вас A зависит от B. То что вы не сами создаете инстанс а получаете его извне не устраняет зависимости A от B. Инверсии направления зависимостей не происходит. Что нужно сделать — либо разместить в модуле A интерфейс, который будет имплементить B, и тогда уже B будет зависеть от A. Происходит инверсия. Другой вариант — введение модуля C, от которого будут зависеть и A и B.
Ну и очень важный момент который многие упускают, это выбор того когда нам нужна инверсия. В частности инверсию мы можем использовать для того, что бы все направления зависимостей указывали от менее стабильного модуля (тот кто чаще меняется) к более стабильному (тот кто меняется реже).
нет. У вас A зависит от B. То что вы не сами создаете инстанс а получаете его извне не устраняет зависимости A от B.
Важно не кто создает сервис, а кто его конфигурирует, то есть кто определяет зависимости сервиса. А — клиент, Б — сервис, С — зависимости сервиса. Если конкретная зависимость С выбирается самим сервисом Б — это "прямая зависимость", а если С выбирается клиентом сервиса — это и есть dependency inversion. На примере двух модулей инверсии зависимостей не бывает, всегда нужен, как минимум, клиент, сервис и зависимость сервиса. Если вы ограничитесь сервисом и зависимостью то разворачивать будет нечего.
Ну и очень важный момент который многие упускают, это выбор того когда нам нужна инверсия.
Она нужна практически всегда, т.к. логика за этим простая — клиент практически всегда имеет больше информации о том, что именно ему нужно от сервиса, чем сам сервис. А поскольку семантика сервиса зависит от конкретно выбранных зависимостей, то определять ее и следует клиенту, а не самому сервису.
архитектура портов и адаптеров (hexagonal которая) полностью построена на идее dependency inversion.
SOLID это принципы, набор ограничений. Архитектура приложения это совокупность всех ограничений, которые мы накладываем на систему. Все это является частью процесса проектирования. Как выбор ограничений так и их применение.
По сути эти рекомендации задают договоренности по ограничениям(constraint) структуры кодовой базы с целью облегчения ее понимания всем с ней работающим.
А зачем городить такую монструозную конструкцию:
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 принципы хороши в больших и продолжительных проектах, на коротких проектах это долго и дорого.
Если у вас есть необходимость тестировать код (или даже использовать TDD), то вам придётся следовать принципам SOLID, иначе вы попросту не сможете писать тесты.
То есть всё зависит от того, нужно ли писать тесты или нет. На коротких и неподдерживаемых проектах тесты действительно замедляют разработку. На серьёзных же проектах без тестов никуда: выигрыш времени на старте приведёт к значительно большим потерям в будущем.
Для того, чтобы тесты работали не обязательно следовать каждому принципу, до большинства вещей доходят при расширении приложения.
Можно решить это добавлением кэширования в объект с которым работают классы. Но нужна реализация над классами.
Типа этого?
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. Ничего инверсия зависимостей не требует.
Но просто шарахнуть циклом мы не можем.
мораль — никто опять ничего не понял.
Суть сегрегации интерфейсов не в том что бы разделить интерфейсы, а в том что бы интерфейсы проектировать специализированные, под конкретную задачу, под конкретный клиентский код, что бы скрыть деталей по максимуму, уменьшить влияние этих зависимостей на клиентский код.
А то что вы описали это не про SOLID. Это про то, как люди думаю что понимают в чем смысл SOLID а на самом деле занимаются фигней.
Вам не кажется, что примеры должны быть другими?
Очень сложно объяснить принципы на примитивных примерах. Во первых потому что на примитивных примерах в отрыве от контексте можно упустить суть (и в итоге появятся люди которые будут считать что если они для каждого класса будут делать интерфейс они соблюдают DIP). Ну и во вторых, всякий раз когда кто-то пытается объяснить довольно сложную идею при помощи кода, фокус внимания мгновенно уходит с идеи.
Люди до сих пор не понимают в чем смысл инкапсуляции. Не понимают концепции связанности и эффекта от изменений на клиентский код. Да что уж, и сам термин "клиентский код" для многих что-то о чем они не думают. А уже на SOLID замахнулись.
Та же история с TDD где фокус уходит на тестирование нежели на дизайн как изначально подразумевалось, с DDD где смысл в уменьшении стоимости перевода из требований в код и про сам процесс изучения предметной области в процессе разработки. BDD, где все свели к When I click button
вместо того, что бы использовать этот подход для сбора и формализации требований. Ну и ООП где идея распределенной системы обменивающейся сообщениями (что-то типа actor model) заменила себя на процедурщину и классы.
Полагаю что может быть, есть есть определенный фундамент. Например человек должен прекрасно понимать почему лучше уменьшать количество зависимостей, как проявляется связанность и т.д. Что сам понимаешь уже не так просто. Более того, как и многие такие концепции, даже если их удается просто объяснить, нужен еще способ убедиться в корректности интерпретации.
А вот формального определения SOLID я думаю мы не увидим. Хотя есть люди которые пытаются через логику это описать.
зовете гуру
и тут мы наблюдаем проигрыш в 80% случаев поскольку никакого гуру рядом не проходит, лишь менеджер-чеширский кот загадочно улыбается и указывает трубкой от кальяна на дэдлайн.
И вот тогда приходит просветление.
вы сейчас в целом описали самый неэффективный способ формирования опыта. Наступить на грабли десяток раз, и либо самим догадаться больше так не делать либо вам повезет и мимо будет проходить человек который уже проходил этот путь хотя бы за 5 лет до вас.
А как же доступ к атрибутам класса через методы этого класса.
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)
«Она может использоваться для сокрытия, а может не использоваться.» — только вот использовать её без сокрытия не имеет смысла, и в большинстве случаем их и не разделяют, а если решили разделить, то надо описывать это. В противном случае, привыкнув писать публичные атрибуты и взявшись за что-то более менее сложное, вы и итоге получите бесконечные неуверенности в том, что же у вас там в этом публичном атрибуте хранится.
В большинстве случае публичные атрибуты — зло, исключения составляют случаи когда речь идет о совсем простых классых типа
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 и ничего не усложняет.
В 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
специально для этого
Ну и искать где он там меняется все же сложнее, чем искать вызовы метода.
В чём бОльшая сложность?
В противном случае, привыкнув писать публичные атрибуты и взявшись за что-то более менее сложное, вы и итоге получите бесконечные неуверенности в том, что же у вас там в этом публичном атрибуте хранится.
А каким образом автозамена данных атрибутов на пару геттер/сеттер меня избавит от такой неуверенности?
Не раз приходилось сталкиваться с ситуацией, когда было крайне необходимо добавить дополнительную обработку в setter, отловить входной параметр при бебаге, или преобразовать данные при входе/выходе.
Если у вас операции доступа/чтения данных зачем-то требуют какой-то дополнительной логики — это плохо уже само по себе.
Хороший стиль — это как раз публичные атрибуты (естественно, те, которые следует раскрыть в согласии с интерфейсом) и, если вам нужен метод, который что-то там делает — то пусть это будет конкретно обычный метод, с понятным именованием и понятной семантикой. А не сеттер, в который чет-там понапихали.
Простое объяснение принципов SOLID