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

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

Итого, шаги определения корректности реализации DIP [...] убедиться, что интерфейс принадлежит МВУ

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

Открываем википедию и видим ответ:

Nevertheless, the "inversion" concept does not mean that lower-level layers depend on higher-level layers directly. Both layers should depend on abstractions (interfaces) that expose the behavior needed by higher-level layers.

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

Собственно, ваша проблема хорошо иллюстрируется вот чем.

Вот ваше определение:

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

А вот, что вы предлагаете сделать:

Т.е. в МВУ не должно быть ни одной строчки кода из МНУ.

Но если в "МВУ" нет "ни одной строчки кода из МНУ", то... он его не использует. При этом поскольку ваш "МНУ" использует интерфейс из "МВУ", он... начал его использовать. У вас модули поменялись местами.

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

Я только не понял как это интерфейс в верхнем уровне оказался? Нижний уровень не может знать о интерфейсе верхнего уровня.

Тут еще вопрос что верхнее, а что нижнее. На диаграимах UML родительское сверху рисуют, а тут МВУ это то что использует готовое. Полная инверсия, короче.

Тут еще вопрос что верхнее, а что нижнее. На диаграимах UML родительское сверху рисуют, а тут МВУ это то что использует готовое. Полная инверсия, короче.

Вот поэтому я и пишу, что поменялось местами.

Когда код, например, мейна(любой клиентский код), будет использовать эти оба модуля, он запихнёт модуль А в конструктор модуля В через параметры и это правило будет работать, но я могу это представить только на С(на указателях функций) или Голанге(на утиной типизации), а на С++ и языках с чётким ООП не могу без дополнительных обёрток(которые точно испортят читаемость).

Пример бы посмотреть, а то статья без него какая-то неполная.

вы про какие обертки? интерфейс/абстр.класс это не обертка, это способ выделения абстракции

class Interface{
  virtual void m() = 0;
}

class A {
  A(Interface i) {}
}

class B extends Interface {
	virtual void m() { ... implement me ... }
}

void main() {
  B b = new B();
  A a = new A(b);
}

void main_test() {
// а в тестах подкладываем другую имплементацию: мок или стаб
  MockB b = new MockB();
  // при этом в классе A ничего не поменяется, у него зависимость на интерфейс
  A a = new A(b);
}

Это всё прекрасно, пока у вас всё это в одном файле модуле, а вот как модуль нижнего уровня должен имплементировать интерфейс, если тот объявлен в модуле верхнего уровня? Импортировать этот модуль? Но это ж дичь какая-то

все верно, именно в этом суть инверсии: детали зависят от абстракций

А если вынести все эти контракты в отдельный юнит/модуль?

Кмк так будет даже удобнее.

выглядит как нарушение пакетных принципов - разрывает cohesion

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

а вы ничего не перепутали? например coupling и cohesion?

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

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

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

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

по какой причине и по какому правилу они собраны? соответствует ли эта группировка common closure / сommon reuse principle?

и кроме того априори модуль с десятком зависимостей имеет запашок и тенденцию стать big ball of mud. крайне не рекомендую

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

Давайте так. Что вы понимаете под словом "модуль"?

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

единица программы, предназначенная для решения конкретной задачи

Хорошо. А статический метод в таком определении это модуль? :) А класс? А пакет? Метапакет?

Ну и вот вам гипотетически-практический пример из головы.
Есть модуль, ну скажем, payment_processors. В нем контракты-интерфейсы типа base_payment_processor, online_payment_processor, offline_payment_processor и т.п., плюс другие вещи типа payment_status и т.п. Там же модули - фасады к конкретным платежным методам, типа paypal, bitcoin и прочее. За фасадами конкретные реализации, типа разные интеграции paypal с разными sdk и прочее. Плюс еще могут быть модули типа payment_subscriptions и другое разное.
И вот куда тут вы предлагаете воткнуть интерфейсы? При условии, что они могут использоваться за пределами основного модуля как минимум для типизации. Самое логичное - держать их прямо в основном пакете, типа payment_processors.online_payment_processor / payment_processors.payment_status.paid / и т.п. Можно вынести в модуль для удобства, например payment_processors.contracts / types или что-то вроде того.

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

import <module.name>;
или include <module.name>;
или require "<module.name>";

вы что никогда либы не подключали к проекту? скорее всего подключали.

как узнали про их интерфейсы? наверно из исходников и документации

Тут имеет место недопонимание. Я написал MВУ, в нем есть некий класс, в нем - поле. Оно имеет тип того самого интерфейса, на котором вы ставите акцент. Реализации пока нет - я хочу позаимствовать ее в Интернете. Нахожу стороннюю либу с нужным мне функционалом, естественно там свой интерфейс. Как быть?

Ваша идея будет работать только если вы сами поставляете tool, не готовый продукт. Но тогда, как вам и указали выше, вы просто поменяли МВУ и МНУ местами.

Тут еще вопрос с циклическими зависимостями. На уровне кода их нет. А вот на уровне модулей есть, если вы с МВУ захотите запустить IoC контейнер.

МВУ предоставляет интерфейс и разумеется вы обязаны его имплементировать. имплементация лежит в МНУ - вы его создаете под конкретный кейс в конкретном контексте (как именно реализовать интерфейс это out of scope и вообще не важно с использованием сторонних библиотек или без)

где тут цикл может возникнуть я не представляю: цикл МНУ -> МВУ -> МНУ ???

МВУ никогда не зависит от МНУ, иначе это нарушение целой пачки принципов, DIP в том числе

Модули не поменялись местами.

Но если в "МВУ" нет "ни одной строчки кода из МНУ", то... он его не использует.

Речь шла про зависимости. Если в модуле нет ни одной строчки кода, это означает, что он «не зависит», а не «не использует».

При этом поскольку ваш "МНУ" использует интерфейс из "МВУ", он... начал его использовать. У вас модули поменялись местами.

Здесь аналогично, речь про зависимости, а не «использование реализации»

Как сказано в статье:

Модуль верхнего уровня (далее МВУ) — это модуль, который в конечном итоге использует реализацию другого модуля

Обратите здесь особое внимание на слово «реализацию». Т.е., потребитель реализации как был МВУ, так им и остался.
Поменялись местами не модули, а зависимости, т.е., другими словами, зависимость инвертировалась, что нам и было нужно.

Здесь аналогично, речь про зависимости, а не «использование реализации»
Обратите здесь особое внимание на слово «реализацию».

Я бы в это поверил, если бы дальше в этом же определении не было:

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

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

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

Я бы в это поверил, если бы дальше в этом же определении не было:
модуль верхнего уровня — это модуль, который находится выше в «пищевой цепочке», т.е. потребляет (
имеет внутри себя) код другого модуля.

Речь шла об аналогии:

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

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

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

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

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

В вашей финальной реализации ваш "МНУ" неизбежно ссылается на "МВУ" (потому что в нем определен интерфейс), значит, в вашей терминологии он стал модулем верхнего уровня.

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

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

А к чему вы привязываете "верхний" и "нижний" уровни? Какая у вас формулировка термина?

Речь шла об аналогии:

Проблема с аналогиями в том, что они врут.

Какая у вас формулировка термина?

У меня или у Мартина? Моя формулировка не имеет отношения к делу, потому что я не использую "нижний" или "верхний" для DIP, меня это не волнует (и так, на самом деле, намного проще жить).

Мартин же изначально это никак не расшифровывает (для него просто очевидно, что операция копирования - это high-level policy, а примитивы чтения и записи - low level detailed modules). Позже, в Clean Architecture он это расшифровывает следующим образом:

A strict definition of “level” is “the distance from the inputs and outputs.” The farther a policy is from both the inputs and the outputs of the system, the higher its level. The policies that manage input and output are the lowest-level policies in the system.

Эта расшифровка полностью совместима с его примером в работе 1996 года.

Проблема с аналогиями в том, что они врут.

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

A strict definition of “level” is “the distance from the inputs and outputs.” The farther a policy is from both the inputs and the outputs of the system, the higher its level. The policies that manage input and output are the lowest-level policies in the system

Это формулировка совершенно не противоречит тому, что сказано в текущей статье про определение МВУ/МНУ, а наоборот, подтверждает корректность определений.

Следите за руками:

A strict definition of “level” is “the distance from the inputs and outputs.” The farther a policy is from both the inputs and the outputs of the system, the higher its level.

Переводим:

Строгое определение "уровня" — это "расстояние от ввода и вывода". Чем дальше политика от ввода и вывода, тем выше ее уровень.

Будем рассматривать "расстояние" только от ввода, т.к. для вывода все аналогично.
Представим ввод в виде функции input(). Функция input() является крайней в стеке вызовов, или выражаясь аналогией из статьи, является последней в “пищевой цепочке”.

Теперь, давайте введем две функции: потребитель() и потребляемый(). Из функции потребитель() вызывается функция потребляемый().
Т.е. потребитель() использует/вызывает/потребляет реализацию функции потребляемый().

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

Посмотрим на стек вызовов:

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

Ну и уже, надеюсь, очевидное из сказанного/показанного выше, которое просто переведем:

The policies that manage input and output are the lowest-level policies in the system.

Переводим:

Политики, управляющие вводом и выводом, являются самыми низкоуровневыми в системе.

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

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

Неа. Вполне можно объяснить термины без аналогий.

Функция input() является крайней в стеке вызовов

...а это вы с чего взяли?

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

Теперь, давайте введем две функции: потребитель() и потребляемый(). Из функции потребитель() вызывается функция потребляемый().
Т.е. потребитель() использует/вызывает/потребляет реализацию функции потребляемый().

А почему не может быть так, что "потребитель" вызывает и "потребляемого" и input?

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

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

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

Это не допущение. Это буквально написано в приведенной вами же цитате: The policies that manage input and output are the lowest-level policies in the system.
Т.е. input и output являются крайними в нашей рассматриваемой системе.
Даже базовый пример, который отображает работу программы, обычно изображают так: input - [программа] - output

А почему не может быть так, что "потребитель" вызывает и "потребляемого" и input?

См. выше

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

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

прикладной слой

бизнес-слой

прикладной слой вызывает бизнес-слой, и полученный результат выводит в консоль

Прикладной слой - вызывающий, но он же ближе к вводу/выводу.

Это буквально написано в приведенной вами же цитате: The policies that manage input and output are the lowest-level policies in the system.
Т.е. input и output являются крайними в нашей рассматриваемой системе.

"Крайние" только с точки зрения уровня в этом определении. Это не значит, что они крайние в цепочке вызовов.

Даже базовый пример, который отображает работу программы, обычно изображают так: input - [программа] - output

Как из этого примера понять, где тут цепочка вызовов?

См. выше

Не понимаю. Что "выше" запрещает мне написать вот так:

void Translate()
{
  var input = Console.ReadLine();
  var translation = Dependency.Translate(input);
  // ...
}

См. выше

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

void Application() //это прикладной слой
{
  var output = Domain.GetQuoteOfTheDay(); //это вызов бизнес-слоя
  Console.WriteLine(output);
}

Этот пример выражает одновременно все, что я сказал.

"В МВУ нет ни одной строчки кода из МНУ" - это достигается за счёт передачи в качестве зависимости (например через аргумент функции, аргумент в конструкторе) интерфейса, вместо непосредственно самого МНУ, поэтому в МВУ не будет строчек кода МНУ, а будет вызов метода интерфейса МВУ, т.е. строчек кода МВУ.

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

МНУ не используется интерфейс из МВУ, а реализует его, то есть зависит от абстракции - интерфейс.

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

Вот у вас есть интерфейс

IOrderRepository
{
  void SaveOrder(Order order)
}

И IOrderRepository, и Order определены в МВУ, потому что больше негде. Когда в МНУ вводится реализация Repository: IOrderRepository, она внутри себя неизбежно обратится к свойствам Order. Обращение к свойствам объекта - это использование, как ни крути. Это значит, что МНУ в этом примере использует МВУ. Но DIP не нарушен.

Так потому и инверсия, что поменялись местами.

Чтобы была инверсия, модули должны остаться теми же самыми, а поменяться только направление зависимостей. Я же утверждаю, что если пользоваться введенной в посте терминологией, модуль, содержащий А, станет "МНУ", модуль, содержащий Б, станет "МВУ", и "МВУ" продолжит зависеть от "МНУ".

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

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

Открываем википедию и видим ответ:

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

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

И где должен размещаться код этого адаптера?

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

И где должен размещаться код этого адаптера?

В модуле нижнего уровня

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

Речь не про какие-то сложные системы, речь про DIP.

Если DIP сильно усложняет систему, то никто не обязывает вас его использовать

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

Хм, ну если мы хотим изолироваться от сторонней библиотеки, то вызов ее методов нужно поместить в МНУ. Если изоляция (обертка над библиотеками) вызовов методов библиотек не нужна, то и DIP к ним применять не нужно.

Но почему? Я прям прошел и перепроверил, пример Мартина оперирует чтением с клавиатуры и выводом на принтер. Это типичный пример системной библиотеки.

Но почему? 

Не совсем понял к чему вопрос. Что почему? Я предыдущий комментарий отредактировал, поэтому, возможно, вопрос относится к старой его версии.

Да, мой вопрос относился к предыдущей версии, которая звучала как "DIP тут не при чем".

Хм, ну если мы хотим изолироваться от сторонней библиотеки, то вызов ее методов нужно поместить в МНУ.

