Должна ли инъекция зависимостей происходить за счет инкапсуляции?

128

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

Это раскрывает внедряемую зависимость и нарушает принцип инкапсуляции ООП.

Правильно ли я определил этот компромисс? Как вы справляетесь с этой проблемой?

См. Также мой ответ на мой вопрос ниже.

Urig
источник
7
это очень умный вопрос imho
dfa
5
Чтобы ответить на этот вопрос, сначала нужно обсудить, что означает инкапсуляция . ;)
Джефф Стернал,
2
Инкапсуляция поддерживается интерфейсами. Они раскрывают основные характеристики объекта и скрывают такие детали, как зависимости. Это позволяет нам до некоторой степени «открывать» классы, чтобы обеспечить более гибкую конфигурацию.
Лоуренс Вагерфилд

Ответы:

62

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

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

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

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

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

Николас Блюмхардт
источник
18
Инкапсуляция - это не просто модная терминология. Это реальная вещь с реальными преимуществами, и не имеет значения, считаете ли вы свою программу «компонентно-ориентированной» или «объектно-ориентированной». Инкапсуляция должна защищать состояние вашего объекта / компонента / службы / чего-либо от неожиданного изменения, и IoC действительно снимает часть этой защиты, так что определенно есть компромисс.
Рон Инбар
1
Аргументы, передаваемые через конструктор, по-прежнему попадают в область ожидаемых способов «изменения» объекта: они явно предоставляются, а инварианты вокруг них применяются. Скрытие информации - лучший термин для обозначения типа конфиденциальности, о котором вы говорите, @RonInbar, и это не всегда полезно (из-за этого макароны сложнее распутать ;-)).
Николас Блюмхардт
2
Вся суть ООП состоит в том, что клубок макаронных изделий разделен на отдельные классы, и вам нужно вообще возиться с ним, только если вы хотите изменить поведение этого конкретного класса (именно так ООП снижает сложность). Класс (или модуль) инкапсулирует свои внутренние компоненты, предоставляя удобный общедоступный интерфейс (именно так ООП облегчает повторное использование). Класс, который раскрывает свои зависимости через интерфейсы, усложняет работу своих клиентов и в результате становится менее пригодным для повторного использования. Кроме того, он по своей природе более хрупкий.
Neutrino
1
Независимо от того, как я смотрю на это, мне кажется, что DI серьезно подрывает некоторые из наиболее ценных преимуществ ООП, и мне еще не приходилось сталкиваться с ситуацией, когда я действительно обнаружил, что он используется таким образом, чтобы решить настоящую проблему. существующая проблема.
Neutrino
1
Вот еще один способ сформулировать проблему: представьте, если бы сборки .NET выбрали «инкапсуляцию» и не объявляли, на какие другие сборки они полагаются. Было бы безумно читать документы и просто надеяться, что что-то сработает после загрузки. Объявление зависимостей на этом уровне позволяет автоматическим инструментам справляться с крупномасштабной композицией приложения. Вы должны прищуриться, чтобы увидеть аналогию, но аналогичные силы действительно действуют на уровне компонентов. Есть компромиссы, и YMMV как всегда :-)
Николас
29

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

Классически этот последний случай обрабатывается, как вы говорите, с помощью аргументов конструктора или методов установки. Однако это не обязательно так - я знаю, что последние версии среды Spring DI на Java, например, позволяют аннотировать частные поля (например, с помощью@Autowired ), и зависимость будет установлена ​​с помощью отражения без необходимости раскрывать зависимость через любой из общедоступных методов / конструкторов классов. Возможно, это именно то решение, которое вы искали.

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

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

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

