Нужно ли вводить зависимость за счет инкапсуляции? - PullRequest
121 голосов
/ 17 июня 2009

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

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

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

Пожалуйста, смотрите мой ответ на мой собственный вопрос ниже.

Ответы [ 21 ]

59 голосов
/ 26 ноября 2009

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

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

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

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

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

29 голосов
/ 17 июня 2009

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

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

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

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

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

17 голосов
/ 25 апреля 2010

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

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

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

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

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

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

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

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

Ну, много вещей делает .

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

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}
13 голосов
/ 22 июля 2009

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

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

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

5 голосов
/ 17 июня 2009

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

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

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

4 голосов
/ 25 февраля 2010

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

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

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

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

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

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

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

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

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

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

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

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

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

4 голосов
/ 17 июня 2009

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

4 голосов
/ 25 апреля 2010

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

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

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

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

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

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

3 голосов
/ 14 января 2010

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

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

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

2 голосов
/ 07 июля 2011

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

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

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

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

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

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

Это я еду на Комби на пляж.

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

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

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

...