Ну то есть у нас сначала библиотека была МНУ, а потом мы добавили еще один модуль, который стал новым МНУ, а библиотека, видимо, еще одним МНУ?

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

Мы хотим изолироваться не от библиотеки как таковой, а от конкретной реализации.

Ну то есть у нас сначала библиотека была МНУ, а потом мы добавили еще один модуль, который стал новым МНУ, а библиотека, видимо, еще одним МНУ?

Если я правильно понял ваш кейс, то схема будет выглядеть так:

Т.е. от добавления third-party библиотек реализация DIP не меняется.
Если не хотим видоизменять класс BClass, то в МНУ добавляем адаптер, который реализует AInterface и вызывает BClass.b(). В этом случает BClass останется нетронутым. Вообщем, с МНУ можем делать что захотим, главное, чтобы он подстроился под интерфейс AInterface, а как именно он это сделает уже дело десятое.

Нет, вы неправильно понимаете.

Исходный случай - это когда a() напрямую вызывает код в библиотеке. Самый простой пример (практически идентичный Мартиновскому) - это Console.ReadLine().

Да что там, вот код прямо из Мартиновской статьи. Вот это - исходный high level policy.

void Copy()
{
  int c;
  while ((c = ReadKeyboard()) != EOF)
    WritePrinter(c);
}

(возможно, из Мартиновского кода вам не очевидно, что ReadKeyboard и WritePrinter менять нельзя, именно поэтому в моем примере Console.ReadLine(), который поставляется в стандартной библиотеке)

возможно, из Мартиновского кода вам не очевидно, что ReadKeyboard и WritePrinter менять нельзя, именно поэтому в моем примере Console.ReadLine(), который поставляется в стандартной библиотеке

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


Что вы хотите сделать с приведенным вами кодом? Вы зачем-то хотите изолировать функции стандартной библиотеки ReadKeyboard() , WritePrinter(c) ?
Если да (хотя непонятно для чего), то нужно выносить эти ReadKeyboard() , WritePrinter(c) в обертку, и далее использовать стандартный механизм DIP (например, как на скриншоте выше в предыдущем комментарии)

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

Что вы хотите сделать с приведенным вами кодом? Вы зачем-то хотите изолировать функции стандартной библиотеки ReadKeyboard() , WritePrinter(c) ?
Если да (хотя непонятно для чего)

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

то нужно выносить эти ReadKeyboard() , WritePrinter(c) в обертку

Можно.

Но вопрос в том, насколько специфичной будет эта обертка (и ее интерфейсы). Я же не зря в самом начале говорил про достаточно сложные системы: в изолированном примере у меня есть только политика Copy, но в системе у меня будет еще и политика Timestamp (которая к каждой строке из ввода добавляет время, и выплевывает в вывод), и политика Split, которая будет брать один ввод и делить его на два вывода. Каждая такая политика - отдельный модуль, и они ничего не знают друг о друге.

Где будут абстракции? Сколько адаптеров нужно написать?

Опять же, вы говорите о решении какой-то частной задачи с применением DIP, а не о самом DIP.

Я, гм, говорю о задаче, которая приведена в фундаментальной статье про DIP как иллюстрация этого принципа.

DIP не становится плохим из-за того, что кто-то хочет его применить и это усложняет ему систему.

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

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

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

Я же не зря в самом начале говорил про достаточно сложные

DIP от сложности системы не меняется.

Где будут абстракции? Сколько адаптеров нужно написать?

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

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

Вы это поняли, потому что я сходу не определил, что код который вы привели указан в какой-то из статей?

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

DIP от сложности системы не меняется.

Принцип - не меняется. А то, как он применяется - меняется. Вы же не думаете, что у него ровно один вариант применения?

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

Это не является строгим требованием DIP (я уже приводил цитаты).

И именно потому, что это чрезвычайно избыточно, я и не рекомендую так делать.

Как часто вы внедряете библиотеки для которых нужно дополнительно еще внедрять какие-то абстракции (т.к. без них МВУ не будет работать)?

Каждый раз, когда начинаю проект?.. Вы, наверное, не слышали про Microsoft.Extensions.*?

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

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

Вы действительно не видите намеков на cross-cutting concerns, даже когда я прямым текстом про логирование написал?

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

Как я понимаю, инверсия зависимостей - это когда выполняемый в верхнем уровне код полностью определяется модулем нижнего уровня. Яркий пример: используем некий фреймворк, навешиваем событие OnClick, например, на кнопку и вставляем туда произвольный код.

Мне кажется, что вы понимаете неправильно.

Именно на этом примере я понял этот принцип

А разве этот пример вообще имеет отношение к этому принципу?

Да, этот пример 100% имеет отношение.

По первому вопросу: возможно да, я чего-то недопонимаю насчет общности этого принципа, но что-то и понимаю - по крайней мере, приведенный частный пример.

Да, этот пример 100% имеет отношение.

Что в этом примере модуль верхнего уровня, а что - нижнего, и почему?

Фреймворк - верхний уровень, потому что мы им пользуемся.

Фреймворк - верхний уровень, потому что мы им пользуемся.

Это очень странное определение, я никогда его не видел. Откуда вы его берете?

Слишком много вопросов. Пример про фреймворк я взял из переводной статьи, пытаясь понять суть этого DIP.

Слишком много вопросов.

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

Я перепутал: имелось ввиду IoC (Inverse of Control)... Тоже инверсия)

...но совсем другой принцип.

Тогда автор что-то преувеличил с 90%-тами непонимания и я на это клюнул)

Пример с навешиванием какой-то функции на событие OnClick у кнопки ничего общего с принципом DIP не имеет, это вообще частная реализации шаблона Observer, который решает совершенно другую проблему.

Возможно. Я не теоретик программирования)

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

Все верно, но только наоборот. Когда конфигурация мну определяется мну - это как раз прямая зависимость. Когда конфигурация мну определяется мву - обратная зависимость.

Да я перепутал DIP с IoC. Но возможно они связаны друг с другом 🙃

Принцип и реализация. Если не вдаваться в словоблудие об абстракциях - это одно и тоже.

90% разработчиков не понимают

Что всегда смущает в подобных утверждениях - это вот такие обобщения. При этом, если почитать дальше, выяснится, что автор скорее всего имел в виду 90% тех авторов, которые пишут про SOLID в интернете. Но автор, я хочу вам заметить, что не стоит этот вывод обобщать на всех разработчиков (которые на мой взгляд, где-то с уровня синьора вообще про такие вещи как SOLID не думают и статей не пишут, потому что им уже это не интересно).

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

Меня больше забавляется тот факт, что большинство вместо того, чтобы понять что/как/зачем/почему, заучивают как аксиому и потом без осознания используют.

Т.е. мало выучить принципы, нужно понять, как к ним пришли, что они решали.

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

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

Золотые слова! Без этого и получается что используют инструмент для решения несуществующих в данном месте проблем.

Я бы еще сказал - и в данное время. Потому что необходимость упростить сопровождение и понимание кода сильно зависит от того, кто понимает и сопровождает. Ну т.е. для синьора и джуна один и тот же код может быть легким для понимания, и совсем недоступным (как из-за уровня знания инструментов/языков, так и уровня понимания текущего проекта).

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

И если вы пишете проект в одно лицо

В этом случае можно очень много углов срезать)

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

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

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

Хотя по-хорошему, стоило бы прикинуть, ну вот мы тут соблюли принцип, нам это стоило скажем 2 человеко-дня, а если бы мы его не соблюдали, модификация этого куска кода стоила бы нам дополнительно человеко-день 

Согласен, но такого я не встречал к сожалению.

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

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

требование определение интерфейса на том же (как написано в статье) или более высоком уровне (позволяет делать общие интерфейсы)

Хм. Тот же вопрос, что и выше: какой уровень вы считаете более низким, а какой - более высоким, и почему?

Берём чистую архитектуру:

Самый высокий уровень - entity (если вы его используете)

Затем usecase он зависит от entity. Фактически тут определяется львиная доля бизнес логики.

Затем внешнее взаимодействие - самый низкий из 3х основных. Для уточнения это уровень внешнего представления и репозиториев. Он зависит от usecase и entity. Именно на этом уровне могут появляться зависимости от Фреймворков и прочей гадости, которые нужно уметь менять как перчатки.

Самый высокий уровень - entity (если вы его используете)

Но почему это высокий уровень? Скажем, в статье, которую мы обсуждаем, направление ровно обратное: если usecase зависит от entity, то usecase - это модуль более высокого уровня, чем entity.

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

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

Я приведу простой ограниченный пример:

Мне нужно сделать метод приёма заказа:

1) я определяю на русском языке понятные для бизнеса параметры на вход, параметры на выход, порядок действий.

Например: на вход пользователь, список товаров с количеством, адрес и дата доставки, на выход идентификатор заказа; порядок действий: сгенерировать идентификатор, сохранить в хранилище заказ (модель заказа) и наконец выдать ид заказа наружу

2) оцифровываю их в виде юзекйса, в котором я описываю 2 интерфейса: для генерации ид и сохранения заказа в хранилище и две структуры входной и выходной параметр. Попутно я пользую модель заказа из энтити.

Так же я делаю сам юзкейс в котором будет единственный метод "сохранить заказ"

3) И наконец я делаю реализацию сохранений и генераций. А так же внешнее апи

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

Это вопрос зависимости с точки зрения посторонняя бизнес логики.

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

Я пытаюсь понять, какой конкретно критерий говорит, что entity - это верхний уровень.

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

Буква D в солид говорит, что так не надо, а надо наоборот, в начале писать общее абстрактное а потом на него накручивать реализацию.

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

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

Непонятно ни про многих ни про зависимость. Кажется ровно наоборот.

Непонятно ни про многих ни про зависимость. Кажется ровно наоборот.

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

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

Модуль нижнего уровня (далее МНУ) — это тот модуль, реализация которого используется другим модулем.

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

Тут хорошее определение: использует, а не зависит.

Что такое "использует"?

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

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

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

Тут появляется очень много имхо ненужной конкретики: инфраструктура, геттеры, конструктор. Они имхо к сути не имеют никакого отношения.

Не понимаю сути вопроса.

Не понимаю сути вопроса.

Суть вопроса очень проста: как формально определить, какой модуль "выше", чем другой.

Тут появляется очень много имхо ненужной конкретики: инфраструктура,

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

Использует ли слой "внешнего взаимодействия" слои use-case и entity? Опираясь на ваше же определение ниже, "обращается" ли слой внешнего взаимодействия к слоям use-case и entity?

Что такое формально определить?

Что хочется услышать на это? Есть ли непротиворечивый пример?

Суть вопроса не понятна.

Ибо выше и ниже чётко определяется в статье.

Что такое формально определить?

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

Есть ли непротиворечивый пример?

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

Ибо выше и ниже чётко определяется в статье.

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

Кстати, само по себе слово "использует"-то понятно. Проблема в том, что в приведенном вами выше примере средний слой (use-case) "использует" и entity, и внешнее взаимодействие. И это само по себе нормально, проблема только в том, что в этот момент нельзя употреблять слово "использует" для отношения "выше-ниже" (или надо признать, что этот слой "выше" остальных).

В теле сатьи есть определения МВУ и МНУ. Они чётко дают понимание терминов выше и ниже.

Кажется мы разговариваем о разном. Я предлагаю ещё раз вернуться к определениям и понять в них есть слово использует но нет слова зависит.

А вот зависимость кодовой базы - это уже применение принципов скрываемых за идеей, например за SOLID.

В теле сатьи есть определения МВУ и МНУ. Они чётко дают понимание терминов выше и ниже.

Для меня - не дают. Потому что они опираются на слово "используют", которого я в этом контексте не понимаю, и поэтому и спрашиваю на конкретном примере:

Самый высокий уровень - entity (если вы его используете)
Затем usecase он зависит от entity. Фактически тут определяется львиная доля бизнес логики.
Затем внешнее взаимодействие - самый низкий из 3х основных. Для уточнения это уровень внешнего представления и репозиториев.

В этом конкретном перечислении

  • usecase использует entity, или нет?

  • usecase использует внешнее взаимодействие, или нет?

Тут есть зависит. В в данном случае зависит (имеет зависимость) близко по смыслу к включает, наследует использует кодовую базу непосредственно. Не путать с использует реализацию.

На примере юзекйса и внешнего взаимодействия (конкретных):

Внешнее взаимодействие зависит от юзкейса.

Юзкейс может использовать реализации из внешнего взаимодействия

Тут есть зависит.

Я явным образом спрашиваю про "использует". Вот у вас есть пример с тремя слоями:

Самый высокий уровень - entity (если вы его используете)
Затем usecase он зависит от entity. Фактически тут определяется львиная доля бизнес логики.
Затем внешнее взаимодействие - самый низкий из 3х основных. Для уточнения это уровень внешнего представления и репозиториев.

если мы возьмем определение из поста, в котором есть слово "использует"

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

и применим это слово "использует" к вашему примеру, какие будут ответы на два этих вопроса:

  • usecase использует entity, или нет?

  • usecase использует внешнее взаимодействие, или нет?

Юзкейс может использовать реализации из (читай использует) внешнего взаимодействия

Upd Модель основана на чистой архитектуре. Если она Вам не знакома, то увы объяснение терминов может свестись к некому описанию чистой архитектуры. Что не очень продуктивно.

Usecase использует entity, или нет?

Usecase зависит от entity.

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

Нет usecase зависит от entity и использует реализации из внешнего взаимодействия.

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