Анджей Дойл
источник
2
Хорошие моменты. Я не рекомендую использовать @Autowired в частных полях; это затрудняет тестирование класса; как вы тогда вставляете моки или заглушки?
lumpynose
4
Я не согласен. DI нарушает инкапсуляцию, и этого можно избежать. Например, с помощью ServiceLocator, которому, очевидно, не нужно ничего знать о классе клиента; ему нужно только знать о реализациях зависимости Foo. Но в большинстве случаев лучше всего просто использовать оператор «новый».
Rogério
5
@Rogerio - Возможно, любая структура DI действует точно так же, как описанный вами ServiceLocator; клиент не знает ничего конкретного о реализациях Foo, а инструмент DI не знает ничего конкретного о клиенте. И использование «нового» намного хуже для нарушения инкапсуляции, поскольку вам нужно знать не только точный класс реализации, но также точные классы и экземпляры всех необходимых ему зависимостей.
Анджей Дойл
4
Использование «нового» для создания экземпляра вспомогательного класса, который часто даже не является общедоступным, способствует инкапсуляции. Альтернативой DI было бы сделать вспомогательный класс общедоступным и добавить общедоступный конструктор или установщик в клиентский класс; оба изменения нарушат инкапсуляцию, обеспечиваемую исходным вспомогательным классом.
Rogério
1
«Это хороший вопрос, но в какой-то момент инкапсуляция в ее чистом виде должна быть нарушена, если для объекта когда-либо будет выполнена его зависимость». Исходная посылка вашего ответа просто неверна. Как утверждает @ Rogério, внутреннее создание зависимости, и любой другой метод, в котором объект внутренне удовлетворяет свои собственные зависимости, не нарушает инкапсуляцию.
Neutrino
17

Это раскрывает внедряемую зависимость и нарушает принцип инкапсуляции ООП.

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

Итак, что нарушает инкапсуляцию?

Наследование делает .

«Поскольку наследование раскрывает для подкласса детали реализации его родителя, часто говорят, что« наследование нарушает инкапсуляцию »». (Банда четырех 1995: 19)

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

Объявление друга в C ++ делает .

Расширение класса в Ruby делает . Просто переопределите строковый метод где-нибудь после того, как строковый класс был полностью определен.

Ну, много чего делает .

Инкапсуляция - хороший и важный принцип. Но не единственный.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}
Правильный геймдев
источник
3
«Это мои принципы, и если они вам не нравятся ... ну, у меня есть другие». (Граучо Маркс)
Рон Инбар
2
Я думаю, что это своего рода суть. Это зависимая инъекция против инкапсуляции. Поэтому используйте зависимую инъекцию только там, где она дает значительные преимущества. Это DI повсюду, что делает DI плохую репутацию
Ричард Тингл
Не уверен, что этот ответ пытается сказать ... Что можно нарушать инкапсуляцию при выполнении DI, что это «всегда нормально», потому что это все равно будет нарушено, или просто то, что DI может быть причиной нарушения инкапсуляции? С другой стороны, в наши дни больше нет необходимости полагаться на общедоступные конструкторы или свойства для внедрения зависимостей; вместо этого мы можем вводить данные в закрытые аннотированные поля , что проще (меньше кода) и сохраняет инкапсуляцию. Таким образом, мы можем использовать оба принципа одновременно.
Rogério
Наследование в принципе не нарушает инкапсуляцию, хотя может нарушить, если родительский класс написан плохо. Другие вопросы, которые вы поднимаете, - это одна довольно второстепенная парадигма программирования и несколько языковых функций, которые имеют мало общего с архитектурой или дизайном.
Neutrino
13

Да, DI нарушает инкапсуляцию (также известную как «скрытие информации»).

