Pull to refresh

Comments 62

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

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

Это оправдано архитектурно, т.к. компоненты использующие DD могут использовать друг друга, а, следовательно, ранжировщики смогут получить доступ не только к данным, зарегистрированным в текушем контексте, но и к данным зарегистрированным во внешнем контексте по отношению к TopBuilder.

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


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

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


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

Дизайн, ага.
Хотела бы вставить свои 5 копеек.

Опыт показывает, что создавать поставщиков по примеру:
public interface IRankerProvider
    {
        IEnumerable<IRanker> GetRankers();
    }



нет ни какой необходимости при использовании DI.
На много проще управляться с агрегатами и композиторами, имея в системе всегда только один оценщик.

Для демонстрации примера возьму ваш
  public interface IRanker
    {
        int Rank(ITeam team);
    }


И композитор будет вот такой:
public class RangerComposition : IRanker
{
   private IRanger[] rangers;

   public RangerComposition(IRanger[] rangers)
   {
       this.rangers = rangers;
   }

   pubic int Rank(ITeam team)
   {
        // Только для примера
        return _rangers.First().Rank(team);
   }
}


Тем самым система упрощается в понимании и тестировании и избавляет от желания использовать приведение типов для нахождения нужно компонента. Создаёт возможность за один раз использовать больше чем один оценщик итд.

Моим примером я избавила вас как минимум от двух не нужных компонентов:
закрытого и не тестируемого BuilerWorker а так же от IRankerProvider

Возможно вы не знаете одну из особенностей Unity контейтера:

container.RegisterType<IModule, ModuleCompiosition>(); //default
container.RegisterType<IModule, One>("One");
container.RegisterType<IModule, Two>("Two");
container.RegisterType<IModule, Three>("Two");


Дак вот, если предствить, что конструктор ModuleCompiosition принимает массив IModule[] то при попытке сделать:

container.Resolve<IModule>(); // без указания имени регистрации


То, Unity подсунет туда все остальные IModule которые были указаны поимённо при регистрации.

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

В зависимости от используемого контейнера, может дополнительно понадобиться примитив поставщика оценщиков
Упомянутая возможность Unity мне хорошо известна и была использована в первую очередь. Проблема может возникнуть, если оценщики зависят друг от друга, именованые регистрации в Unity, к сожалению, друг о друге ничего не знают. Дефолтная реализация IRankerProvider делает именно то, что вы написали — разрешает массив. Но, сам интерфейс делается для того, чтобы поведеление можно было изменить. Можно ещё зарегистрировать InjectionFactory для массива. Как говорится, по желанию и никакого приведения типов.

BuilerWorker в даной ситуации больше похож на паттерн «MethodObject» и ничто не мешает его тестировать, при соответствующем изменении видимости.

Моим примером я избавила вас как минимум от двух не нужных компонентов:
закрытого и не тестируемого BuilerWorker а так же от IRankerProvider


Но вы не решили проблему, с которой я начал — как в ранжировщики передавать «опциональные» параметры.

По какому критерию выбираются «опциональные» параметры?
Задача любого конкретного ранжировщика — выставить оценку команде. Какие данные нужны конкретному ранжировщику, это детали его реализации и к задаче реализации компонента не имеют. Это хорошая абстракция. Но, ранжировщикам потенциально могут потребоваться все данные, которые получает на вход в качестве параметров метод BuildTop. Применяя шаблон Data Dependency мы позволяем любой реализации ранжировщика получить доступ к этим данным, но не обязываем её это делать.

В случае, когда никому из ранжировщиков эти данные не нужны — Data Dependency применять не нужно.

Шаблон полезно иметь в арсенале на случай, если такое требование появится позже. В этом случае мы просто применим рефакторинг «Convert method to Method Object» и шаблон Data Dependency. Изменения интерфейса IRanker не потребуется. Таким образом, мы удовлетворим новое требование и сохраним совместимость с имеющимися реализациями.
Ужасное решение. Интерфейсы на то и придуманы, чтобы показывать контракт.
Когда неявно подразумевается, что в контейнере должны лежать данные для вызова метода, это крайне неочевидно для реализующего интерфейс человека.