Нет usecase зависит от entity и использует реализации из внешнего взаимодействия.

Если в вашей терминологической системе usecase не использует entity, я, честное слово, не понимаю, что вы обозначете этим словом, и как это соотносится с бытовым пониманием.

(Потому что "очевидно что" когда usecase "использует кодовую базу непосредственно" (ваши слова), он неизбежно "использует реализацию" тоже ваши слова. Вот в обратную сторону возможны варианты. А в эту - нет.)

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

Я увидел что Вам не понятна разница между использованием реализации [интерфейсов] и зависимостью, то есть неким аналогом наследования, использования кодовой базы непосредственно.

Это тема кажется не интересной к обсуждению так как относится или к смешению контекста или глубинному не пониманию абстракций.

Прошу прощения если задел.

Я увидел что Вам не понятна разница между использованием реализации [интерфейсов] и зависимостью

Мне-то она понятна. Но я боюсь, что то, что мне понятно, отличается от того, что понятно вам.

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

Нет он от него зависит

Нет он от него зависит

Цитирую ваш же текст (выделение мое):

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

Это слово означает что-то другое, не использование?

Просто если я пойду к произвольному знакомому мне разработчику и покажу ему вот такой код

namespace UseCase
{
  void CreateOrder(string customer)
  {
    var order = new Entity.Order(customer);
  }
}

я услышу в ответ, что UseCase.CreateOrder использует Entity.Order и зависит от него же.

В вашей терминологической системе не так?

Читай комментарии сверху они дают различие в терминологии.

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

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

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

Давайте немного расширим пример.

class OrderCreationUseCase(IOrderRepository repository)
{
  void CreateOrder(string customer)
  {
    var order = new Entity.Order(customer);
    repository.Save(order);
  }
}

OrderCreationUseCase не пользуется результатами деятельности new Entity.Order? Или не пользуется результами деятельности repository.Save?

С точки зрения кода (а DIP - он про код, про отношения модулей) эти отношения идентичны, разве нет?

Нет не идентично.

Ибо суть понята не до конца верно.

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

Нет не идентично.

В каком-то из двух случаев не используются результаты деятельности?

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

Тут не понятно что имеется ввиду? Каких случаях? И просьба задать вопрос без кода, так как код должен являться результатом применения модели / принципа, а не источником.

Код сверху не описывает зависимости, так что плохо применим к описанию проблемы.

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

Каких случаях?

Упомянутых в коде и в последующем вопросе к нему.

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

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

Код сверху не описывает зависимости, так что плохо применим к описанию проблемы.

Да нет, в нем как раз все зависимости прекрасно видны.

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

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

@liarЯ изложил свою точку зрения в комментарии по ссылке: https://habr.com/ru/articles/872078/#comment_27755320. Он получился довольно объемным, поэтому я решил не вставлять его в ваш диалог. :)

Использует - это базовое слово.

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

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

Я использую легковое такси. Как способ доставки моего товара от подъезда до подъезда.

Я не завишу от марки и модели автомобиля который перевезёт мой товар. Не завишу ни от чего внутри автомобиля.

Я определил интерфейс - перевезти. И его использую.

Но от его реализации я не завишу.

upd Как дополнение. В момент перевозки я использую конкретный экземпляр такси. Который предоставлен мне конкретным провайдером. Которого я могу сменить без влияния на результат.

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

Верно в примере такси зависит от нашей потребности.

Рассмотрим второй вариант:

Мы говорим в интерфейсе, что нужно сделать перевозку.

Есть внешний агрегатор такси. Не зависящий от нас

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

Для меня как для заказчика (юзкейса) абсолютно не важно какой размер груди у секретаря или какие мозоли у логиста. Главное что есть кому сказать отвези.

Из этого примера видно, что слой внешнего взаимодействия зависит от слоя юзкейсов и от внешнего АПИ одновременно.

Ну вот и получается, что у вас интерфейс в отдельном слое, а не в вашей квартире (модуле).

Только это не соответствует диаграмме в статье.

В статье были введены ровно две сущности (МВУ и МНУ), обозначена иерархия между ними ( словами "верхний" и "нижний") и задано ровно одно отношение ("зависит").
Вы зачем-то ввели новый термин ("использует") и стали топить за то, что это как-то влияет на иерархию сущностей. @lair интересовала только иерархия, но меня - только необходимость новой терминологии. И когда я попросил Вас разъяснить эту терминологию, вы внезапно вводите третью сущность, "интерфейс".
Не надо плодить сущности без необходимости, как говорил Оккам.
Чтобы не затягивать, я сам переформулирую вопрос из статьи и вопрос @lairиз комментариев. И, чтобы всем было проще понять, в чём суть, первая сущность так и останется "модулем", а вторая станет "интерфейсом".
Итак, начальное утверждение:
"Модуль в своём коде использует интерфейс"
и два вопроса:
1) можно ли сказать, что "использует" и "зависит" в данном случае синонимы?
2) кто из них по иерархии расположен выше - модуль или интерфейс?

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

Во-вторых: очевидно, что интерфейс расположен по иерархии выше, чем модуль. Иначе будет нарушен первый пункт из DIP.

А если вернуться к иллюстрации из статьи (той, которая с зелёной галочкой), то видно, что она неправильная. Правильно будет так: где-то наверху интерфейс, он же "модуль высокого уровня", а ниже его - то, что обозначено как "A" ( потому что использует интерфейс) и то, что обозначено как "B" ( то, что использует интерфейс для реализации этого самого интерфейса), два "модуля нижнего уровня" по отношению к интерфейсу. Между "A" и "B" иерархии нет, они независимы.

Ну собственно, даже в англ википедии так и написано (а оно вроде как цитата из Martin, Robert C. (2003). Agile Software Development, Principles, Patterns, and Practices):

In conventional application architecture, lower-level components (e.g., Utility Layer) are designed to be consumed by higher-level components (e.g., Policy Layer)

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

То есть, то, что "выше", потребляет (consumes) то, что ниже?

Распространяется ли это отношение на абстракции; иными словами, можно ли сказать, что модуль A потребляет (consumes) некую абстракцию из модуля Б (и в этом случае модуль А потребляет модуль Б)?

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

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

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

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

Это определение буквально говорит о том, что top-level в любом приложении — это int main(int argc, char** argv)

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

НЛО прилетело и опубликовало эту надпись здесь

Принципы SOLID лучше понимать в обще-математическом смысле, потому что языки программирования бывают разные.

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

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

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

При классической схеме core-infrastructure-application интерфейсы действительно оказываются в core, т.е. переезжают на верхний уровень с нижнего - инфраструктуры.

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

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

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

Хидеры библиотек несомненно являются частью библиотеки. И принцип инверсии не применим - для АПИ библиотеки не существует вызывающей стороны, определяющей АПИ.

Но стоит вспомнить, что все библиотеки надо где-то объединить и использовать - все встанет на место.

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

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

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

Но все же, кто захочет такой оверинжиниринг на ровном месте, когда фасад может получиться толще полезного функционала, если можно в main.cpp жахнуть #include <openssl.h>.

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

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

Не разводить ООП с фасадами, интерфейсами, реализациями и DI для выбора какой-то из них, а просто вынести куда-то методы для работы с ней и вызывать библиотеку только в них.

Во-первых, вы только что описали фасад.

Во многих случаях реализация одна-единственная и обновление/замена библиотеки сведётся к изменению только кода этих методов.

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

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

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

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

Только если ваш фасад не статический. К сожалению, обычно люди, которые "без интерфейсов, реализаций и DI", делают статический.

И вдобавок далеко не для всего кода есть смысл всё строго-канонично разносить

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

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

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

Используя любые стандартные библиотеки мы очень часто нарушаем принцип

Я бы сказал всегда.

Модуль может быть "заражён" зависимостями, но дальше он их не распространяет, если закрыт DIP.

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

Тогда в модуле с бизнес логикой вы описываете какой-то общий интерфейс для работы с ФС. Для текущего примера в интерфейсе может быть только один метод - SaveFile(path, data). Для каждой ОС Вы делаете отдельный модуль. В каждом из которых реализуете интерфейс бизнес слоя вызывая нужные функции ядра ос.

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

Роберт Мартин в своей книге Чистая Архитектура разбирает похожий сценарий.

Первые 90% знают, что не понимают.
А остальные 10% думают, что понимают, но тоже не понимают.

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

у меня топографический критинизм и хотелось бы пояснений..

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

если все так, то выглядит вроде красиво по отношению к тому, что названо МВУ..

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

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

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

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

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

Тут я хочу тогда спросить, а какую проблему решаем?

вот в чем вопрос..

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

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

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

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

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

Или вы предлагаете сделать 3-ий модуль в котором лежит только эта абстракция и оба других модуля зависят от этого третьего?

Не я, а описание принципа.

Тут я хочу тогда спросить, а какую проблему решаем?

Оригинальное описание принципа решает проблему "невозможно переиспользовать А в разных ситуациях (с разными реализациями Б)".

Лично я с помощью этого принципа еще решаю чисто технологические вещи типа "А лежит в пакете Х, А нужно использовать Б, Б лежит в пакет У, Б нужна функциональность из пакета Х - циклическая зависимость пакетов".

Мне кажется, что не существует бизнес проблем которые формулируются как

невозможно переиспользовать А в разных ситуациях (с разными реализациями Б)

Бизнес проблемы они больше про - стало больше пользователей чем раньше и система не тянет по производительности. Наши инженеры посмотрели и сказали что модуль Б не вывозит и его надо заменить на модуль Ц. Или наши заморские партнёры по доброте душевной больше не берут наши тугрики и надо заменить модуль Б на модуль Ц. Или наше отечественное руководство заботится о нашем бизнесе и наших пользователях и настоятельно просит использовать модуль Ц из белого списка вместо модуля Б. Или модуль Б был написан ещё древними греками по заветам самого Зевса, но последний сервер способный исполнять этот код запланирован в утиль и надо заменить на модуль Ц который может работать на наших серверах.

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

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

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

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

Это не значит что ДИП не нужен никогда это значит, что он нужен ИМХО только тогда когда для решения бизнес задачи вам надо иметь более одной реализации т.е. вам нужно использовать и модуль Б и модуль Ц вот тогда и нужно внедрять эти абстракции. Да я знаю есть ещё юнит тесты опять же ИМХО для бизнеса нужны не они, а функциональные тесты которым наличие абстракций ни как не помогает. Я сторонник фэйкания внешних зависимостей вместо мокания внутренних реализаций. Юнит тесты имхо идеальны для библиотек решающих конкретную инженерную проблему, а для бизнес логики/приложений предпочтительнее тесты функциональные.

Да конечно это все философия, стилистика, субъективная вкусовщина, но ведь статья про солид, а что это если не философия?

ПС если у вас 2 модуля рекурсивно зависят друг от друга то их разделение через 3 модуль не всегда то что нужно. Возможно их объединение в один модуль будет иметь больше практического смысла чем разделение любой ценой ради соблюдения каких то других философских принципов.

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

Мне кажется, что не существует бизнес проблем которые формулируются как

невозможно переиспользовать А в разных ситуациях (с разными реализациями Б)

Это зависит от того, чем занимается ваш бизнес. Если, например, он продает системы управления продажами, то может быть бизнес-задача "хочу использовать одну и ту же систему с разными службами доставки". Это типичное "переиспользуем А" (юз-кейс продажи) с разными Б (службами доставки).

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

Вы не поверите, но система, в разработке которой я участвую, реально позволяет работать с двумя разными СУБД (а сейчас еще и третью добавляют). Это было непросто сделать, но это сделали. И нет, БД там не избыточна.

Да я знаю есть ещё юнит тесты опять же ИМХО для бизнеса нужны не они, а функциональные тесты которым наличие абстракций ни как не помогает.

Это не больше чем YHO. Есть немало сценариев, когда юнит-тесты (или компонентные тесты) весьма и весьма эффективны.

Я сторонник фэйкания внешних зависимостей вместо мокания внутренних реализаций.

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

Cделать 3-ий модуль в котором лежит только эта абстракция и оба других модуля зависят от этого третьего?

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

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

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

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

Неа. У автора интерфейс находится внутри модуля, поэтому тот модуль, который его включает, от этого интерфейса не зависит. Вот потребитель (некий A) - зависит. А модуль, в котором A описан (МВУ) - нет.

Оба модуля при добавлении абстракции/контракта/интерфейса в МВУ, после которого МВУ не зависит от МНУ,  будут зависеть от абстракций по определению. МВУ будет зависеть от собственного интерфейса, а МНУ от интерфейса МВУ.

МВУ будет зависеть от собственного интерфейса

В отношении "зависит от" нельзя смешивать объекты разной гранулярности. Модуль может зависеть только от модуля. А от интерфейса может зависеть только реализация.

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

модуль не может зависеть от абстракции

Может, если абстракция - тоже модуль.

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

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

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

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

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

Именно так.

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

Адаптер это код от которого никто не должен зависеть. Ни код вашего проекта ( потому что DIP) ни код сторонних библеотек (потому, что они сторонние).

Адаптер требует немного бойлерплейта, практически нулевых мозговых усилий. И несёт много полюсов:

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

  • Тестирование с заглушками становится легким (просто делается заглушка для адаптера)

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

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

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

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

Почему никого ге смущает, что модуль нижнего уровня зависит от модуля верхнего уровня?

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

