Когда НЕ применять принцип инверсии зависимости?

43

В настоящее время я пытаюсь выяснить, ТВЕРДЫЙ. Таким образом, принцип обращения зависимостей означает, что любые два класса должны взаимодействовать через интерфейсы, а не напрямую. Пример: если class Aесть метод, который ожидает указатель на объект типа class B, то этот метод должен фактически ожидать объект типа abstract base class of B. Это также помогает для открытия / закрытия.

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

Причина, по которой я скептически отношусь, заключается в том, что мы платим некоторую цену за следование этому принципу. Скажем, мне нужно реализовать функцию Z. После анализа я пришел к выводу, что функция Zсостоит из функциональности A, Bи C. Я создать фасад класс Z, который, через интерфейсы, использует классы A, Bи C. Я начинаю кодировать реализацию и в какой-то момент понимаю, что задача на Zсамом деле состоит из функциональности A, Bи D. Теперь мне нужно пересмотреть Cинтерфейс, Cпрототип класса и написать отдельный Dинтерфейс и класс. Без интерфейсов только класс должен был бы быть заменен.

Другими словами, чтобы что-то изменить, мне нужно поменять 1. вызывающую сторону 2. интерфейс 3. декларацию 4. реализацию. В реализации, напрямую связанной с Python, мне нужно будет изменить только реализацию.

Vorac
источник
13
Инверсия зависимостей - это просто техника, поэтому ее следует применять только в случае необходимости ... нет предела степени, в которой она может быть применена, поэтому, если вы примените ее повсюду, вы получите мусор: как и в любой другой ситуации техника
Фрэнк Хайлеман
Проще говоря, применение некоторых принципов проектирования программного обеспечения зависит от способности беспощадно проводить рефакторинг при изменении требований. Считается , что из них интерфейсная часть лучше всего отражает договорные инварианты проекта, тогда как ожидается, что исходный код (реализация) допускает более частые изменения.
Rwong
@rwong Интерфейс захватывает договорные инварианты, только если вы используете язык, который поддерживает договорные инварианты. В общих языках (Java, C #) интерфейс - это просто набор сигнатур API. Добавление лишних интерфейсов только ухудшает дизайн.
Фрэнк Хайлеман
Я бы сказал, что вы поняли это неправильно. DIP - это предотвращение зависимостей времени компиляции от компонента «высокого уровня» до компонента «низкого уровня», чтобы позволить повторное использование компонента высокого уровня в других контекстах, где вы бы использовали другую реализацию для низкого уровня -уровневая составляющая; это делается путем создания абстрактного типа на высоком уровне, который реализуется компонентами низкого уровня; поэтому компоненты высокого и низкого уровня зависят от этой абстракции. В конце концов, компоненты высокого и низкого уровня обмениваются данными через интерфейс, но это не суть DIP.
Рожерио

Ответы:

87

Во многих мультфильмах и других средствах массовой информации силы добра и зла часто изображаются ангелом и демоном, сидящим на плечах персонажа. В нашей истории здесь, вместо добра и зла, мы имеем ТВЕРДЫЕ на одном плече, а ЯГНИ (Тебе это не понадобится!) На другом.

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

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

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

как зовут
источник
19
Мое первое знакомство с SOLID было около 15 лет назад на новой системе, которую мы строили. Мы все выпили за помощь. Если кто-нибудь упомянул что-нибудь, что звучало бы как YAGNI, мы были бы как "Пффффт ... плебей". Я имел честь (ужас?) Наблюдать за развитием этой системы в течение следующего десятилетия. Это стало громоздким беспорядком, который никто не мог понять, даже мы, основатели. Архитекторы любят SOLID. Люди, которые действительно зарабатывают себе на жизнь, любят ЯГНИ. Ни один из них не идеален, но YAGNI ближе к идеалу, и он должен быть вашим по умолчанию, если вы не знаете, что делаете. :-)
Calphool
11
@NWard Да, мы сделали это в проекте. Офигел с этим. Теперь наши тесты невозможно прочитать или сохранить, отчасти из-за чрезмерного издевательства. Вдобавок ко всему, из-за внедрения зависимостей, сзади очень тяжело перемещаться по коду, когда вы пытаетесь что-то выяснить. ТВЕРДЫЙ не серебряная пуля. ЯГНИ не серебряная пуля. Автоматизированное тестирование не является серебряной пулей. Ничто не может избавить вас от тяжелой работы: подумать о том, что вы делаете, и принять решение о том, будет ли это помогать или мешать вашей или чужой работе.
jpmc26
22
Здесь много анти-ТВЕРДЫХ настроений. SOLID и YAGNI - это не два конца спектра. Они похожи на координаты X и Y на графике. Хорошая система имеет очень мало избыточного кода (YAGNI) и следует твердым принципам.
Стивен
31
Мех, (а) я не согласен с тем, что SOLID = предприимчивость, и (б) весь смысл SOLID в том, что мы склонны быть крайне плохими предсказателями того, что будет необходимо. Я должен согласиться с @Stephen здесь. YAGNI говорит, что мы не должны пытаться предвидеть будущие требования, которые четко не сформулированы сегодня. SOLID говорит, что мы должны ожидать, что дизайн будет развиваться с течением времени и применять определенные простые методы, чтобы облегчить его. Они не являются взаимоисключающими; оба метода для адаптации к изменяющимся требованиям. Настоящие проблемы возникают, когда вы пытаетесь спроектировать для непонятных или очень отдаленных требований.
Aaronaught
8
«Вы можете легко поменять чтение из файла для сетевого потока» - это хороший пример, когда слишком упрощенное описание DI вводит людей в заблуждение. Люди иногда думают (по сути), «этот метод будет использовать File, так что вместо этого потребуется IFile, работа сделана». Тогда они не могут легко заменить сетевой поток, потому что они чрезмерно требовали интерфейс, и в IFileметоде даже не используются операции , которые не применяются к сокетам, поэтому сокет не может быть реализован IFile. Одной из вещей, для которой DI не является серебряной пулей, является изобретение правильных абстракций (интерфейсов) :-)
Стив Джессоп
11