Как костыль для существующей системы, которую лень рефакторить, хм, может и сойдёт, но проектировать так нельзя.
А ещё можно сделать так, чтобы для реализующего человека при одном взгляде на интерфейс, который он реализует стало понятно, что, лежащие в контейнере данные являются частью контракта, и на какие именно данные он в праве рассчитывать.
А сделать это очень просто…
Интересно, как?
Аттрибутами на интерфейсе, на на которые есть четкая документация.
Есть ещё маркерные интерфейсы.
Это не просто, и не очевидно.
Только в первый раз ;-)
При чтении кода этот «первый раз» будет регулярно. Я в своем-то коде вынужден разбираться, несмотря ни на что, а тут речь о чужом.
Когда-то при чтении кода вы впервые столкнулись с Dependency Injection, и, скорее всего, впервые столкнувшись с ним, вы на мгновение подумали, что это не просто и не очевидно.
Я до сих пор думаю, что это не просто и не очевидно. Просто плюсы перевешивают.
То же самое я думаю и я.
Вот только плюсы вашего решения пока не видны.
Они, так же как и плюсы Dependency Injection становятся отчетливее, когда зависимостей по данным становится очень много.
Передача лишнего параметра в данном случае видится мне аналогией конструирования объекта через new с передачей параметра. Это нормально, до тех пор, пока мы не ловим себя на том, что нам приходится разрешать параметры параметров и параметры параметров параметров, чтобы передать их в конструктор.
Судя по вашей аналогии, вы не очень понимаете, в чем смысл Dependency Injection. Если что, «конструирование объекта через new с передачей параметра» — это как раз DI и есть.
Я всегда думал, что смысл DI не в конструировании объектов. С этим прекрасно справляются обычные фабрики. Я всегда думал, что DI нужен для того, чтобы позволить компоненту ничего не знать про то, от чего зависит компопент, от которого он сам зависит (я говорю про зависимости зависимостей).
Это аналогично принципу «вассал моего вассала — не мой вассал» и сильно упрощает жизнь.

Делегирование конструирования объектов на контейнер, это то, чем мы платим (да это цена, ибо это не очевидно) за избавление от проблемы зависимостей зависимостей, и многих других.
Нет, смысл DI — в том, чтобы реализовать IoC (собственно, это один из двух существующих шаблонов реализации). А смысл IoC — в том, чтобы развязать абстрацию и ее реализацию, и зависеть только от абстракций.
Да.
С точки зрения абстракции мне удобнее не знать, какие именно данные нужны конкретному IRanker чтобы выставить оценку. Это не имеет для алгоритма ранжирования никакого значения. Зачем же я должен передавать в качестве параместра всё что может понадобиться какой-то из реализацй, но скорее всего не понадобится?
Не для сокрытия ли этого как раз и нужен IoC?

С точки зрения абстракции мне удобнее не знать, какие именно данные нужны конкретному IRanker чтобы выставить оценку.

Вам удобнее. Это еще не значит, что это правильнее.

Это не имеет для алгоритма ранжирования никакого значения.

А вот это уже не правда. Алгоритм конкретного IRanker зависит от этого.

Зачем же я должен передавать в качестве параместра всё что может понадобиться какой-то из реализацй, но скорее всего не понадобится?

Вы все равно это передаете. Просто не в вызов, а в конструктор. Суть от этого не меняется.

Обычно для решения вашей проблемы банально вводят контекст операции.
Алгоритм конкретного IRanker зависит от этого.

Да. Но мы хотим зависеть только от обстракции.

Вы все равно это передаете. Просто не в вызов, а в конструктор. Суть от этого не меняется.


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

Получить откуда? Из воздуха? Тогда не было бы никакого вбрасывания данных, конкретные реализации брали бы источники данных, и получали бы данные оттуда. Но нет, вы должны сначала вбросить конкретные данные, относительно которых будет идти ранжирование, и поэтому вы все равно от них зависите.
Но сразу после того как я инжектирую данные в контейнер я могу забыть об этом и, при использовании любого конктерного сконструированного IRanker, не думать более об этом при каждом вызове.
Более того, я поделил обязанности по передаче констекста и собственно ранжированию между разными классами.
Но сразу после того как я инжектирую данные в контейнер я могу забыть об этом и, при использовании любого конктерного сконструированного IRanker, не думать более об этом при каждом вызове.

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

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

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

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