Никогда не встречал такого на практике, чтобы это было не костыльно. В каком-то ява-подобном языке, это выглядело бы как:

  • модуль A

  • модуль B

  • интерфейс реализуемый модулем А, который дергает модуль B

  • интерфейс реализуемый модулем B, который дергает модуль A

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

А в 95% случаев во всех системах что я видел - один интерфейс, который реализуем модуль B для дерганья из модуля А. Это если мы говорим об "уровнях" в архитектуре, которая действительно иерархически построена, как то: база > ентити > репозиторий > контроллер.

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

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

Никогда не встречал такого на практике, чтобы это было не костыльно.

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

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

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

Так вы когда интерфейс имплементируете, вы его указываете: FileLogger implements ILogger, а раз у вас ILogger обьявлен, скажем, в модуле покупок, то вы в верху пишете import ILogger from orderprocessingmodule. Так?

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

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

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

Почему никого ге смущает, что модуль нижнего уровня зависит от модуля верхнего уровня? Зачем ему это?

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

А то, что изменение верхнего модуля расползается по всему проекту - это не важно? Ведь тут точно такая же проблема, но в обратную сторону.

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

Или любой другой, в которой бизнесовой логики быть не должно.

Правильное разделение по слоям тоже важно, но это не относится к инверсии зависимостей.

А то, что изменение верхнего модуля расползается по всему проекту - это не важно? Ведь тут точно такая же проблема, но в обратную сторону.

Как изменения в верхнем модуле могут по всему проекту расползтись?

Я еще раз обращаю внимание что это специфика плюсов, о чем кстати Мартин сам и писал изначально. А потом не стал, видимо решив что на одних плюсистах много не заработать)

Правильное разделение по слоям тоже важно, но это не относится к инверсии зависимостей.

Не относится. Но нарисованная схема ломает это разделение.

Как изменения в верхнем модуле могут по всему проекту расползтись?

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

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

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

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

Наша реализация теперь не только от абстракций зависит, но и от остальной кодовой базы.

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

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

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

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

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

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

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

но даже в мартиновской схеме этого

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

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

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

Ну вот в том и дело, что "дефолтная" трехслойная архитектура изолирует нижний слой от верхнего

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

Вот из Фаулера (выделение мое)

A simple script to pull data from a database and display it in a Web page may all be one procedure. I would still endeavor to separate the three layers, but in that case I might do it only by placing the behavior of each layer in separate subroutines. As the system gets more complex, I would break the three layers into separate classes. As complexity increased I would divide the classes into separate packages.

А вот из того же Фаулера (выделение мое), про зависимости

Together with the separation, there’s also a steady rule about dependencies: The domain and data source should never be dependent on the presentation. That is, there should be no subroutine call from the domain or data source code into the presentation code. This rule makes it easier to substitute different presentations on the same foundation and makes it easier to modify the presentation without serious ramifications deeper down. The relationship between the domain and the data source is more complex and depends upon the architectural patterns used for the data source.

Вызывать код нельзя, но про реализацию интерфейса ничего не сказано)

Но зато сказано вот что

In this scheme the higher layer uses various services defined by the lower layer, but the lower layer is unaware of the higher layer

Понять можно и так и сяк. Со слоем данных там еще интереснее. Да и вообще говоря "слои" в "трехслойном" приложении Файлера это не совсем "модули" Мартина. У Мартина это

all well-structured object-oriented architectures have clearly defined layers, with each layer providing some coherent set of services though a well-defined and controlled interface

А еще же есть cross-cutting concept, пятислойная архитектура (не одна) и прочие гексагоны.

просто заинжектите через интерфейс реализацию и радуйтесь

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

Думаю, мы пришли к единому мнению)

Вызывать код нельзя, но про реализацию интерфейса ничего не сказано)

Мне кажется ваш собеседник пытается объяснить следующее:

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

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

  3. теперь любой разработчик в, например, репозитории может заюзать классы и методы сервиса с бизнес-логикой и никак вы это не запретите

Мне кажется ваш собеседник пытается объяснить следующее:

Это понятно, я к тому что если разбираться в деталях, то могут быть весьма разные ситуации. ЕМНИП в яве есть модификаторы доступа на уровне пакетов, в дотнете только на уровне сборки, в каком-нибудь php свои заморочки.

теперь любой разработчик в, например, репозитории может заюзать классы и методы сервиса с бизнес-логикой и никак вы это не запретите

Является ли это проблемой? Я работал с проектами где разделение на слои было на уровне папок и неймспейсов, все было в одной сборке. Проблем от этого не было никаких. ИМХО если люди захотят сделать костыль они его сделают, не одним способом, так другим)

ИМХО если люди захотят сделать костыль они его сделают, не одним способом, так другим)

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

Я работал с проектами где разделение на слои было на уровне папок и неймспейсов, все было в одной сборке. Проблем от этого не было никаких.

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

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

Если это одна сборка (речь о дотнете), то ничего не мешает использовать класс из соседней папки)

Легкость доступа может привести к тому, что люди не хотели сделать костыль, но сделали. 

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

Если это одна сборка (речь о дотнете), то ничего не мешает использовать класс из соседней папки)

Так-же как никто не мешает нарушать слои любой архитектуры. Просто с неймспейсами будет проще это контролировать.

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

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

А если нет? Разбивать проект на десяток-другой сборок просто потому что кто-то может переиспользовать класс не там где надо?

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

Можно парсингом кода найти, что в файле импортируется другая папка и проверять это на CI.

Например в golang можно аргументы командной строки парсить там, где они нужны. Собирать все вызовы по системе это еще то удовольствие. Чтобы все аргументы были в одном месте на одном из проектов добавили скрипт на CI: который запрещал иморт модуля flags везде короме разрешённых файлов.

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

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

При взаимодействии двух сущностей, одна будет зависеть от другой в любом случае. В DIP нет ни слова про то, что МНУ не должен зависеть от МВУ.
В DIP в исходном (“некорректном”) случае МВУ зависел от МНУ, но после применения принципа зависимость инвертировалась, и логично, что МНУ стал зависеть от МВУ.

В книге «Чистая архитектура» в 19 главе в завершающем абзаце вводного раздела сказано:

In a good architecture, the direction of those dependencies is based on the level of the components that they connect. In every case, low-level components are designed so that they depend on high-level components

Переводим:

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

Просрали профессию. Где-то мы свернули не туда.

интерфейс должен принадлежать МВУ

Можете объяснить, что значит принадлежать?

Как я понял из статьи и собственного применения, читай что можно его (описание интерфейса) расположить в одном файлике/папке/неймспейсе (зависит от языка) с МВУ

А МНУ будет подключать/наследовать МВУ для получения описание интерфейса.

Я в одном файле могу что угодно написать, мне нужно понять функциональную зависимость, которая понимается под этой фразой. МВУ реализует этот интерфейс? Судя по картинкам, наоборот получается.

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

Можете объяснить, что значит принадлежать?

Значит, что интерфейс должен физически находиться в границах МВУ

А если у нас 2 модуля верхнего уровня зависят от одного нижнего модуля, в границах какого модуля лежит интерфейс, который реализует нижний модуль?

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

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

Если в ЯП реализация не указывает явно реализуемый интерфейс (как в go), то без специальных средств в ide просто невозможно получить список методов-реализаций, которые могут вызываться через метод интерфейса. А это есть далеко не во всех IDE, и ни в каких текстовых редакторах.

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

Недостатки концепции DIP мне очевидны.

А какие она даёт преимущества?

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

А зачем?

А какие она даёт преимущества?

Возможность переиспользовать одного и того же потребителя с разными реализациями.

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

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

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

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

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

Это где такие костыли с интерфейсами?

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

Это где такие костыли с интерфейсами?

Например в Magento

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

В PHP есть интерфейсы. Инструкция, что класс реализует интерфейс - прекрасно работает. Но для того чтобы пойти в обратную сторону и узнать кто реализует интерфейс - в фреймворке Magento используется XML конфиг.

Видимо @AlexTest говорит про DI-Container используя слово конфиг. Пример фреймворка - например SPRING

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

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

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

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

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

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

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

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

Однозначно, я не упомянул все варианты использования. Но делать интерфейс ради интерфейса - это тоже плохая затея. Но и причин их использовать - не мало.

runtime = время выполнения

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

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

Например для интерфейса работы с БД во время выполнения программы используется только одна конкретная реализация, зависящая от типа БД с которым работает программа, например только для MySQL или только для PostgreSQL.

Еще пример : допустим программа управления использует интерфейс самолета, если она запускается для управления Боингом то используется только его реализация, а если программа запускается для управления Эмбраером - то используется только его реализация, но никак не две одновременно.

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

Да нет, это совершенно нормальная ситуация. Это же книжное описание полиморфизма (одного из).

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

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

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

Ааа! Вот DI-контейнер мне уже понятен, а то конфиги запутали)

Это не костыли, это фреймворк по инекции зависимости.

В любой IDE(с нормальной поддержкой LSP) есть "Go to Implementation", включая текстовые редакторы типа Vim и Emacs, так что найти имплементацию проблемой не является.

И к какой из десятка имплементаций оно улетит? Или ЛСП корректно весь стек разбирает, когда, например парой уровней выше была функция выбора имплементации в зависимости от переданных извне параметров?

И к какой из десятка имплементаций оно улетит?

Выводит список имплементаций интерфейса и выбираете что смотреть. Другое дело что если криво сделана фабрика/DI бывает сложно понять как конкретная имплементация выбирается в рантайме.

Как выше уже написали, открывается окошко со списком всех имплементации.

то эту реализацию невозможнопереиспользовать в другом проекте


Так делайте внешнюю библиотеку. Только тогда DIP не применим.

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

Переиспользование кода с точки зрения надёжности это плохо.

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

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

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

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

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

Переиспользование внутреннего кода это дополнительная работа и ответственность. Это шанс сломать всё одним изменением.

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

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

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

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

Анонимное собрание джавистов объявляется открытым =)

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

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

SOLID - это конечно не фундаментальные законы разработки, а скорее красивый стишок с советами, это да. Но в целом, код в духе SOLID становится проще а не сложнее, если конечно его применять там где он действительно нужен. Т.е. там где есть некая реальная сложность, а не высосанная из пальца. И собственно, какие именно ненормальные требования предъявляет SOLID? Что в интерфейсах нехорошо тянуть все джунгли за бананом? Что наследованием нужно пользоваться правильно (или вообще не пользоваться)? Что хорошо иметь интерфейсы и возможность подменять классы, хотя-бы для тестирования? Что код в который надо добавлять лучше кода который надо переделывать? Что лучше не смешивать слишком много разного в одном классе? Что из этого оверкилл, если задача достаточно сложная?

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

Это действительно неплохо, но причем тут именно SOLID?

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

SOLID по сути требовал только добавить сколько-нибудь абстракные интерфейсы

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

Например, добавил я интерфейс чисто для тестов, чтобы замокать можно было. Никакой инверсии зависимостей нету, OCP в мартиновском виде нету, это разве будет SOLID? Так можно далеко зайти, сделал что-то для улучшения кодовой базы - применил принцип SOLID! Пусть даже просто вынес в отдельную функцию повторяющийся код.

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

Если я написал консольное приложение Hello word!, то нет смысла показывать на него пальцем и говорить что оно не SOLID, потому что в нем просто нет проблем, которые решает SOLID. Если я искуственно усложнил это приложени настолько, чтобы там отражались принципы SOLID, то это не значит что я "использовал SOLID", это значит я просто страдал фигней.

Тут согласен) Ставить во главу угла SOLID, как набор важнейших принципов, я бы тоже не стал.

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

Он сочинил абзац текста с миссией. Потом сочинил цель компании. Миссия слегка противречила цели. Деде в раж вошел, сочинил три заповеди, семь принципов, 6 "столпов", 12 мудростей. Все они либо повторялись, либо противоречили друг-другу. Не успокоился, заставил все учить наизусть, а потом начал собирать семинары, где давал многочасовые толкования типа, почему в трктьей заповеди написано " Главное - это приюыль", а в шестом "столпе" Что главное - это клиент, а в третьей мудрочти, ято главное - как директор сказал. Семинары были в пятизвезжочных отелях в других странах, поэтому все с удоволбствием его слушали.

Блин, но никто всерьез даже не думал к ним относиться! И вот я как в прошлое окунулся, про толкование 5-го принципа, только без отеля)

Ssolid, три пиинципа ооп - это все бесполезная хрень для собеседований, а теперь оказалось что ее еще никто и не понимает)

Ну и да. компания реальео держалась на одном жирном клиенте, когда он отвалился, всех уволили одним днем.

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

Спасибо) именно это я хотел сказать)

Написанное тут остальными 10% показывает, что это действительно какая-то писаная торба.

Очередной совет мышам стать ежиками

Если какую-то модель не понимают 90% потребителей, может, что-то в модели поправить? И перестать носиться с ней как писаной торбой?

И перестать носиться с ней как писаной торбой?

Стоит только вспомнить Clean Code, как раньше все относились к нему как к священному писанию, а сейчас по большей части это джуновский говнокод.

Вот пример рабочего кода PHP, объясняющий D:

D - Dependency Inversion Principle 
D - Dependency Inversion Principle 

Класс EmailNotifier зависит от абстракции Notifier (имплементирует);
Класс UseRegistration тоже зависит от абстракции Notifier (получает в свой конструктор);
Между собой эти два класса не связаны и не зависят от друг-друга.

Может быть правильнее было бы назвать этот принцип принципом разрыва зависимостей, Dependency Breaking Principle?