Но настоящая проблема возникает, когда разработчики используют это как предлог для нарушения принципов KISS (Keep It Short and Simple) и YAGNI (You Ain't Gonna Need It).

Лично я предпочитаю простые и эффективные решения. Я в основном использую оператор «новый» для создания экземпляров зависимостей с отслеживанием состояния, когда и где они необходимы. Он прост, хорошо инкапсулирован, прост для понимания и тестирования. Так почему не?

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

Хороший контейнер / система для внедрения зависимостей допускает внедрение конструктора. Зависимые объекты будут инкапсулированы, и их вообще не нужно будет открывать публично. Кроме того, при использовании системы DP никто из вашего кода даже не «знает» детали того, как создается объект, возможно, даже включая создаваемый объект. В этом случае больше инкапсуляции, поскольку почти весь ваш код не только защищен от знаний об инкапсулированных объектах, но даже не участвует в построении объектов.

Теперь я предполагаю, что вы сравниваете со случаем, когда созданный объект создает свои собственные инкапсулированные объекты, скорее всего, в своем конструкторе. В моем понимании DP мы хотим снять эту ответственность с объекта и передать ее кому-то другому. С этой целью «кто-то другой», который в данном случае является контейнером DP, действительно обладает глубокими знаниями, которые «нарушают» инкапсуляцию; Преимущество в том, что он извлекает эти знания из самого объекта. Кто-то должен это иметь. Остальная часть вашего приложения этого не делает.

Я бы подумал об этом так: контейнер / система внедрения зависимости нарушает инкапсуляцию, а ваш код - нет. Фактически, ваш код более "инкапсулирован", чем когда-либо.

Билл
источник
3
Если у вас есть ситуация, когда клиентский объект МОЖЕТ напрямую создавать экземпляры своих зависимостей, то почему бы этого не сделать? Это определенно самое простое решение, которое не обязательно снижает тестируемость. Помимо простоты и лучшей инкапсуляции, это также упрощает создание объектов с сохранением состояния вместо синглтонов без состояния.
Rogério
1
В дополнение к тому, что сказал @ Rogério, он также потенциально значительно более эффективен. Не каждому классу, когда-либо созданному в мировой истории, требовалось, чтобы каждая из его зависимостей создавалась на протяжении всего времени существования объекта-владельца. Объект, использующий DI, теряет самый элементарный контроль над своими зависимостями, а именно их время жизни.
Neutrino
5

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

Кажется, есть два основных лагеря того, что означает инкапсуляция:

  1. Все, что связано с объектом, - это метод объекта. Таким образом, Fileобъект может иметь методы для Save, Print, Display, ModifyTextи т.д.
  2. Объект - это свой собственный маленький мир, не зависящий от внешнего поведения.

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

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

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

kyoryu
источник
Я определенно согласен с вами по поводу первого определения. Это также нарушает SoC, что, возможно, является одним из главных грехов программирования и, вероятно, одной из причин, по которой он не масштабируется.
Маркус Стэйд
4

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

Джейсон Уоткинс
источник
1
Потому что ... нет? Если у вас есть средство ведения журнала и у вас есть класс, которому требуется средство ведения журнала, передача регистратора в этот класс не нарушает инкапсуляцию. И это все, что есть инъекция зависимостей.
jrockway
3
Я думаю, вы неправильно понимаете инкапсуляцию. Например, возьмем наивный урок свидания. Внутри он может иметь переменные экземпляра дня, месяца и года. Если бы они были представлены как простые установщики без логики, это нарушило бы инкапсуляцию, так как я мог бы сделать что-то вроде установки месяца на 2 и дня на 31. С другой стороны, если установщики умны и проверяют инварианты, тогда все в порядке , Также обратите внимание, что в последней версии я мог изменить хранилище на дни с 01.01.1970, и ничто из того, что использовало интерфейс, не должно было знать об этом, при условии, что я соответствующим образом переписываю методы дня / месяца / года.
Джейсон Уоткинс,
2
DI определенно нарушает инкапсуляцию / сокрытие информации. Если вы превратите частную внутреннюю зависимость в нечто, представленное в общедоступном интерфейсе класса, то по определению вы нарушите инкапсуляцию этой зависимости.
Rogério
2
У меня есть конкретный пример, в котором, как мне кажется, инкапсуляция нарушается DI. У меня есть FooProvider, который получает «данные foo» из БД, и FooManager, который кэширует их и вычисляет данные поверх провайдера. У меня были потребители моего кода, которые ошибочно обращались к FooProvider за данными, где я бы предпочел, чтобы он был инкапсулирован, чтобы они знали только о FooManager. Это в основном триггер для моего первоначального вопроса.
urig
1
@Rogerio: Я бы сказал, что конструктор не является частью общедоступного интерфейса, поскольку он используется только в корне композиции. Таким образом, зависимость "видна" только корню композиции. Единственная обязанность корня композиции - связать эти зависимости вместе. Таким образом, использование внедрения конструктора не нарушает инкапсуляцию.
Джей Салливан,
4

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

  • Классический объектно-ориентированный объект использует конструкторы для определения открытого контракта «инициализации» для потребителей класса (скрытие ВСЕХ деталей реализации; инкапсуляция). Этот контракт может гарантировать, что после создания у вас будет готовый к использованию объект (т. Е. Никаких дополнительных шагов инициализации, которые нужно запомнить (т. Е. Забыть) пользователем).

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

Теоретический пример:

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

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

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

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

Хуже того, что, если виджет необходим только для 2 из 4 методов этого класса; Я могу понести накладные расходы на создание экземпляра для Widget, даже если он не может использоваться!

Как это скомпрометировать / примирить?

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

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

Как мне добиться гарантированной инициализации в этом мире DI, когда инициализация БОЛЬШЕ, чем ТОЛЬКО внешние зависимости?

shawnT
источник
Обновление: я только что заметил, что Unity 2.0 поддерживает предоставление значений для параметров конструктора (таких как инициализаторы состояния), при этом по-прежнему используя обычный механизм для IoC зависимостей во время resolve (). Возможно, это поддерживают и другие контейнеры? Это решает техническую сложность смешивания состояния init и DI в одном конструкторе, но по-прежнему нарушает инкапсуляцию!
shawnT
Я тебя слышу. Я задал этот вопрос, потому что мне тоже кажется, что две хорошие вещи (DI и инкапсуляция) достигаются за счет другого. Кстати, в вашем примере, где только 2 из 4 методов нуждаются в IWidget, это будет означать, что другие 2 принадлежат другому компоненту IMHO.
urig 04
3

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

Но если вы строите автомобиль, которому нужен объект двигателя определенного типа, тогда у вас есть внешняя зависимость. Вы можете либо создать экземпляр этого движка - например, new GMOverHeadCamEngine () - внутри конструктора объекта автомобиля, сохраняя инкапсуляцию, но создавая гораздо более коварную связь с конкретным классом GMOverHeadCamEngine, либо вы можете внедрить его, позволяя вашему объекту Car работать агностически (и гораздо более надежно), например, на интерфейсе IEngine без конкретной зависимости. Независимо от того, используете ли вы контейнер IOC или простой DI для достижения этого, не имеет значения - дело в том, что у вас есть Автомобиль, который может использовать многие виды движков без привязки к любому из них, что делает вашу кодовую базу более гибкой и менее подвержен побочным эффектам.

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

Дэйв Симс
источник
3

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

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

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

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

dsimcha
источник
2

Я верю в простоту. Применение IOC / внедрения зависимостей в доменных классах не дает никаких улучшений, за исключением того, что код становится более сложным для работы с кодом из-за наличия внешних xml-файлов, описывающих отношение. Многие технологии, такие как EJB 1.0 / 2.0 и struts 1.1, обращаются вспять, уменьшая количество материала, помещаемого в XML, и пытаются поместить их в код в качестве аннотации и т. Д. Таким образом, применение IOC для всех разрабатываемых вами классов сделает код бессмысленным.

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

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

Сентил Кумар Вайтилингам
источник
2

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

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

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

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

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

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

Это я вожу свой Комби на пляж.

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

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

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

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

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

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

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

«предотвращение установки пользователями внутренних данных компонента в недопустимое или несогласованное состояние».

В то же время нельзя сказать, что

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

Обе цитаты взяты из Википедии .

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

Вообще я полностью за Д.И. В этом конкретном (крайнем) примере это кажется мне опасным.

Urig
источник
2

DI нарушает инкапсуляцию для NON-Shared объектов - точка. Общие объекты имеют срок жизни за пределами создаваемого объекта, и поэтому должны быть АГРЕГАТИВНЫ в создаваемый объект. Объекты, которые являются частными для создаваемого объекта, должны быть СОСТАВЛЕНЫ в созданный объект - когда созданный объект уничтожается, он принимает составной объект с собой. Возьмем, к примеру, человеческое тело. Что составлено и что агрегировано. Если бы мы использовали DI, конструктор человеческого тела имел бы сотни объектов. Например, многие органы (потенциально) можно заменить. Но они по-прежнему входят в состав тела. Клетки крови создаются (и разрушаются) в организме каждый день, без необходимости во внешних воздействиях (кроме белков). Таким образом, клетки крови создаются внутри организма - new BloodCell ().

Сторонники DI утверждают, что объект НИКОГДА не должен использовать оператор new. Такой «пуристский» подход нарушает не только инкапсуляцию, но и принцип замещения Лискова для всех, кто создает объект.

Грег Брэнд
источник
1

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

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

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

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

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Клиентский код до сих пор не знает деталей реализации.

Правильный геймдев
источник
Как в вашем примере что-нибудь вводится? (За исключением наименования вашей функции установки)
foo
1

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

Кроме того, за счет использования какой-то функции автоматического подключения (например, Autofac для C #) код становится очень чистым. Создав методы расширения для построителя Autofac, я смог вырезать МНОГО кода конфигурации DI, который мне пришлось бы поддерживать с течением времени по мере роста списка зависимостей.

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

Я думаю, что самоочевидно, что как минимум DI значительно ослабляет инкапсуляцию. В дополнение к этому, следует рассмотреть еще несколько недостатков DI.

  1. Это затрудняет повторное использование кода. Модуль, который клиент может использовать без явного указания зависимостей, очевидно, проще в использовании, чем модуль, в котором клиент должен каким-то образом обнаружить зависимости этого компонента, а затем каким-то образом сделать их доступными. Например, компонент, изначально созданный для использования в приложении ASP, может ожидать, что его зависимости будут предоставляться контейнером DI, который предоставляет экземплярам объектов время жизни, связанное с клиентскими http-запросами. Это может быть непросто воспроизвести на другом клиенте, который не имеет того же встроенного контейнера DI, что и исходное приложение ASP.

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

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

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

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

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

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

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

Нейтрино
источник
0

Я согласен с тем, что в крайнем случае DI может нарушать инкапсуляцию. Обычно DI предоставляет зависимости, которые никогда по-настоящему не инкапсулировались. Вот упрощенный пример из книги Мишко Хевери « Синглтоны - патологические лжецы» :

Вы начинаете с теста CreditCard и пишете простой модульный тест.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

В следующем месяце вы получите счет на 100 долларов. Почему с вас сняли обвинение? Модульный тест затронул производственную базу данных. Внутренние звонки по кредитной карте Database.getInstance(). Рефакторинг CreditCard таким образом, чтобы он принимал DatabaseInterfaceв своем конструкторе, раскрывает факт наличия зависимости. Но я бы сказал, что зависимость никогда не была инкапсулирована с самого начала, поскольку класс CreditCard вызывает внешне видимые побочные эффекты. Если вы хотите протестировать CreditCard без рефакторинга, вы, безусловно, можете наблюдать зависимость.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

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

Крейг П. Мотлин
источник
1
Модульные тесты обычно пишутся автором класса, поэтому с технической точки зрения можно описать зависимости в тестовом примере. Когда позже класс кредитной карты изменится на использование веб-API, такого как PayPal, пользователю потребуется изменить все, если он был отключен. Модульное тестирование обычно проводится с глубоким знанием объекта тестирования (разве не в этом весь смысл?), Поэтому я думаю, что тесты - это скорее исключение, чем типичный пример.
kizzx2
1
Задача DI - избежать описанных вами изменений. Если вы переключитесь с веб-API на PayPal, вы не измените большинство тестов, потому что они будут использовать MockPaymentService, а CreditCard будет построена с помощью PaymentService. У вас будет всего несколько тестов, изучающих реальное взаимодействие между CreditCard и реальной PaymentService, поэтому будущие изменения очень изолированы. Преимущества еще больше для более глубоких графов зависимостей (таких как класс, который зависит от CreditCard).
Крейг П. Мотлин,
@ Крэйг п. Motlin Как CreditCardобъект может измениться с WebAPI на PayPal без изменения чего-либо внешнего по отношению к классу?
Ян Бойд,
@Ian Я упомянул, что CreditCard следует реорганизовать, чтобы включить DatabaseInterface в свой конструктор, который защищает его от изменений в реализациях DatabaseInterfaces. Возможно, это должно быть еще более общим и взять StorageInterface, другой реализацией которого может быть WebAPI. Однако PayPal находится на неправильном уровне, потому что это альтернатива CreditCard. Как PayPal, так и CreditCard могут реализовать PaymentInterface, чтобы защитить другие части приложения от сторонних источников.
Крейг П. Мотлин 05
@ kizzx2 Другими словами, бессмысленно говорить, что кредитная карта должна использовать PayPal. Они альтернативы в реальной жизни.
Крейг П. Мотлин 05
0

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

  1. Класс как есть : то, что вы инкапсулируете, является единственной обязанностью класса. Что умеет делать. Например, сортировка. Если вы вводите какой-то компаратор для заказа, скажем, клиентов, это не является частью инкапсулированной вещи: быстрой сортировки.

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

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

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

Это реализация настроенного экземпляра:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Вот как его использует другой клиентский код:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Он инкапсулирован, потому что если вы измените реализацию (вы измените clientSorterопределение bean-компонента), это не нарушит работу клиента. Может быть, когда вы используете XML-файлы, все написанные вместе, вы видите все детали. Но поверьте, клиентский код ( ClientService) ничего не знает о своем сортировщике.

Гелиос
источник
0

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

public class A { 
    private B b;

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


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

С точки зрения того, кто работает над Aклассом, во втором примере Aгораздо меньше известно о природеthis.b

А без DI

new A()

против

new A(new B())

Человек, просматривающий этот код, знает больше о природе Aво втором примере.

С DI по крайней мере все просочившиеся знания находятся в одном месте.

Том Б
источник