Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

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

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

"Поскольку наследование предоставляет подкласс для подробностей его родительской реализации, он часто говорит, что" наследование прерывает инкапсуляцию ". (Gang of Four 1995: 19)

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

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

Расширение класса в Ruby делает. Просто переопределите метод string где-то после полного определения класса строки.

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

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

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

Ответ 4

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

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

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

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 9

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

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

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

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

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

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

Ответ 10

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

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

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

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

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

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

Оба кавычка из wikipedia.

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

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

Ответ 11

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

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

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

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

Ответ 12

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

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

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

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

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

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

Чтобы я водил мой Комби на пляж.

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

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

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

Ответ 13

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

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

Ответ 14

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

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

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

Ответ 15

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

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

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

Ответ 16

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

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

Ответ 17

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

Вы начинаете с теста CreditCard и пишите простой unit test.

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

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

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

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

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

Ответ 18

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

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

  • Конфигурированная функциональность. Если вы хотите предоставить готовые к использованию функции, то вы не предоставляете класс 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) ничего не знают о сортировщике.

Ответ 19

Возможно, стоит упомянуть, что 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, по крайней мере, все, что утечка знаний находится в одном месте.

Ответ 20

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

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

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

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

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

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

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

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

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