Напрашивается реклама: Программируешь сложную архитектуру? Течет мозг?.. Dependency Breaking Principle! Стань архитектором своей жизни: разорви всё и будь счастлив!

Может быть правильнее было бы назвать этот принцип принципом разрыва зависимостей, Dependency Breaking Principle?

Мартин в свое время объяснял, почему этот принцип назван так. С тех пор уже прижилось.

Вы как раз привели пример нарушения принципа DIP.

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

Просто прокинуть куда-то интерфейс - это не про инвертировать зависимость

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

В определении DIP такого прямого требования нет. Но принадлежность общего интерфейса к более верхним уровням логично возникает сама-собой.

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

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

Если Notification не принадлежит модулю c UserRegistration

Это вы сами предположили. Давайте считать, что принадлежит.

Это вы сами предположили. Давайте считать, что принадлежит.

Это логично вытекает из вашего примера кода. Т.е. это не я предположил, а вы показали в коде через нейминг сущностей.
Но, если допустить, что вы просто неудачно подобрали имя для интерфейса и назвали его общим именем Notification , но методы этого интерфейса будут вызываться только из МВУ, где есть сущность UserRegistration и больше нигде, то да, тогда все корректно (но опять же, нейминг интерфейса намекает на обратное).

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

Мы как будто на разных языках говорим.

 но методы этого интерфейса будут вызываться только из МВУ, где есть сущность UserRegistration и больше нигде, то да, тогда все корректно

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

Про то, что "90% не понимают" это враньё. А вот про 90% не применяют в реальных проектах - вот это запросто.

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

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

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

А зачем разбивать на модули то, что естественным образом на модули не бъется и придумывать себе правила которые слишком сложно и накладно соблюдать? Лучше применять SOLID там, где это разбитие на модули нужно как воздух, и применять SOILD для того, чтобы выполнить это разбитие эффективно.

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

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

Например, когда говорят, что класс A зависит от класса B, то это означает, что в классе A физически находится код из класса B и мы не можем класс A вынести, условно, в другой проект, так как внутри него содержится/вызывается код, который объявлен в другом классе/файле и если мы хотим вынести класс A в другой проект, то должны вместе с ним «тянуть» за собой и класс B.

В такой формулировке у меня в классе A объявлен экземпляр класса B. Что мешает мне вынести класс B в другой проект/модуль, а в файле класса A добавить import? Это работает даже без абстракций, разве что может потребоваться изменить модификаторы доступа в классе B.

При анализе своего кода на предмет использования принципа инверсии зависимостей (DIP) я выявил несколько способов его применения.

Тип 1: Интерфейс находится на том же уровне, что и "верхний модуль".

Примером может служить следующая строка кода для конфигурации ASP.NET Core.

services.AddSingleton<IControllerFactory, MyCustomControllerFactory>();

Интерфейс объявлен на уровне фреймворка: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/IControllerFactory.cs.

Такой подход позволяет разработчику влиять на поведение фреймворка (важно помнить о соблюдении post conditions и pre conditions из LISP). Постулаты, описанные в статье соблюдены.

Так же данный пример может служить расширенной версией описанного в статье, так как разработчиками фреймворка так же предусмотрена стандартная реализация этого интерфейса https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/DefaultControllerFactory.cs.

Тип 2: Интерфейс вынесен на уровень общей библиотеки.

Например, в проекте может быть библиотека Service Registry, в которой описаны контракты всех модулей, сервисов и микросервисов. Эти контракты строго соблюдаются на уровне реализации служб (например, в других репозиториях). Когда Service A необходимо обратиться к Service B, он использует контракт ServiceRegistry.ServiceB.Execute(request).

Предположим, что Service B реализует этот интерфейс на уровне двух протоколов: gRPC и JSON-RPC (например, Yandex Cloud API). Также существуют две клиентские библиотеки: ServiceB.JsonRPCClient и ServiceB.GRPCClient. На уровне инфраструктуры Service A происходит выбор клиентской библиотеки. Тем не менее, нельзя утверждать, что Service A теперь независим от реализации Service B. Единственное, от чего он не зависит, — это от транспорта или способа коммуникации между ними. Однако он зависит от контракта и поведения. Возможно, такой подход также подпадает под DIP, но в данном случае происходит абстрагирование не от логики, выполняемой Service B, к которой Service A становится привязан, а от способа коммуникации.

Если рассматривать пример с репозиторием, я бы отнес его к Типу 1. Интерфейс репозитория определяется на уровне, где требуется получить данные о сущности, например, в сценариях использования (use cases). Это позволяет не беспокоиться о том, откуда загружается эта сущность (будь то SQL база данных, NoSQL база данных, кеш или файловая система), однако создает зависимость от конкретной сущности, и это именно то, чего мы хотим.

Для написания работающего кода невозможно абстрагироваться от всего, поэтому, как мне кажется, DIP позволяет абстрагироваться только от тех аспектов, которые не важны для реализации конкретного пользовательского сценария. Если пользовательскому сценарию необходимо перевести сущность из состояния А в состояние Б, он сосредотачивается на этом, снимая с себя "ответственность" за способ записи в базу данных. Если сценарий Service A жестко зависит от поведения Service B, то от поведения Service B абстрагироваться не удастся (и не нужно), но можно, по крайней мере, абстрагироваться от используемого транспортного протокола.

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

Осталось чтобы появившаяся альтернативная либа реализовывала ваш интерфейс

Пожалуйста!

Если ваша библиотека предполагает возможность расширения пользователем, то без интерфейсов не обойтись. Без них пользователь не сможет адаптировать поведение библиотеки под свои нужды. Например, интерфейс AutoMapper.IValueConverter: https://github.com/AutoMapper/AutoMapper/blob/master/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs#L306. А вот пример его использования из документации: https://docs.automapper.org/en/latest/Value-converters.html.

Но если у вас библиотека типа Math, то здесь вряд ли потребуются расширения, разве что математика каким-то образом меняется от клиента к клиенту. :)

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

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

то есть модуль пользователя будет нижним модулем, а либа будет верхним модулем

Обычно как раз наоборот, знаете ли. Если мы говорим про библиотеки именно.

Не факт

Для меня факт) Вот тут описали мой сценарий https://habr.com/ru/articles/872078/comments/#comment_27757026 только у меня форматирование вывода на экран, библиотека выводит на экран в дэфолтном виде, но заимплементя интерфейс это поведение можно еще доотформатировать.

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

Предположим приложение состоит из двух классов:

  • Program - инициализация приложения;

  • MyAppColorSchema - реализует интерфейс IColorSchema из библиотеки @house2008.Библиотека позволяет окрашивать вывод в консоль используя разные цвета.

Отношение будет такое:

Program (МВУ) использует библиотеку (в данном контексте МНУ, но без инверсии). В Program происхожит инициализаия MyAppColorSchema (в данном контексте МНУ для Program) и передача ее в библиотеку (предположим через статический класс, не принципиально). Когда происходит вывод в консоль, библиотека (в этом контексте уже МВУ) использует MyAppColorSchema (МНУ).

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

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

Program (МВУ) использует библиотеку (в данном контексте МНУ, но без инверсии)

У вас модули несопоставимых размеров. Если библиотека - модуль, то вызывающий ее модуль - не Program, а все приложение целиком.

Поэтому с такой точки зрения ковыряться в этом нет смысла.

Я, собственно, поэтом и говорю, что разделение на "МВУ" и "МНУ" в статье лишено какой-либо пользы (а вот вред может принести).

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

Из википедии: "Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций". Оба типа модулей отдельно, а абстракции отдельно.

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

Почему B (МНУ) не должен зависеть от A (МВУ)? Про это в DIP не говорится. Мы инвертировали зависимость, и если раньше A зависел от B, то теперь B зависит от A.

Оба типа модулей отдельно, а абстракции отдельно.

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

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

"One might question why I use the word “inversion”. Frankly, it is because more traditional software development methods, such as Structured Analysis and Design, tend to create software structures in which high level modules depend upon low level modules, and in which abstractions depend upon details. Indeed one of the goals of these methods is to define the subprogram hierarchy that describes how the high level modules make calls to the low level modules. [...] Thus, the dependency structure of a well designed object oriented program is “inverted” with respect to the dependency structure that normally results from traditional procedural methods." ("The Dependency Inversion Principle", Robert C. Martin, C++ Report, May 1996)

Это не определение. И это в каком-то смысле реткон.

Оба типа модулей отдельно, а абстракции отдельно.

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

Если рассматривать систему, как набор модулей/компонентов, то было бы странно увидеть где-то отдельно стоящую абстракцию/интерфейс.
Приведу несколько наглядных примеров/изображений из книги «Чистая архитектура»:

Глава 14 Раздел «Breaking the cycle» («Разрыв цикла»). На изображении приведен как раз пример инверсии зависимости (до/после):

В главе 19 об уровнях можно увидеть пример, где интерфейс принадлежит верхнему уровню:

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

Да прямо в статье 1996 года и есть такой пример. Вы уверены, что вы слово "модуль" понимаете так же, как Мартин? Потому что в статье, о которой я говорю, нет модуля в вашем понимании этого слова (т.е. чего-то, внутри чего может быть интерфейс), там модуль - это подпрограмма (или класс, или абстрактный класс), там все элементы одного уровня.

А если открыть Clean Architecture, то там в определении нет слова модуль:

The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.

А дальше, в отдельноу главе, написано черным по белому:

In a statically typed language, like Java, this means that the use, import, and include statements should refer only to source modules containing interfaces, abstract classes, or some other kind of abstract declaration. Nothing concrete should be depended on.

Иными словами, "модуль", от которого зависят другие модули, может содержать только абстракции, но не реализации. Ваш МВУ это требование нарушает.

там модуль - это подпрограмма (или класс, или абстрактный класс)

Где такое сказано? можете привести цитату (Главу, раздел)?

А если открыть Clean Architecture, то там в определении нет слова модуль:

И? Т.е. к чему вы это говорите? Вы хотите сказать, что в DIP нет модулей или что?

Иными словами, "модуль", от которого зависят другие модули, может содержать только абстракции, но не реализации. Ваш МВУ это требование нарушает.

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

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

Где такое сказано? можете привести цитату (Главу, раздел)?

"Figure 1 is a “structure chart” . It shows that there are three modules, or subprograms, in the application. The “Copy” module calls the other two. One can easily imagine a loop within the “Copy” module. (See Listing 1.) The body of that loop calls the “Read Keyboard” module to fetch a character from the keyboard, it then sends that character to the “Write Printer” module which prints the character."

("The Dependency Inversion Principle", Robert C. Martin, C++ Report, May 1996; раздел "Example: the “Copy” program.")

И? Т.е. к чему вы это говорите? Вы хотите сказать, что в DIP нет модулей или что?

Я это говорю к тому, что в вашей статье приведено вот такое определение DIP:

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

Это определение дословно соответствует определению, данному в статье 1996 года:

A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.

Это значит, что для того, чтобы понять, что означает слово "модуль" в этом определении, нужно ориентироваться на эту статью, а не на "Clean Architecture", где, как я уже говорил, определение другое ("The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.")

И понимание модуля в статье 1996 года - не такое, как в вашей статье.

И на этом изображении модуль Entities как-будто бы не состоит из сплошных абстракций... или, по вашему, состоит?

Состоит. И из того, что вынесение общих абстракций Мартин приводит следующим пунктом, альтернативным, я делаю вывод, что к моменту "Clean Architecture" (2017 год) его понимание DIP изменилось по сравнению с его же пониманием в 1996. Больше 20 лет прошло, это нормально...

Собственно, я потому и говорю, что объяснения в Clean Architecture - это реткон, потому что к этому моменту уже есть сложившееся использование (достаточно посмотреть в википедию), и надо быть очень акккуратным, чтобы не получить противоречий.

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

Я уже цитировал Мартина по этому поводу:

One might question why I use the word “inversion”. Frankly, it is because more traditional software development methods, such as Structured Analysis and Design, tend to create software structures in which high level modules depend upon low level modules, and in which abstractions depend upon details. Indeed one of the goals of these methods is to define the subprogram hierarchy that describes how the high level modules make calls to the low level modules. Figure 1 is a good example of such a hierarchy. Thus, the dependency structure of a well designed object oriented program is “inverted” with respect to
the dependency structure that normally results from traditional procedural methods.

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

Так что да, можно долго и упорно придумывать себе объяснение, что "это потому, что "нормальным образом" мы бы написали код, в котором копирование зависит от IO, а согласно этому принципу мы написали код, где IO зависит от абстракций"... или можем просто сказать, что слово "инверсия" там правда неудачное, и не надо на нем зацикливаться, а надо смотреть на то, какие проблемы и как решает этот принцип. Do as I do, not as I say.

Да прямо в статье 1996 года и есть такой пример. Вы уверены, что вы слово "модуль" понимаете так же, как Мартин? Потому что в статье, о которой я говорю, нет модуля в вашем понимании этого слова (т.е. чего-то, внутри чего может быть интерфейс), там модуль - это подпрограмма (или класс, или абстрактный класс), там все элементы одного уровня.

"Figure 1 is a “structure chart” . It shows that there are three modules, or subprograms, in the application. The “Copy” module calls the other two. One can easily imagine a loop within the “Copy” module. (See Listing 1.) The body of that loop calls the “Read Keyboard” module to fetch a character from the keyboard, it then sends that character to the “Write Printer” module which prints the character."