По словам непрофессионала:

Применять DIP легко и весело . Неправильный дизайн с первой попытки - не достаточная причина для отказа от DIP.

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

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

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

Тулаинс Кордова
источник
5
Я не знаю, какие языки или IDE используются OP, но в VS 2013 невероятно просто работать с интерфейсами, извлекать интерфейсы и реализовывать их, и критично, если используется TDD. Нет никаких дополнительных затрат на разработку с использованием этих принципов.
Стивенбайер
Почему этот ответ говорит о DI, если вопрос был о DIP? DIP - это концепция 1990-х годов, а DI - 2004 года. Они очень разные.
Роджерио
1
(Мой предыдущий комментарий был предназначен для другого ответа; игнорируйте его.) «Программирование на интерфейсах» гораздо более общее, чем DIP, но оно не о том, чтобы каждый класс реализовывал отдельный интерфейс. И это только облегчает «тестирование / макетирование», если инструменты тестирования / макетирования страдают от серьезных ограничений.
Роджерио
@ Rogério Обычно при использовании DI не каждый класс реализует отдельный интерфейс. Один интерфейс, реализуемый несколькими классами, является общим.
Тулаинс Кордова
@ Rogério Я исправил свой ответ, каждый раз, когда я упоминал DI, я имел в виду DIP.
Тулаинс Кордова
9

Используйте инверсию зависимостей там, где это имеет смысл.

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

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

Есть два места, где DI должен автоматически использоваться по моему мнению:

  1. В модулях, предназначенных для расширения. Если целью модуля является расширение его и изменение поведения, то имеет смысл запрограммировать DI с самого начала.

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

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

DI - отличный инструмент, но, как и любой инструмент *, он может быть использован чрезмерно или неправильно.

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


источник
3
Что если «ваша проблема» - это дыра в стене? Пила не удалит это; это сделало бы это хуже. ;)
Мейсон Уилер
3
@MasonWheeler с мощной и забавной в использовании пилой, «дыра в стене» может превратиться в «дверной проем», что является полезным активом :-)
1
Разве вы не можете использовать пилу, чтобы сделать патч для отверстия?
Джефф
Несмотря на некоторые преимущества наличия Stringтипа, не расширяемого пользователем , во многих случаях было бы полезно использовать альтернативные представления, если бы у типа был хороший набор виртуальных операций (например, скопировать подстроку в указанную часть short[], сообщить, подстрока содержит или может содержать только ASCII, попытаться скопировать подстроку, которая, как считается, содержит только ASCII, в указанную часть a byte[]и т. д.) Это очень плохо, фреймворки не имеют своих строковых типов, реализующих какие-либо полезные интерфейсы, связанные со строками.
суперкат
1
Почему этот ответ говорит о DI, если вопрос был о DIP? DIP - это концепция 1990-х годов, а DI - 2004 года. Они очень разные.
Рожерио
5

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

