Pull to refresh

Comments 15

inject_as_object делает привязку к одной конкретной реализации, а что, если захочется иметь две разные реализации?
Также что делать, если нужно использовать разные параметры конструктора для разных клиентов?
Мне кажется более удобным и гибким решением делать inject на уровне объектов, а не классов.
Есть несколько замечаний:
1. Ну идея определять время жизни объекта в том месте где ты его инжектишь — как минимум странная. Клас по идее вообще не должно волновать — какое время жизни у объекта — главное он есть и доступен.
2.
Объект времени исполнения (Runtime). Все клиенты используют один и тот же объект, который может изменяться во время работы программы.

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

Вообще статическая типизация и DI дружат очень хорошо (как раз с динамической намного больше проблем). Просто в С++ не достает средств интроспекции чтобы сделать все красиво. Посмотрите например на Dagger для Java (http://square.github.io/dagger/) он мало того что работает поверх языка со статической типизацией, как еще и проверят весь граф объектов на момент компиляции.
3. Инжектить в поля — это моветон. Мешает использовать ваш объект без DI фреймворка, и делает зависимости класса менне явными (плюс много черной магии)

А как вместо этого лучше, в методы? Я просто для себя затрудняюсь решить этот вопрос, в своём коде чаще всего обращаюсь напрямую к фабричным методам, но мне понравилась идея автора об отражении зависимости в интерфейсе (пусть и закрытом) класса.
Лучше всего — в конструктор. Это семантически более понятно и логично. И класс всегда можно использовать вне зависимости от DI фреймворка.
но мне понравилась идея автора об отражении зависимости в интерфейсе (пусть и закрытом) класса

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


Но представьте, что будет, если зависимостей хотя бы штук 5, а конструкторов несколько.

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

1. Конструктор один
2. Параметров у него немного
3. Зависимостей тоже одна-две
Но представьте, что будет, если зависимостей хотя бы штук 5, а конструкторов несколько.

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

Очень странная мысль. Как можно отделить конструирование объекта от его зависимостей? Это значит что у нас точно есть состояние когда объект недособран? Как это состояние отлавливать? Вставлять кучу проверок на null? Мне этот подход кажется очень странным. Как минимум это убирание проверок с момента компиляции в рантайм. Тут начинаем терять часть прелестей статической типизации. Вкупе с моделью памяти в C++, где неинециализированный указатель это не null — есть реальная возможность словить кучу сложно-уловимых ошибок.
Это наталкивает меня на мысль что класс отвечает за слишком много вещей

Трудно в общем случае сказать, слишком или нет. Возможно, предметная область реально сложная, и маленькими классами не обойтись. Может, приходится дорабатывать унаследованную систему. Я же не говорю, что это решение на все случаи жизни. Большие классы время от времени случаются. Обычно нельзя сказать «Тут проблема дизайна — ничего делать не буду, я работаю только с идеальными решениями. И вообще — давайте все перепишем с нуля!» :)

Это значит что у нас точно есть состояние когда объект недособран

Нет такого состояния. Все зависимости внедряются вызовами конструкторов полей по умолчанию. Т.е. к началу выполнения тела любого конструктора (и тем более нестатического метода) все будет инициализировано. Причем, если зависимость не определена, будет ошибка компиляции. Сообщение об ошибке получится не очень очевидным, но нужно помнить, что в статье приводится лишь идея, это даже близко не готовый продукт
Обычно нельзя сказать «Тут проблема дизайна — ничего делать не буду, я работаю только с идеальными решениями. И вообще — давайте все перепишем с нуля!» :)

По моей практике как раз такой подход и приводит к переписыванию с нуля :)
Заложить время на рефакторинг в следующей фиче, которая будет касаться этого класса — имхо более правильный подход, чем аа с этим классом все равно уже ничего не сделать — так пусть маячит тут куском кода на 400-500 строк.
Нет такого состояния. Все зависимости внедряются вызовами конструкторов полей по умолчанию. Т.е. к началу выполнения тела любого конструктора (и тем более нестатического метода) все будет инициализировано.

Вот тут вы меня убедили. Опасного состояния действительно нет. Тогда остается лишь вопрос сментики.
Но представьте, что будет, если зависимостей хотя бы штук 5, а конструкторов несколько.
Для разрешения этой проблемы придуман паттерн Builder.
Спасибо за уточнение.
Да, передача зависимостей в конструктор мне тоже нравится, при использовании обычно смущало только возрастающее количество параметров. Но я согласен с вашим замечанием в комментариях о том, что это дополнительный сигнал проверить, не перегружен ли класс функциональностью. А это скорее плюс.
Вызов конструктора пришлось записать в несколько непривычном (по крайней мере, для меня) виде, потому что вариант
В том числе поэтому придумали uniform initialization:
static T instance{args...};
Мне понравилось! Спасибо, было интересно.

Вот несколько вещей которые бы я поменял:
* #define inject(I, Name) я бы реализовал в виде класса. В этом случае мы получим уменьшение объема бинарного файла, за счет уменьшения количества различных struct I##Proxy объявленных в разных классах:

// Где-то вне класса
tempalte <class I>
struct IProxy: private injector<I>::Factory {
I* operator->() {
return &this->get();
}
};

Теперь пользователь сможет инжектить зависимости так:
IProxy<IWork> mWork1;

* inject_as_share я бы реализовал с использованием std::optional/boost::optional. Это позволило бы избавиться от динамической аллокации (new)
В данной реализации все модули должны быть скомпилированы с одной настройкой фабрик.
А в managed-языках доводилось использовать такую конфигурацию, когда создаётся несколько DI-контейнеров с разными политиками, и одинаковые классы могут создаваться из разных контейнеров, получая разные зависимости.

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

class Employee
{
public:
    Employee(IContainer& cfg) : mWork1(cfg.resolve<IWork>()), mWork2(cfg.resolve<Work2>()) { ... }
private:
    IWork* mWork1;
    Work2* mWork2;
};

int main()
{
    Container cfg;
    cfg.RegisterSingleton<IWork, Work1>();
    Employee* e = cfg.resolve<Employee>();
    // ...
}
Sign up to leave a comment.

Articles