Здесь нет какого-то “правильного” ввода определения модуля.
Странно, что вы с таким упорством пытаетесь везде показать на тайный смысл слова модуль.
Смысловая нагрузка всегда одна — речь идет о наборе связанного функционала, а как его назвать, модуль, или политика, или компонент, или что-то еще, большой разницы нет.

Это значит, что для того, чтобы понять, что означает слово "модуль" в этом определении, нужно ориентироваться на эту статью, а не на "Clean Architecture", где, как я уже говорил, определение другое ("The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.")

И понимание модуля в статье 1996 года - не такое, как в вашей статье.

Тут аналогично, ответил выше.

Состоит. И из того, что вынесение общих абстракций Мартин приводит следующим пунктом, альтернативным, я делаю вывод, что к моменту "Clean Architecture" (2017 год) его понимание DIP изменилось по сравнению с его же пониманием в 1996. Больше 20 лет прошло, это нормально...

Собственно, я потому и говорю, что объяснения в Clean Architecture - это реткон, потому что к этому моменту уже есть сложившееся использование (достаточно посмотреть в википедию), и надо быть очень акккуратным, чтобы не получить противоречий.

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

Я уже цитировал Мартина по этому поводу:

One might question why I use the word “inversion”. Frankly, it is because more traditional software development methods, such as Structured Analysis and Design...

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

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

Кто-то может задаться вопросом, почему я использую слово “инверсия”.

Не похоже, что из этой фразы можно сделать вывод, что он в чем-то сомневался.

Здесь нет какого-то “правильного” ввода определения модуля. [...] Смысловая нагрузка всегда одна — речь идет о наборе связанного функционала

Да нет же. Подпрограмма (или класс) - это не набор связанного функционала. Это просто подпрограмма.

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

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

Не похоже, что из этой фразы можно сделать вывод, что он в чем-то сомневался.

Если бы он не сомневался, он бы не писал "можно задаться вопросом". Риторические обороты - такие риторические.

Вот вам еще про этот же принцип, на этот раз из "Agile Principles, Patterns, and Practices in C#" (Micah Martin, Robert C. Martin, 2006) (к сожаление, у меня нет под рукой "Agile Software Development, Principles, Patterns, and Practices" (Robert C. Martin, Prentice Hall, 2002), но как можно будет дальше видеть, это не должно быть принципиально). В разделе 11, "The Dependency-Inversion Principle (DIP)" идет долгий (и почти дословный) пересказ кусков статьи 1996 года (именно поэтому я не думаю, что в книге 2002 есть фундаментальные отличия; забавно, что там опять есть объяснение "Over the years, many have questioned why I use the word inversion in the name of this principle"), после которого идет более интересный нам фрагмент "Ownership Inversion".

Note that the inversion here is one of not only dependencies but also interface ownership. We often think of utility libraries as owning their own interfaces. But when DIP is applied, we find that the clients tend to own the abstract interfaces and that their servers derive from them.
[...]
In this context, ownership simply means that the owned interfaces are distributed with the owning clients and not with the servers that implement them. The interface is in the same package or library with the client. This forces the server library or package to depend on the client library or package.

Это совпадает с тем, что говорите вы... но вот вам следующий абзац:

Of course, there are times when we don’t want the server to depend on the client. This is especially true when there are many clients but only one server. In that case, the clients must agree on the service interface and publish it in a separate package.

И это то, о чем говорю я - когда модули зависят не друг от друга, а от абстракций.

И, все оттуда же, буквально следующий абзац:

A somewhat more naive, yet still very powerful, interpretation of DIP is the simple heuristic: “Depend on abstractions.”

...из чего вытекает, что да, трактовка "зависьте от абстракций" была (в 2006 году; в 2017 из Clean Architecture можно сделать другие выводы, почему я и говорю, что важно оговаривать источники) для Мартина допустимым пониманием DIP. Что напрямую противоречит самому началу вашей статьи:

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

И, финальный аргумент в эту же сторону: в "Clean Architecture" Мартин ссылается на http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod:

These principles have been described in detail in many different publications [For example, Agile Software Development, Principles, Patterns, and Practices, Robert C. Martin, Prentice Hall, 2002, http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, and https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) (or just google SOLID).]

Открываем эту ссылку, и читаем:

DIP - The Dependency Inversion Principle - Depend on abstractions, not on concretions.

Выглядит так, что это все-таки допустимая трактовка, не правда ли?

И это то, о чем говорю я - когда модули зависят не друг от друга, а от абстракций.

От абстракций модули и у меня зависят. У вас, как я понял, они зависят от отдельного «места/модуля для абстракций». По поводу отдельного «места для абстракций» написал подробнее тут https://habr.com/ru/articles/872078/comments/#comment_27770836

...из чего вытекает, что да, трактовка "зависьте от абстракций" была (в 2006 году; в 2017 из Clean Architecture можно сделать другие выводы, почему я и говорю, что важно оговаривать источники) для Мартина допустимым пониманием DIP. Что напрямую противоречит самому началу вашей статьи:

Не противоречит. Как сказано в текущей статье, при выполнении «МВУ не должны зависеть от МНУ» оба модуля будут зависеть от абстракций. МВУ от своей абстракции, а МНУ от абстракции МВУ.

И, финальный аргумент в эту же сторону: в "Clean Architecture" Мартин ссылается на http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod:

These principles have been described in detail in many different publications [For example, Agile Software Development, Principles, Patterns, and Practices, Robert C. Martin, Prentice Hall, 2002, http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, and https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) (or just google SOLID).]

Открываем эту ссылку, и читаем:

DIP - The Dependency Inversion Principle - Depend on abstractions, not on concretions.

Ответная цитата) :
из книги «Чистая Архитектура» Часть 3

DIP: The Dependency Inversion Principle
The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.

Выглядит так, что это все-таки допустимая трактовка, не правда ли?

Она и была допустимой, но тут надо понимать, что это не просто абстракции. Другими словами, у этих абстракций есть цель — инвертировать зависимость, чтобы МВУ не зависел от МНУ (это 1ое предложение описания принципа) и это ключевой момент и если он не соблюден, то DIP не выполнен.

Если выражаться более академично, нужно сделать так, чтобы направления потока управления и зависимости смотрели в противоположных направлениях. Об этом, кстати, упомянули тут: https://habr.com/ru/articles/872078/comments/#comment_27764234

Этот поток бесконечных боданий цитатами больше похож на спор ради спора.
Настоящих, на мой взгляд, аргументов от вас в пользу того, что текущая статья противоречит действительности, я не услышал. Но все равно спасибо, что благодаря одному из ваших комментариев я подкорректировал определение МВУ, т.к. до этого понять его можно было двояко https://habr.com/ru/articles/872078/comments/#comment_27756172

Ваша трактовка принципа приблизительно ясна. Оказывается есть еще какой-то отдельный 3-й «модуль абстракций», стоящий отдельно от МВУ и МНУ и поэтому  каким-то образом где-то происходит инверсия зависимостей. Ок. Я с этим не согласен, но спорить больше не буду)

Она и была допустимой, но тут надо понимать, что это не просто абстракции.

Нет, это именно просто абстракции. Это явно написано в двух приведенных цитатах (особенно, в первой, в книге 2006 года).

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

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

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

Это меня не удивляет. Ведь иначе нельзя утверждать в заголовке, что 90% разработчиков чего-то не понимают.

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

Ваша трактовка принципа приблизительно ясна. Оказывается есть еще какой-то отдельный 3-й «модуль абстракций», стоящий отдельно от МВУ и МНУ

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

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

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

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

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

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

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

Я бы сказал, что 90% не знают зачем в принципе используют солид на проекте

Предлагаю к обсуждению иллюстрацию применения принципа DIP на примере машиностроения

Предположим, что собирается автомобиль, который использует болты M6 с определённой маркой стали. Производители автомобилей и производители болтов ничего не знают друг о друге: одни проектируют и собирают автомобили, другие производят болты. Однако существует ГОСТ, который определяет требования к болтам, их размерам, материалам и условиям применения. ГОСТ задаёт универсальный стандарт, понятный как производителям автомобилей, так и производителям болтов.

В данном случае:

  1. Модуль верхнего уровня (МВУ): Производитель автомобилей. Он описывает, какие характеристики болтов ему необходимы (например, диаметр 6 мм, определённая марка стали) для сборки автомобилей, но ему не нужно знать, кто именно поставляет эти болты.

  2. Абстракция: ГОСТ, который определяет требования к болтам. Это уровень абстракции, обеспечивающий стандарты взаимодействия между МВУ и МНУ. ГОСТ описывает "интерфейс взаимодействия", задавая требования к публичным методам классов в терминах программирования.

  3. Модуль нижнего уровня (МНУ): Производители болтов. Они изготавливают болты в соответствии с ГОСТом, не имея информации о том, как именно их продукция будет использоваться — в автомобилях, бытовой технике или другом оборудовании.

Таким образом, вместо двух уровней взаимодействия (МВУ и МНУ), фактически выделяется три уровня, где абстракция (ГОСТ) выступает связующим звеном.


Границы модулей в контексте примера:

  • МВУ (Модули верхнего уровня): Производство автомобиля + Абстракция (ГОСТ) на болты.
    Задача: Производитель автомобилей определяет стандарты (требования к болтам) и использует эти стандарты для взаимодействия с болтами.

  • МНУ (Модули нижнего уровня): Производство болтов по ГОСТу.
    Задача: Производители болтов реализуют требования ГОСТа, создавая продукт, соответствующий стандартам.

    Абстракцию (в данном случае ГОСТ) создаёт верхний уровень (МВУ), а не нижний (МНУ). Это важно, чтобы МВУ был независим от деталей реализации, которые могут изменяться на нижнем уровне.

    Если бы ГОСТ создавался производителями болтов, то производитель автомобилей был бы зависим от конкретных деталей реализации, что нарушает DIP.

Обратите внимание на мой комментарий https://habr.com/ru/articles/872078/#comment_27755320. Описываемая вами ситуация подходит под тип 2 на основе контрактов. Автопроизводитель это модуль А. ГОСТ это Контракт / Реестр. Производитель болтов это модуль B.

Тем не менее в вашем утверждении заложена неточность:

Задача: Производитель автомобилей определяет стандарты (требования к болтам) и использует эти стандарты для взаимодействия с болтами.

Стандарты и требования к болтам определяет ГОСТ (Контракт / Реестр). Автопроизводитель определяет, какие болты из реестра ГОСТ он будет использовать. Автопроизводитель не определяет, что собой представляет болт формата М6. Производитель болтов определяет, какие болты в соответствии со стандартами ГОСТ он будет производить.

Для соответствия типу 1, необходимо, чтобы автопроизводитель разработал свой собственный стандарт болта. Далее он создаёт свой собственный реестр болтов и вносит туда этот стандарт. Далее от открывает доступ заводам к этому реестру. С этого момента происходит инверсия зависимости: автопроизводитель берет на себя роль МВУ, а фабрика берет на себя роль МНУ.

P.S. В моем случае Service Registry подразумевает реестр ваших собственных служб, но это не обязательно. В реестр могут быть внесены так же более общие контракты, например IEmailSender. Было бы замечательно, если бы был общий реестр таких интерфейсов встроенный напрямую в стандартную библиотеку классов. И как мне кажется, такое уже постепенно происходит. Это можно наблюдать на примере интерфейсов Microsoft.Extensions.Configuration.IConfiguration, Microsoft.Extensions.Logging.ILogger. Это позволяет управляющему коду (производитель авто) использовать разные логгеры (serilog, log4net) опираясь на ГОСТ (интерфейс ILogger).

Модуль верхнего уровня (МВУ): Производитель автомобилей. Он описывает, какие характеристики болтов ему необходимы (например, диаметр 6 мм, определённая марка стали) для сборки автомобилей, но ему не нужно знать, кто именно поставляет эти болты.

Абстракция: ГОСТ, который определяет требования к болтам.

Однако существует ГОСТ, который определяет требования к болтам, их размерам, материалам и условиям применения

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

Все верно, но кто определяет ГОСТ вообще не важно. Важно только что он определяется абтрактно для того, чтобы их удобно было производить и использовать.

Важно не забывать что есть

DI - Dependency Injection

DI - Dependency Inversion

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

Основная идея DepInj в том - что вместо конструирования используемого класса в конструкторе. Мы передаем указатель на этот класс в конструктор.(См. Types of dependency injection) https://en.wikipedia.org/wiki/Dependency_injection

Далее солид расширяет эту идею через интерфейсы. Говоря мы будем передавать в конструктор используя DepInj не любой тип данных а интерфейс. И это уже будет называться DepInv. И приводить к тому что наш класс знает только о интерфейсе.

DI - Dependency Inversion

Корректнее будет DIP — dependency inversion principle

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

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

Я не могу представить, как можно обсуждать какой-либо принцип без наглядного примера. Например, закон всемирного тяготения часто иллюстрируется анекдотом о Ньютоне, которому на голову упало яблоко. Здесь все ясно: яблоко упало, потому что Земля, как более массивный объект, притягивает его. Это понятно даже ребенку, и в этом анекдоте есть многослойная мораль о том, что на голову могут падать предметы, это больно, опасно; и объяснение, почему это происходит.

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

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

Разработчик расширения пишет код -> передает его мне -> я интегрирую его в код своего браузера. Все жестко зафиксировано. Грубо говоря интерфейс здесь это мой email на который мне может написать разработчик.

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

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

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

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

  • Недостатки: Нужно потратить некоторое время на проектирование такой системы, четко определить границы того, к чему у расширений есть доступ, а к чему нет.