По сути, вы используете дочерний контейнер как контекст-бэг.

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

Погода на Марсе — тоже?

Благо список всего, что им можно получить четко обозначен контрактом.

Каким контрактом?

Т.к. компонент в этом случае не заработает в принципе, даже если данные не нужны никому.

Это почему, интересно?

И не пройдет ни один юнит тест.

Юнит-тесты тут вообще не при чем, корректность вбрасывания проверяется интеграционными.

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

По факту, это означает, что вы используете обычный контекст, просто извращенным способом.

Смысл называть это отдельным шаблоном?
Погода на Марсе — тоже?

Нет. Только то что передано в качастве параметров методоу BuildTop
Каким контрактом?

Описанием метода BuildTop и соответствующими аттрибуттами на его параметрах, которые я, к сожалению, оставил, за рамками поста.
Юнит-тесты тут вообще не при чем, корректность вбрасывания проверяется интеграционными.

Корректность передачи контекста тоже интеграционными?
Я говорю про тесты на способность реализации метода BuildTop выполнять его функцию. Если теперь, из-за использования Data Injection они стали интеграционными, то ок.

По факту, это означает, что вы используете обычный контекст, просто извращенным способом.

Смысл называть это отдельным шаблоном?

Этот шаблон решает ту же проблему что и контекст, но на мой взгляд, делает это удобнее. Но решает он её иначе. Поэтому это отдельный шаблон.
Нет. Только то что передано в качастве параметров методоу BuildTop

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

Описанием метода BuildTop

Откуда зависимость (IRanker) знает об этом методе?

Я говорю про тесты на способность реализации метода BuildTop выполнять его функцию.

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

Да. Методы интерфейсов, выполнение которых подразумевает инжектирование данных (BuildTop) помечаеются аттрибутами говорящими «Я инжектирую это, это и это».
Интерфейсы компонентов, реализация которых подразумевает разрешение данных из контейнера (IRanker) помечаются аттрибутом «Я ожидаю данные этого, этого и этого типов из контейнера».

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

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

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

А вообще — двусторонняя связанность, потребитель знает о зависимости, а зависимость знает о том, что потребитель в нее вбросит. Как раз то, чего IoC пытается избежать.
Ну и да, какой с этих атрибутов толк? У вас есть статическая проверка того, что при любой активации зависимости эти данные вброшены в контекст

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

А вообще — двусторонняя связанность, потребитель знает о зависимости, а зависимость знает о том, что потребитель в нее вбросит. Как раз то, чего IoC пытается избежать.

Я знаю, что это плохо. Но передача контекста параметром с этой точки зрения не лучше. Одному компоненту прийдется знать что надо передавать контекст, а другому его получать. Этого не избежать.
Чего я избежал — так это необходимости сообщать об этой зависимости всем посредникам между ITopBuilder и IRanker. К таковым может относиться композит из всех реализаций IRanker.
Статической быть не может,

Кстати, на этот счет я погорячился. Её можно сделать. Но пока её нет :-)
динамическую делает сам контейнер.

… то есть вы не узнаете о некорректной настройке вплоть до резолва всех зависимостей. Круто и очень удобно.

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

Но передача контекста параметром с этой точки зрения не лучше.

Лучше. В этом случае знание односторонне — все знают о контексте, контекст не знает ни о ком.
Лучше. В этом случае знание односторонне — все знают о контексте, контекст не знает ни о ком

А ещё все знают о том, что все знают о контексте, и что его надо всем передавать…

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

Это способ документирования кода. Эти аттрибуты нужны в качестве спецификации для разработчика.
А ещё все знают о том, что все знают о контексте, и что его надо всем передавать…

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

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

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

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

Разве что надо помнить о среде выполнения — не везде функция может продолжиться в том же потоке, где прервалась. Но на то есть HttpContext.Items и т.п., специфичное для среды.
Вообще-то Unity помимо прочего гарантирует освобождение ресурсов и время жизни объектов в контейнере. И не привязано к потоку.
Unity помимо прочего гарантирует освобождение ресурсов и время жизни объектов в контейнере

