Внедрение зависимости ; передовая практика по сокращению стандартного кода

25

У меня простой вопрос, и я даже не уверен, что у него есть ответ, но давайте попробуем. Я пишу на C ++ и использую инъекцию зависимости, чтобы избежать глобального состояния. Это работает довольно хорошо, и я не часто сталкиваюсь с неожиданным / неопределенным поведением.

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

Ничто не сравнится с хорошим примером, так что поехали:

У меня есть класс TimeFactory, который создает объекты Time.

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

В моем приложении много классов должны создавать объекты Time. Иногда эти классы глубоко вложены.

Допустим, у меня есть класс A, который содержит экземпляры класса B, и так далее вплоть до класса D. Класс D должен создавать объекты Time.

В моей наивной реализации я передаю TimeFactory конструктору класса A, который передает его конструктору класса B и так далее до класса D.

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

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

Что вы думаете ?

Dinaiz
источник
5
Я отправил этот вопрос программистам вместо stackoverflow, потому что, как я понял, программисты больше предназначены для связанных с дизайном квестов, а stackoverflow - для вопросов «когда вы находитесь перед вашим компилятором». Извините заранее, если я ошибаюсь.
Dinaiz
2
аспектно-ориентированное программирование также хорошо подходит для этих задач - en.wikipedia.org/wiki/Aspect-oriented_programming
Е.Л. Юсубов
Является ли класс A фабрикой для классов B, C и D? Если нет, то почему он создает их экземпляры? Если только класс D нуждается в объектах Time, зачем вам вводить любой другой класс с TimeFactory? Класс D действительно нуждается в TimeFactory, или мог бы только экземпляр Time быть введен в него?
Симораман
D должен создавать объекты времени, с информацией, которую должен иметь только D. Я должен подумать об остальной части вашего комментария. В моем коде может быть проблема "кто создает кого"
Dinaiz
«кто создает кого» решается контейнерами IoC, см. подробности в моем ответе… «кто создает кто» - это один из лучших вопросов, который вы можете задать при использовании Dependency Injection.
GameDeveloper

Ответы:

23

В моем приложении много классов нужно для создания объектов Time

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

Так что мое предложение: не используйте DI для общих типов данных, таких как Time. Напишите модульные тесты для самого Timeкласса, а когда это будет сделано, используйте его везде в своей программе, точно так же, как stringкласс, или vectorкласс, или любой другой класс стандартной библиотеки lib. Используйте DI для компонентов, которые должны быть отделены друг от друга.

Док Браун
источник
Вы правильно поняли. «Читаемость вашей программы снизится на порядок». Это именно то, что случилось, да. При условии, что я все еще хочу использовать фабрику, это не так уж плохо, чтобы сделать ее синглтоном тогда?
Dinaiz
3
@Dinaiz: идея заключается в том, чтобы принять тесную связь между Timeдругими частями вашей программы. Таким образом, вы можете принять и принять жесткую связь с TimeFactory. Однако я бы избегал иметь единственный глобальный TimeFactoryобъект с состоянием (например, информацию о локали или что-то в этом роде), что могло бы вызвать неприятные побочные эффекты для вашей программы и затруднить общее тестирование и повторное использование. Либо сделайте его безгосударственным, либо не используйте его как одиночный.
Док Браун
На самом деле у него должно быть состояние, поэтому, когда вы говорите «Не используйте его как одиночный», вы имеете в виду «продолжать использовать инъекцию зависимости, то есть передавать экземпляр каждому объекту, который нуждается в этом», верно?
Dinaiz
1
@Dinaiz: честно говоря, это зависит от состояния, типа и количества частей вашей программы, Timeа также TimeFactoryот степени «эволюционируемости», которая вам понадобится для будущих расширений, связанных с TimeFactory, и так далее.
Док Браун
Хорошо, я постараюсь сохранить зависимость, но у меня будет более продуманный дизайн, который не требует столько шаблонов!
Большое спасибо Doc
4

Что вы подразумеваете под «Я теряю всю гибкость и удобочитаемость, которые, я полагаю, получаю с помощью внедрения зависимостей» - DI не о читабельности. Речь идет о разъединении зависимости между объектами.

Похоже, у вас есть класс A, создающий класс B, класс B, создающий класс C, и класс C, создающий класс D.

То, что вы должны иметь, это класс B, вводимый в класс A. Класс C, вводимый в класс B. Класс D, вводимый в класс C.

C0deAttack
источник
Я могу быть о читабельности. Если у вас есть глобальная переменная, «магически» появляющаяся в середине метода, это не поможет понять код (с точки зрения обслуживания). Откуда эта переменная? Кому это принадлежит? Кто это инициализирует? И так далее ... Во-вторых, вы, вероятно, правы, но я не думаю, что это применимо в моем случае. Спасибо, что
нашли
D Я повышаю читабельность главным образом потому, что «new / delete» будет волшебным образом удален из вашего кода. Если вам не нужно беспокоиться о динамическом распределении кода, его легче прочитать.
GameDeveloper
Также не забудьте сказать, что это делает код немного сильнее, потому что он больше не может делать предположения о том, «как» создается зависимость, ему просто «нужно». И это облегчает поддержание класса .. посмотрите мой ответ
GameDeveloper
3

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

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

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