Если мой браузер предполагает 3-4 расширения, которые к тому же будут редко обновляться, я не буду тратить время реализуя сложный движок, а просто зашью эти "расширения" в свою систему.

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

Здесь все ясно: яблоко упало, потому что Земля, как более массивный объект, притягивает его. 

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

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

Наверное есть более научное объяснение этому, но это не мой профиль, я не провожу в работе над законами физики 12 часов в день на протяжении 10 лет :)

Кстати как раз этот важный нюанс про взаимное притяжение этом анекдоте опущен, так что пример не самый удачный :)

Чем выше масса и меньше расстояние, тем выше сила притяжения, поэтому я позволил себе обобщить, что земля притягивает яблоко сильнее, чем яблоко землю

Сила одинаковая.

Полезное замечание.

Автор накинул на вентилятор и растворился в тумане. А хотелось бы от него какие-то ответы получить.

Меня в описанном крайне смущает такой сценарий:

  • Есть 3 модуля (полноценных, отдельно компилирующихся, можно рассматривать и как библиотеки). Назовем их A, B, C.

  • Модуль A зависит от B

  • Модуль C зависит от B

  • Кроме классов с реализацией модуля B должны присутствовать интерфейсы (или абстракции).

  • Автор говорит, что эти интерфейсы не могут присутствовать в самом модуле B, а должны дублироваться в А и C

Как мы знаем, все библиотеки сделаны так, что либо интерфейсы к реализации присутствуют в самой библиотеке, либо оформляются отдельным модулем (пример slf4j как интерфейс и logback как реализация). И оба этих кейса подразумевают, то интерфейс относится к МНУ. Почему делают именно так, думаю понятно, чтобы избежать абсурдной ситуации с дублированием как в моем примере, а ещё потому, что именно поставщик отвечает за интерфейс, а не потребитель.

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

Напрашивается ответ - ошибка в архитектуре (например, если МНУ это SQL).

Если же МНУ более утилитарный, то получается вполне нормальная ситуация: два модуля, две команды разработчиков, два интерфейса.

Зачем им два интерфейса к одному сервису или библиотеке? Почему не переиспользовать один, который предоставляет сама библиотека или сервис?

Такой интерфейс будет абстрагировать только транспорт. В некоторых случаях это имеет смысл, например, когда транспорт у одной службы может быть разный (json-rpc / grpc). Сложно представить случай, когда появляется необходимость менять транспорт в рантайме.

Логика самой службы все ещё остаётся не абстрагированной. Об этом я говорил в комментариях выше.

И оба этих кейса подразумевают, то интерфейс относится к МНУ.

Оба этих кейса подразумевают обратное. МВУ становится тот модуль, который вызывает интерфейс. МНУ тот модуль, который реализует интерфейс.

Пример кода МНУ:

public class ModuleB : IContract {
  ...
}

Пример кода МВУ:

public class ModuleA {
  public ModuleA(IContract dependency){
    ...
  }
}

При этом конечно Module A , сам может быть МНУ для Module C:

public class ModuleA : IAnotherContract {
  public ModuleA(IContract dependency){
    ...
  }
}

Таким образом формируется граф зависимостей.

Давая волю рассуждениям, по идее, где-то в самом верху будет МВУ (точка входа в граф), который не является МНУ ни для кого. Но это уже что-то про уровень ядра операционных систем или глубже 🤔 А на уровне приложения такой точкой выступает Composition Root или же DI контейнер, который является частным случаем Composition Root.

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

Из моего опыта, в рамках веб-служб, в вашем собственном коде, который вы контролируете в рамках одного репозитория, интерфейсы, вероятно, будут следующими:

  1. Доступ к данным: На уровне пользовательского сценария создаются собственные интерфейсы для доступа к данным, например, через репозиторий. Данные интерфейсы реализуются на инфраструктурном уровне. Можно использовать готовый интерфейс, например, EntityFramework.IDbContext, который обеспечивает гибкость в работе с хранилищем, а также огромное количество МНУ (EntiryFramework.MSSQL, EntityFramework.SQLLite & etc).

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

  1. Бизнес-службы:

  • Виртуальные: Обобщенные службы, необходимые для выполнения бизнес-сценария. Например, вы можете создать IPaymentGateway на уровне пользовательских сценариев и реализовать адаптеры StripePaymentGateway и AuthorizeNetPaymentGateway на инфраструктурном уровне. Или в отдельной библиотеке, подключая их через DI-контейнер. Это нечто среднее между доступом к данным и бизнес-стратегией.

  • Конкретные: Например, IStripeService, если в вашем бизнес-сценарии важен именно Stripe. Здесь многое зависит от того, как написан StipeSDK для вашего языка программирования. Возможно, вы сможете использовать IStripeClient напрямую из библиотеки. Если это невозможно, вы создадите более удобный интерфейс для ваших пользовательских сценариев и делегируете конфигурацию StripeService - адаптеру, расположенному на уровне инфраструктуры. Так же возможен сценарий, когда у вас используется сервисно ориентированная архитектура и есть библиотека классов, которая предоставляет общий контракт служб. Тогда там будут находится интерфейсы вроде IMyProductServiceA и IMyProductServiceB и так далее. В даннном случае вы так же абстрагируетесь только от деталей транспорта (протокол, авторизация и аутентификация), но используете конкретную службу.

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

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

С вики страницы нашел вот такие оригиналы

https://web.archive.org/web/20110714224327/http://www.objectmentor.com/resources/articles/dip.pdf

https://linux.ime.usp.br/~joaomm/mac499/arquivos/referencias/oodmetrics.pdf

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

Но вопрос в другом, когда строится сложная система, то для того, чтобы не тащить конкретную специфику инфраструктуры, выделают интерфейс (только это не конструкция языка, а абстракция модуля) и таким образом заявляют/утверждают требуемые функции от инфраструктуры реализации.
И пример у Роберта Мартина как раз об этом и говорит, что не надо тащить KeyboardReader в приложение, т.к. приложению все равно откуда читать и куда писать.
Но DIP надо применять тоже разумно, TTM играет важную роль и если вынос в DIP стоит относительно недорого, то для начальной реализации можно сначала реализовать прямую связь, а потом уже внести абстракцию - это очень тонкий и индивиудальный момент и не всегда такое получается сделать.

Ответ: 1. DIP реализован корректно.

НЕТ!! Есть например модуль шифрования/обращения по сети/любая другая либа, с какого перепугу этот модуль должен реализовывать какие-то левые интерфейсы из приложений, если его использует 100500 приложений?

Автор обвинил, что 90% неправильно понимают и сам же в диаграммах ресует чушь. Пост просто реклама ТГ канала.

НЕТ!! Есть например модуль шифрования/обращения по сети/любая другая либа, с какого перепугу этот модуль должен реализовывать какие-то левые интерфейсы из приложений, если его использует 100500 приложений?

Очевидно, что библиотеки не должны реализовывать какие-то левые интерфейсы .
Библиотека сама по себе является МВУ , т.е. самодостаточна и не зависят от клиентского кода. И если вы не хотите, чтобы в вашем клиентском МВУ (который не библиотека) использовалась/вызывалась какая-то библиотека, то вызов этой библиотеки нужно сделать в МНУ (другими словами, строить обертку над библиотекой).
Т.е. если вы называете подключаемую стороннюю библиотеку модулем, то у вас в итоге 3 модуля: библиотека (МВУ), клиентский МВУ и МНУ, где вы «изолируете» библиотеку.
Но это уже частное решение какой-то задачи с использованием DIP, а не DIP

>> модуль шифрования/обращения по сети/любая другая либа
> Библиотека сама по себе является МВУ

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

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

Сама по себе либа — это МВУ, т.к. зависит сама от себя и ей безразлично на код, который она будет использовать.

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

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

В какой терминологической системе? Лично вашей, вами введенной, или используемой в литературе (например, у Мартина)?

В текущей статье сформулировал более доступные определения и то, почему они полностью соответствует определению, приведенному Мартином, объяснил в комментарии https://habr.com/ru/articles/872078/comments/#comment_27767816
Книгу Роберта Мартина не так просто читать, и для того, чтобы понять, что именно имеет ввиду автор, порой, нужно по 10 раз перечитывать каждую строку. Отчасти поэтому ведется столько споров вокруг принципов SOLID.

Какая статья... Какие споры...

А по факту получается так же, как и со скрамом: если разработчики не самостоятельны и/или несостоятельны, то какие ритуалы ни заставь их соблюдать, разработка быстрее не пойдёт. И прежде, чем заниматься в этим словоблудием, лучше спросить, а сколько разработчиков в состоянии написать простые "библитечные" функции типа преобразования поинтеров в значения и обратно или преобразования int в bool)

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

Не создавайте MySQLConnection или PgConnection - передавайте снаружи DbConnection. Не создавайте SomeServiceGrpcClient или SomeServiceRestClient - используйте переданный снаружи SomeServiceClient. Слишком много зависимостей чтобы передать их как параметры - создайте DependencyFactory который будет возвращать нужные зависимости. И продемонстрировать примеры.

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

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

Тогда каким образом зависимость инвертируется (поменяет свое направление на противоположное), какая конкретно это будет зависимость (которая ближе к МВУ или к МНУ) и почему именно она?

Давайте начнем с классического определения инверсии зависимостей, которое звучит так: When classes talk to each other in a very specific way, they both depend on each other to never change. Instead classes should use promises (interfaces, parents), so classes can change as long as they keep the promise.

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

Dependency injection по сути наиболее характерное применение этого подхода

НЛО прилетело и опубликовало эту надпись здесь

Как именно декларировать такие интерфейсы сильно зависит от языка и от сложности интерфейсов. Упираться в маскировку реализации КМК смысла нет - ну тут все же надо таки иметь хоть какую-то самодисциплину, в конце то концов.

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

interface XYZ{}

private class MyXYZ { … }

func CreateMyXYZ() XYZ { return new MyXYZ(); }

У нас может быть всего два случая взаимодействия сущностей:

  • A зависит от B

  • A не зависит от B. B зависит от A

Позднее связывание вышло из чата.

Вообще говоря, правильным уже довольно давно признан вариант Dependency Injection, когда функции передается имплементация в рантайме, типа

function apply_formatting(text, formatter) {
  formatter.format(text)
}

И всё.

Позднее связывание вышло из чата.

Зависимость-то всё равно остаётся, только её компилятор не проверяет.

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

Статья начинается с того что 90% разработчиков не верно трактуют DIP, а где итог? Как в двух словах объяснить DIP так, чтобы трактовка не звучала в корне не верной?

А зачем в статье вообще нужны понятия "верхний уровень" и "нижний уровень"?

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

Вот и вся инверсия.

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

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

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

Вот и вся инверсия.

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

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

Для меня это всегда был один из самых простых и понятных принципов, и никаких схем классов для него не нужно. Просто в реальной разработке, допустим, какого-то железа, происходит следующая ситуация, если разработка ведётся итеративно без плана. Разработчик драйвера устройства пишет код и интерфейс драйвера, как он его видит, со всеми ограничениями физическими и т.п. И этот интерфейс переусложнен. Там нужно вызвать метод "дрыгнуть ножкой АЦП", потом прождать 50 мс и т.п. Эта информация вообще не нужна разработчику управляющей программы, для него конечное устройство должно просто иметь пару методов вроде задал - получил и все. Исходя из этого он делает архитектуру. И принцип инверсии зависимостей - это то, что разработчик всей системы должен прийти к разработчику драйвера и дать ему интерфейс, которому тот должен соответствовать, а если тому кажется, что это невозможно, написать его за него, а не наоборот. В классах это и получается то, что Вы написали.

Я это студентам также объясняю: верхние уровни решают, как им будет удобно работать с нижними уровнями, а нижние приспосабливаются.

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

Вот пример нарушения инверсии зависимостей

# manager.py - какой-то модуль нижнего уровня

class MyManager:
  def manage(self):
    print("hello world, i am a manager")


# runner.py - верхнеуровневый модуль

from manager import MyManager


class Runner:
  def __init__(self):
    self.manager = MyManager()  # жёсткая связь с низкоуровневыми модулем

  def run(self):
    self.manager.manage()


# main.py - точка входа

from runner import Runner


runner = Runner()
runner.run()

>>> hello world, i am a manager

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

# manager.py - какой-то модуль нижнего уровня

class Manager:
  def manage(self):
    raise NotImplementedError


class MyManager(Manager):
  def manage(self):
    print("hello world, i am a manager")


# runner.py - верхнеуровневый модуль

from manager import Manager


class Runner:
  def __init__(self, manager: Manager):
    self.manager = manager  # образовалась мягкая связь

  def run(self):
    self.manager.manage()


# main.py - точка входа

from manager import MyManager
from runner import Runner

manager = MyManager()
runner = Runner(manager)
runner.run()

>>> hello world, i am a manager

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

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

Если мы захотим новое поведение, придётся создавать новый менеджер, и новый раннер

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

И все, без всяких интерфесов обошлись. Т.е. поведение как раз будет зависеть от взаимного расположения (и конфигов где что лежит).

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

Пример (за такое побъют и правильно сделают но тем не менее)

# файл impl1.py
class MyManager:
  def manage(self):
    print("hello world, i am a manager 1")
# файл impl2.py
class MyManager:
  def manage(self):
    print("hello world, i am a manager 2") 