Правда? Кто вам это сказал?
При вызове Dispose у контейнера освобождается всё что в контейнере заргистрировано.
Это относится и к дочернему контейнеру.
Конечно же можно забыть вызвать Dispose у дочернего контейнера, а можно забыть сделать дочерний контейнер. А можно ещё сделать десять других ошибок.
При вызове Dispose у контейнера освобождается всё что в контейнере заргистрировано.

Правда? Кто вам это сказал?
Угу. Вот и обратите внимание, что это работает только для конкретного LifetimeManager, а не для всех объектов.
В своем посте я других не использовал. В контексте обсуждаемого — мое утверждение верно. Я любым обсуждениям вне темы — не место в этой ветке.
Тот факт, что вы не использовали других в своем посте, еще не означает, что их не бывает, и кому-то они не понадобятся.

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

Это убивает всю прелесть интерфейсов — проверку параметров в compile-time.
Такой атрибут для разработчика это просто комментарий, который будут забывать обновлять. Есть опасность, что ошибки в этих аннотациях-атрибутах могут всплыть очень не скоро.
Такая проблема есть.
В классе который должен инжектировать данные в контейнер можно забыть инжектировать данные. На это можно написать валидатор, который будет проверять наличие в теле метода вызова Register.

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

Допустим даже, это можно сделать легко и очевидно (что явно не так).
Но это ограничит дальнейшие оптимизации и рефакторинги. Например, подготовку контейнера, в который кладётся 100500 данных (включая погоду на марсе), я бы вынес в отдельную функцию. А ещё можно полунастроенные контейнеры передавать в функции, чтобы повторно не настраивать для множества вызовов с одинаковыми доп. параметрами. В таких сценариях метод, вызывающий сервис, не будет содержать в своём теле вызовы RegisterInstance.
Например, подготовку контейнера, в который кладётся 100500 данных (включая погоду на марсе), я бы вынес в отдельную функцию.

Это и вынесено в отдельную функцию. BuildTop сама ничего помимо этого не делает. Сама логика вынесена в Method Object. В контейнер кладется не всё (включая погоду на марсе), а только данные в контексте. Этот набор не может быть шире набора параметров метода. Собственно на параметры метода интерфейса и навешиваются аттрибуты.

А ещё можно полунастроенные контейнеры передавать в функции, чтобы повторно не настраивать для множества вызовов с одинаковыми доп. параметрами. В таких сценариях метод, вызывающий сервис, не будет содержать в своём теле вызовы RegisterInstance.

Ничего не понял. Нужен пример.
Пример:
int GetAverageRank()
{
    var childcontainer = _container.CreateChildContainer();
    childcontainer.RegisterInstance(teams);
    childcontainer.RegisterInstance(count);
    return (CalculateFavoriteRank(childcontainer) + CalculateOutsiderRank(childcontainer))/2;
}
int CalculateFavoriteRank(IUnityContainer c)
{
    c.RegisterInstance("velocity", GetFavoriteTeamVelocity());
    var ranker = c.Resolve<IRanker>();
    return ranker.Rank(GetFavoriteTeam());
}
int CalculateOutsiderRank(IUnityContainer c)
{
    c.RegisterInstance("velocity", GetOutsiderTeamVelocity());
    var ranker = c.Resolve<IRanker>();
    return ranker.Rank(GetOutsiderTeam());
}
Здесь мы общие параметры (teams, count) кладём в контейнер сразу, а зависимые от контекста — ближе к вызову.
Какую проблему вы пытаетесь решить регистрируя параметр в контейнере для одного единственного вызова?
Это должен быть параметр а Data Dependency здесь не место.
А CalculateOutsiderRank и CalculateFavoriteRank это вообще две разные реализации IRanker, а не методы ранжировщика. А velocity — вообще свойство команды.
Прошу прощения. Я неправильно выразился.
В вашем примере небыло и намека на DataDependency.
Это был откровенный говнокод.
я немного покопипастил из статьи :)
Но, тем не менее, код ваш к статье отношения не имеет.
Sign up to leave a comment.

Articles