Причина, по которой я скептически отношусь, заключается в том, что мы платим некоторую цену за следование этому принципу. Скажем, мне нужно реализовать функцию Z. После анализа я прихожу к выводу, что функция Z состоит из функций A, B и C. Я создаю класс фаскад Z, который через интерфейсы использует классы A, B и C. Я начинаю кодировать реализации, и в какой-то момент я понимаю, что задача Z на самом деле состоит из функциональных возможностей A, B и D. Теперь мне нужно отказаться от интерфейса C, прототипа класса C и написать отдельный интерфейс и класс D. Без интерфейсов только класс должен был бы заменить волну.

Чтобы по-настоящему воспользоваться DIP, вы сначала должны создать класс Z, и он будет вызывать функциональность классов A, B и C (которые еще не разработаны). Это дает вам API для классов A, B и C. Затем вы создаете классы A, B и C и заполняете детали. По сути, вы должны создавать необходимые абстракции при создании класса Z, основываясь исключительно на том, что нужно классу Z. Вы даже можете написать тесты вокруг класса Z, прежде чем классы A, B или C даже будут написаны.

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

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

Класса D никогда не будет, потому что вы решили, что Z нужны A, B и C до того, как они были написаны. Изменение требований - это совсем другая история.

Стивен
источник
5

Краткий ответ «почти никогда», но есть несколько мест, где DIP не имеет смысла:

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

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

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

  4. Любые объекты, которые предоставляются в качестве внешнего API и должны создавать другие объекты, детали реализации которых вы не хотите раскрывать публично. Это подпадает под общую категорию «дизайн библиотеки отличается от дизайна приложения». Библиотека или фреймворк могут свободно использовать DI внутри, но в конечном итоге придется выполнять какую-то реальную работу, иначе это не очень полезная библиотека. Допустим, вы разрабатываете сетевую библиотеку; Вы действительно не хотите, чтобы потребитель мог предоставить собственную реализацию сокета. Вы можете внутренне использовать абстракцию сокета, но API, который вы предоставляете вызывающим, собирается создавать свои собственные сокеты.

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

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

Aaronaught
источник
Как насчет "когда вы работаете на динамическом языке"?
Кевин
Нет? Я много работаю в JavaScript, и он все еще применяется там же. Тем не менее, «O» и «I» в SOLID могут стать немного размытыми.
Aaronaught
Хм ... Я считаю, что первоклассные типы Python в сочетании с типизацией утиных клавиш делают это менее необходимым.
Кевин
DIP не имеет абсолютно ничего общего с системой типов. И каким образом «первоклассные типы» уникальны для python? Когда вы хотите протестировать что-то изолированно, вы должны заменить test double для его зависимостей. Эти тестовые двойники могут быть альтернативными реализациями интерфейса (в языках со статической типизацией), или они могут быть анонимными объектами или альтернативными типами, которые имеют одинаковые методы / функции на них (типизированная утка). В обоих случаях вам все еще нужен способ фактически заменить экземпляр.
Aaronaught
1
Python @Kevin едва ли был первым языком, который обладал либо динамической типизацией, либо свободными издевательствами. Это также совершенно не имеет значения. Вопрос не в том, что тип объекта, а в том, как / где этот объект создан. Когда объект создает свои собственные зависимости, вы вынуждены проводить модульное тестирование того, что должно быть деталями реализации, выполняя ужасные вещи, такие как заглушка конструкторов классов, о которых публичный API не упоминает. А забывание о тестировании, смешанном поведении и построении объекта просто приводит к тесной связи. Утиная печать не решает ни одну из этих проблем.
Ааронаут
2

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

  • у вас есть пара C / CImpl или IC / C, и только одна реализация этого интерфейса
  • подписи в вашем интерфейсе и реализации совпадают один на один (нарушая принцип DRY)
  • Вы часто меняете C и CImpl одновременно.
  • C является внутренним для вашего проекта и не используется вне вашего проекта как библиотека.
  • вы разочарованы тем, что F3 в Eclipse / F12 в Visual Studio приводит вас к интерфейсу вместо реального класса

Если это то, что вы видите, возможно, было бы лучше, если бы Z напрямую вызвал C и пропустил интерфейс.

Кроме того, я не думаю, что оформление методов с помощью внедрения зависимостей / динамического прокси-сервера (Spring, Java EE) аналогично истинному SOLID DIP - это больше похоже на детали реализации того, как оформление методов работает в этом техническом стеке. Сообщество Java EE считает улучшением то, что вам не нужны пары Foo / FooImpl, как вы привыкли ( ссылка ). В отличие от этого, Python поддерживает оформление функций как первоклассную языковую функцию.

Смотрите также этот пост в блоге .

wrschneider
источник
0

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

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

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

Для более полного объяснения см. Эту статью :

Суть правильного применения принципа обращения зависимостей заключается в следующем:

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

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

Сору
источник