# файл main.py
import importlib
class Runner:
  def __init__(self):
    self.manager = MyManager()  # Будет использована загруженная имплементация

  def run(self):
    self.manager.manage()

impl = importlib.import_module("impl2")
MyManager = impl.MyManager
runner = Runner()
runner.run()

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

Где же тут инверсия? Как "runner.py - верхнеуровневый модуль" использовал Manager, так и использует, только теперь зависимость внедряется через конструктор. Еще добавлен интерфейс и его реализация. Это конечно лучше (как правило), но инверсии зависимостей нет. Я вовсе не утверждаю что она тут нужна)

Для инверсии базовый класс Manager (ну или интерфейс) должен быть определен в "runner.py - верхнеуровневый модуль", а уже "manager.py - какой-то модуль нижнего уровня" должен его имплементить.

Вот что пишет Гуру Солида

Each of the upper-level layers declares an abstract interface for the services that it needs. The lower-level layers are then realized from these abstract interfaces.

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

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

При прямой зависимости, нижний модуль будет ломать верхний при изменении. Это не добавляет системе стабильности. Верхний модуль приносит прибыль, заначит мы его хотим защитить. Добавляем интерфейс. Теперь мы защищены тем, что оба модуля работают по контракту. Чтобы что-то сломать, надо менять интерфейс, а это уже сложнее.
Сам интерфейс конечно защиты не даёт, но можно построить процес вокруг.

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

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

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

Верхний он владет этой абстракцией, он задаёт интерфейс и он предлагает изменения.

Как-то непонятно. В итоге интерфейс в "верхнем" модуле это хорошо или это плохо?

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

В Питоне будет циклическая зависимость

Да вроде как и не должно, вот так все компилируется:

# app/feature1/feature1_service.py верхний уровень, не импортирует ничего

class IFeature1Repository:
	def get_record_with_specific_columns_by_using_specific_conditions(self):
		raise NotImplementedError


class Feature1Service:
	def __init__(self, feature1_repository):
		self.feature1_repository = feature1_repository

	def do_some_feature1_specific_stuff(self):
		record = self.feature1_repository.get_record_with_specific_columns_by_using_specific_conditions()

		print(record)


# app/feature1/feature1_repository.py нижний уровень, импортирует интерфейс
from app.feature1.feature1_service import IFeature1Repository

class Feature1RepositoryImpl1(IFeature1Repository):
	def get_record_with_specific_columns_by_using_specific_conditions(self):
		return "SELECT a, b FROM table WHERE cond"


# app/__main__.py

from app.feature1.feature1_service import Feature1Service # тут не импортируется feature1_repository
from app.feature1.feature1_repository import Feature1RepositoryImpl1

feature1_repository = Feature1RepositoryImpl1()
feature1_service = Feature1Service(feature1_repository)
feature1_service.do_some_feature1_specific_stuff()


# python -m app
# SELECT a, b FROM table WHERE cond

Верхний, нижний... Зачем путаться в этих относительных терминах, если смысл понятен без них? Смысл в том, чтобы код бизнес-логики не менялся, если потребуется поменять любую из внешних зависимостей. Так накладные расходы смены зависимостей сводятся к минимуму - нет необходимости трогать код бизнес-логики от слова "совсем". В этом смысле те же фреймворки это внешние зависимости. И библиотеки. И всё, что угодно, что не является бизнес-логикой. А бизнес-логикой является то, что непосредственно вы должны разрабатывать и поддерживать.

И вот, нужно мне в бизнес-логике получить данные по ряду тразнакций и выдать некий результат. Для этого надо идти в базу данных. Без инверсии зависимостей я должен включить необходимые модули для работы с используемой базой данных и пользоваться их специфическими методами и их сигнатурами. Обращая зависимости, я объявляю зависимость от интерфейса, обязующегося реализовать ряд специфических методов. В классе адаптера, реализующего этот интерфейс, я работаю с конкретными модулями конкретных баз данных. Но если эти СУБД каким-нибудь образом обделались, и я хочу заменить их на другие, то в коде мне необходимо только написать новый адаптер, реализующий уже имеющийся интерфейс. А бизнес-логику трогать нет никакой необходимости. И так по отношению ко всем внешним зависимостям, включая фреймворки. Бизнес-логика - это ядро, которое не должно меняться при смене внешних зависимостей типа фреймворков или СУБД. В этом смысл инверсии зависимостей - чтобы твой персональный код не зависел от технических реализаций всякой внешней хрени, которой тьма, и предсказать изменения которой априори невозможно. И тогда, когда по любой причине тебе нужно поменять внешние зависимости - ты просто пишешь новые адаптеры, и подключаешь их вместо старых - И ВСЁ. Код твоей бизнес-логики НЕ ТРЕБУЕТ ИЗМЕНЕНИЙ.

Зачем путаться в этих относительных терминах, если смысл понятен без них?

Как видим непонятен, иначе откуда столько споров? Я вот вижу что люди упорно называют инверсией зависимостей простое добавление интерфейса. Хотя у Мартина написано почему инверсия, что именно это значит и нарисованы стрелочки.

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

А как тут поможет именно инверсия зависимостей? Интерфейс объявить можно и без всякой инверсии. Об этом, кстати, у Буча написано, на которого ссылается Мартин.

There is a clear separation of concerns between the interface and implementation of each layer, making it possible to change the implementation of a layer without violating the assumptions made by its clients

Видите? Разделение на интерфейс и реализацию есть, возможность изменения реализации тоже есть. Инверсии нет.

А как тут поможет именно инверсия зависимостей? Интерфейс объявить можно и без всякой инверсии.

Ищите в моём тексте слово "адаптеры".

Вы грамотно и точно всё описали.

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

Вследствие этого люди не понимают очень многие когниции.

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

Спасибо вам, что грамотно изложили и потрудились над текстом. Но ваши 90% даже здесь найдут повод для дискуссии:))) им это не поможет:)

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

Мне нравится хибрнейт/спринг дата, которые отделяют реальную базу данных от кода. Но я под 20 лет программирую и ни разу не видел смены БД под приложением. А вот "все выкидываем и с начала" я видел.

Задавайтесь чаще вопрос почему я это делаю, что это мне даст и что это даст бизнесу и будет вам счастье.

Разбудите меня через 100 лет и спросите чем занимаются на хабре, и я отвечу - пытаются понять SOLID и CleanCode.

Вот ваши примеры мне понравились, буду использовать при обучении неофитов.

Спасибо.

У меня есть подозрение, что этот принцип интуитивно понятен любому, кто писал на C/C++ и разрешал циклические зависимости включения заголовков.

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

Если бы это было не так (зависимость не меняла бы направление), то об этом уже давно бы все говорили и знали о несоответствии названия принципа и его содержания. Раз никто по поводу самого названия принципа не спорит, значит инверсия все же где-то есть. То есть, смена направления зависимости должна присутствовать обязательно!

Автор объясняет смысл слова "инверсия" в названии принципа не так, вот цитата из его "Чистая архитектура" в издании "Питер" (он пишет это в конце главы посвященной DIP):

Обратите внимание, что поток управления пересекает извилистую линию в направлении, обратном направлению зависимостей в исходном коде. Зависимости следуют в направлении, противоположном направлению потока управления - именно поэтому принцип получил название принципа инверсии зависимости.

То есть автор определяет направление зависимости относительно потока управления, из этого следует, что для реализации принципа DIP потребуется DI и IoC.

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

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

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

Поэтому, цитата, которую вы привели, не противоречит формулировке в текущей статье.

Про поток управления подробнее написано в 5ой главе книги “Чистая архитектура” в разделе “Dependency inversion”.
Вот скриншот оттуда:

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

Также есть некоторые упоминания потока управления в главе 18 в разделе «The dreaded monolith» («Ужасный монолит»)

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

тем не менее, раз уж разбирается название принципа, лучше таки привести мнение автора на этот счёт

В статье старался избегать академических терминов, чтобы объяснить принцип более доступно

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

Не всегда.

Посмотрите на иллюстрацию 5.1 из той же главы:

main - не высокоуровневая сущность. Более того, в консольной программе main может напрямую получить данные на вход, и дальше передать их в сущность более высокого уровня, та - в более высокий уровень и так далее. И это не только в консолях будет так: во многих современных программах entry point (не важно, будь это lambda function handler, или web controller, или event handler) - это элемент уровня представления, и он же - начало потока управления (по крайней мере, той чего части, которая контролируется и понятна разработчику).

И в таких программах поток управления будет идти, грубо говоря, от какого-нибудь LLx, где сообщение пришло в систему, дальше в соответствующий MLx, который отвечает за обработку сценария, оттуда будет вызван HLx, который бизнес-сущности, а дальше в обратном порядке - сценарий (MLx) и обработчик ответа (LLy).

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

main - не высокоуровневая сущность

Конечно высокоуровневая. main потребляет реализации других сущностей, поэтому она высокоуровневая.
На скриншоте даже видно по неймингу сущностей направление их увеличения/уменьшения уровней. main - HL (High Level) - ML (Medium Level) - LL (Low Level)

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

Гм.

Что такое, по вашему, main на этой диаграмме? Какому конкретно месту в коде традиционной программы оно соответствует? Что оно делает? Кто его вызывает?

Программисты его не понимают потому что им это не нужно, в 99% работы с реальными проектами а не лабораторными работами это сводится к "пиши код так что бы при изменении/замене одной вещи другие не ломались", а то что в какой то книжке писал дед который последний раз код писал в восьмидесятые людей на работе не особо волнует

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

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

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

- самое главное - требование включать интерфейс как часть модуля выского уровня в корне не верно. Вводя интерфейс мы вводим два условия: МВУ использует контракт взаимодействия (зависит от интерфейса), МНУ реализует контракт взаимодействия (тоже зависит от интерфейса). При этом интерфейс сам по себе может являться отдельным модулем, он не обязательно вообще становится частью одного или второго. Самое главное что у интерфейса нет никакой зависимости ни от А, ни от Б. Почему это важно - при переносе модулей А и Б в другую систему, интерфейс должен быть перенесен вместе с ними так как у них есть от него зависимость, и отсутствие зависимостей делает перенос интерфейса тривиальным. Это позволяет переиспользовать и А, и Б без изменений при условии сохранения контракта, даже в рамках одного проекта - в чем и есть улучшение масштабируемости архитектуры.
В то же время интерфейс может стать частью А или Б в зависимости от конкретной системы, это сугубо ситуативное решение.

Более наглядная взаимосвязь между модулями - интерфейс является независимой сущностью
Более наглядная взаимосвязь между модулями - интерфейс является независимой сущностью

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

Спасибо, сняли с языка)

А вот с третьим пунктом не соглашусь:

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

Делает перенос интерфейса тривиальным как раз его расположение в модуле A, т.к. нам достаточно просто перенести модуль A, ведь он самодостаточен, у него есть все для собственного существования. Если интерфейса в модуле A не будет, то при переносе модуля A нам нужно будет еще дополнительно переносить интерфейс. По этому поводу недавно писал тут https://habr.com/ru/articles/872078/comments/#comment_27770836

В то же время интерфейс может стать частью А или Б в зависимости от конкретной системы, это сугубо ситуативное решение

Если интерфейс станет частью Б, то тогда DIP будет нарушен, т.к. A как зависел от B, так и зависит. От простого добавления абстракции/интерфейса зависимость не меняет направление.

С последним замечанием согласен, спасибо :)

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

по поводу абстракций - это просто базовый принцип разработки любого ПО, [...] сущности должны зависеть от абстракций и не конкретных реализаций

Любопытно, кстати. Не знаете ли вы, случайно, где этот принцип был сформулирован (в таком виде) в первый раз? Потому что все упоминания, которые мне удалось найти - как раз в DIP.

Есть связанный, но отличающийся, принцип в GoF: "Program to an interface, not an implementation." Это 1994 год, это и просто по дате предшествует статье Мартина, и в его статье есть ссылки на GoF, так что он, очевидно, был в курсе этой работы; но это, все-таки, другой принцип.

Прочитал статью и не понял смысл, после прочтения "Чистая архитектура" все понятно, а тут явно создаётся ощущение что автор немного перепутал. В книге Мартина часто приводиться аналогия с луковицей, и основной посыл, что уровень который расположен ближе к центру ничего не должен знать о уровне который расположен ближе к краю. Но в разработке часто бизнеса логика (уровни ближе к центру) должны взаимодействовать с выше стоящими уровнями. Допустим какая либо библиотека анализирует сигнал с датчика и при определенном событии должна оповестить об этом программу вызвавшию ее. Библиотека это бизнеса логика и она не должна знать ничего о вызвавшем его коде, для этого и используется инверсия зависимости. В библиотеке есть вызов метода интересного класса ( например класса "Инициатор") в котором есть метод "Получить". Когда мы хотим использовать эту библиотеку мы в контракте (в хэдере) видим что класс вызывающий ее должен быть наследован от абстрактного класса "Инициатор" и надо реализовать следующим образом метод "Получить". После всех проделок мы в вызывающем классе (верхнеуровневый код) при создании экземпляра класса бизнеса логики передаём в нее указатель на наш вызывающий класс как указатель на "Инициатор" и запускаем нужный метод.

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

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

Мне кажется, что аналогия со слояму тут не очень подходит.

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

И вот тут аналогия слоя ломается. Потому что люди будут и на урвоне работкников и еще ниже уровнем под роботами, потому чтобы роботы работали тоже нужны будут люди.

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

Публикации