molyss
источник
Весна для Java, и я использую C ++. Но почему 2 человека понизили тебя? В вашем посте есть пункты, с которыми я не согласен, но все же ...
Dinaiz
Я собираюсь предположить, что понижение было вызвано не ответом на вопрос как таковой, возможно, так же как упоминанием Весны. Тем не менее, предложение об использовании Singleton было правильным, на мой взгляд, и, возможно, не отвечает на вопрос - но предлагает правильную альтернативу и поднимает интересную проблему дизайна. По этой причине у меня +1.
Фергус в Лондоне
1

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

То, что вам, вероятно, нужно, это рамки для DI. Наличие сложных иерархий не обязательно означает плохой дизайн, но если вам нужно внедрить восходящую TimeFactory (от A до D) вместо прямой инъекции в D, то, вероятно, что-то не так с тем, как вы делаете Dependency Injection.

Синглтон? Нет, спасибо. Если вам нужен только один istance, сделайте его общим для контекста вашего приложения (использование контейнера IoC для DI, такого как Infector ++, требует просто привязать TimeFactory как один istance), вот пример (кстати, C ++ 11, но так и C ++. Возможно, переместим на C ++ 11? Вы получаете приложение без утечек бесплатно):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Хорошая особенность контейнера IoC заключается в том, что вам не нужно передавать фабрику времени до D. Если вашему классу "D" нужна фабрика времени, просто укажите фабрику времени в качестве параметра конструктора для класса D.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

как вы видите, вы вводите TimeFactory только один раз. Как использовать «А»? Очень просто, каждый класс вводится, строится в основном или заводится с завода.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

каждый раз, когда вы создаете класс A, он будет автоматически (ленивый istantiation) вводиться со всеми зависимостями вплоть до D, а D будет добавляться с TimeFactory, поэтому, вызывая только 1 метод, вы получаете готовую иерархию (и даже сложные иерархии решаются таким образом). удаление БОЛЬШОГО кода пластины котельной): вам не нужно называть «новый / удалить», и это очень важно, потому что вы можете отделить логику приложения от кода клея.

D может создавать объекты времени с информацией, которую может иметь только D

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

Кто кого создает? Контейнер IoC создает экземпляры и фабрики, фабрики создают остальное (фабрики могут создавать разные объекты с произвольными параметрами, поэтому вам не нужно состояние для фабрики). Вы по-прежнему можете использовать фабрики как оболочки для IoC-контейнера: вообще говоря, Injectin для IoC-контейнера очень плох и аналогичен использованию сервисного локатора. Некоторые люди решили проблему, упаковав Контейнер IoC с фабрикой (это не является строго необходимым, но имеет преимущество в том, что Контейнер решает иерархию, и все ваши фабрики становятся еще проще в обслуживании).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Также не злоупотребляйте внедрением зависимостей, простые типы могут быть просто членами класса или локальными переменными области видимости. Это кажется очевидным, но я видел, как люди вводили «std :: vector» только потому, что была структура DI, которая позволяла это делать. Всегда помните закон Деметры: «Вводите только то, что вам действительно нужно ввести»

Разработчик игр
источник
Ух ты, я не видел твоего ответа раньше, извини! Очень информативно, сэр! Проголосовал
Dinaiz
0

Ваши классы A, B и C также должны создавать экземпляры Time или только класс D? Если это только класс D, то A и B не должны ничего знать о TimeFactory. Создайте экземпляр TimeFactory внутри класса C и передайте его классу D. Обратите внимание, что под «созданием экземпляра» я не обязательно имею в виду, что класс C должен отвечать за создание экземпляра TimeFactory. Он может получить DClassFactory из класса B, а DClassFactory знает, как создать экземпляр Time.

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

rmaruszewski
источник
-1

Я реализовал еще одну инфраструктуру внедрения зависимостей C ++, которая недавно была предложена для повышения - https://github.com/krzysztof-jusiak/di - библиотека не содержит макросов (бесплатно), только заголовки, C ++ 03 / C ++ 11 / C ++ 14 библиотека, обеспечивающая безопасность типов, время компиляции, внедрение макрос-зависимых конструкторов.

krzychoo
источник
3
Ответы на старые вопросы о P.SE - плохой способ рекламировать ваш проект. Учтите, что существуют десятки тысяч проектов, которые могут иметь отношение к какому-либо вопросу. Если бы все их авторы разместили такие здесь, качество ответов сайта резко упало бы.