Контекст: я использую C #
Я разработал класс, и чтобы изолировать его и упростить модульное тестирование, я передаю все его зависимости; он не создает объектов внутри. Однако вместо того, чтобы ссылаться на интерфейсы для получения необходимых данных, я использую ссылки общего назначения, возвращающие требуемые данные / поведение. Когда я внедряю его зависимости, я могу сделать это только с помощью лямбда-выражений.
Мне кажется, что это лучший подход, потому что мне не нужно утомительно издеваться во время юнит-тестирования. Кроме того, если окружающая реализация имеет фундаментальные изменения, мне нужно будет только изменить класс фабрики; никаких изменений в классе, содержащем логику, не потребуется.
Тем не менее, я никогда не видел, чтобы IoC делал так раньше, что заставляет меня думать, что есть некоторые потенциальные ловушки, которые я могу упустить. Единственное, о чем я могу думать, это небольшая несовместимость с более ранней версией C #, которая не определяет Func, и это не проблема в моем случае.
Есть ли проблемы с использованием универсальных делегатов / функций более высокого порядка, таких как Func для IoC, в отличие от более конкретных интерфейсов?
источник
Func
так как вы можете назвать параметры, где вы можете указать их намерение.Ответы:
Если интерфейс действительно содержит только одну функцию, не больше, и нет веских оснований вводить два названия (имя интерфейса и имя функции в интерфейсе), используя
Func
вместо того, чтобы избежать ненужного кода шаблонного и в большинстве случаев предпочтительнее - точно так же, как когда вы начинаете разрабатывать DTO и узнавать, что ему нужен только один атрибут member.Я предполагаю, что многие люди более привыкли к использованию
interfaces
, потому что в то время, когда внедрение зависимостей и IoC становились популярными, не было реального эквивалентаFunc
классу в Java или C ++ (я даже не уверен,Func
был ли доступен в то время в C # ). Поэтому многие учебники, примеры или учебники по-прежнему предпочитаютinterface
форму, даже если использованиеFunc
будет более элегантным.Вы можете посмотреть мой предыдущий ответ о принципе разделения интерфейсов и подходе Flow Design от Ralf Westphal. Эта парадигма реализует DI исключительно с
Func
параметрами , по той же причине, о которой вы уже упоминали (и некоторые другие). Итак, как вы видите, ваша идея не совсем новая, совсем наоборот.И да, я сам использовал этот подход для производственного кода, для программ, которым необходимо обрабатывать данные в виде конвейера с несколькими промежуточными этапами, включая модульные тесты для каждого из этапов. Итак, из этого я могу дать вам личный опыт, чтобы он работал очень хорошо.
источник
Одно из основных преимуществ, которые я нахожу с IoC, заключается в том, что он позволяет мне называть все мои зависимости посредством именования их интерфейса, и контейнер будет знать, какой из них предоставить конструктору, сопоставляя имена типов. Это удобно и позволяет гораздо больше описательных имен зависимостей, чем
Func<string, string>
.Я также часто обнаруживаю, что даже с одной простой зависимостью иногда требуется иметь более одной функции - интерфейс позволяет группировать эти функции вместе, что позволяет самодокументироваться, по сравнению с несколькими параметрами, которые все читаются как
Func<string, string>
,Func<string, int>
,Определенно бывают моменты, когда полезно просто иметь делегата, который передается как зависимость. Это суждение о том, когда вы используете делегат против интерфейса с очень небольшим количеством участников. Если не совсем понятно, какова цель аргумента, я обычно ошибаюсь в создании самодокументируемого кода; то есть. написание интерфейса.
источник
На самом деле, нет. Func - это своего рода интерфейс (в английском значении, а не в C #). «Этот параметр предоставляет X, когда его спрашивают». Func даже имеет преимущество ленивой подачи информации только по мере необходимости. Я делаю это немного и рекомендую это в меру.
Что касается недостатков:
T
а некоторые естьFunc<T>
.источник
Давайте рассмотрим простой пример - возможно, вы вводите средства регистрации.
Впрыскивать класс
Я думаю, что довольно ясно, что происходит. Более того, если вы используете контейнер IoC, вам даже не нужно вводить что-либо явно, вы просто добавляете в свой корень композиции:
При отладке
Worker
разработчику просто нужно обратиться к корню композиции, чтобы определить, какой конкретный класс используется.Если разработчику нужна более сложная логика, у него есть весь интерфейс для работы:
Внедрение метода
Во-первых, обратите внимание, что параметр конструктора теперь имеет более длинное имя
methodThatLogs
. Это необходимо, потому что вы не можете сказать, чтоAction<string>
делать. С интерфейсом это было совершенно ясно, но здесь мы должны прибегнуть к использованию именования параметров. По своей природе это кажется менее надежным и трудным для применения во время сборки.Теперь, как мы вводим этот метод? Ну, контейнер IoC не сделает это за вас. Таким образом, вы оставляете инъекцию явно при создании экземпляра
Worker
. Это поднимает пару проблем:Worker
Worker
, обнаружат, что сложнее понять, как вызывается конкретный экземпляр. Они не могут просто обратиться к корню композиции; им придется отслеживать через код.Как насчет того, если нам нужна более сложная логика? Ваша техника только выставляет один метод. Теперь я полагаю, что вы можете испечь сложные вещи в лямбду:
но когда вы пишете свои модульные тесты, как вы тестируете это лямбда-выражение? Это анонимно, поэтому ваш фреймворк не может создать его экземпляр напрямую. Может быть, вы можете найти какой-то умный способ сделать это, но это, вероятно, будет больше PITA, чем с помощью интерфейса.
Краткое изложение различий:
Если вы согласны со всем вышеперечисленным, тогда можно вводить только метод. В противном случае я бы посоветовал вам придерживаться традиции и внедрить интерфейс.
источник
Worker
одной зависимости за один раз, позволяя каждой лямбда-инъекции вводиться независимо, пока все зависимости не будут удовлетворены. Это похоже на частичное применение в FP. Кроме того, использование лямбда-выражений помогает устранить неявное состояние и скрытую связь между зависимостями.Рассмотрим следующий код, который я написал давно:
Он занимает относительное расположение в файле шаблона, загружает его в память, отображает тело сообщения и собирает объект электронной почты.
Вы можете посмотреть
IPhysicalPathMapper
и подумать: «Есть только одна функция. Это может бытьFunc
». Но на самом деле проблема в том, чтоIPhysicalPathMapper
этого не должно быть. Гораздо лучшим решением является просто параметризация пути :Это поднимает целый ряд других вопросов по улучшению этого кода. Например, может быть, он просто должен принять
EmailTemplate
, а затем, возможно, он должен принять предварительно обработанный шаблон, а затем, возможно, он должен быть просто встроенным.Вот почему я не люблю инверсию контроля как распространенную модель. Обычно это считается богоподобным решением для написания всего вашего кода. Но на самом деле, если вы используете его повсеместно (а не экономно), он значительно ухудшает ваш код , побуждая вас вводить множество ненужных интерфейсов, которые используются полностью задом наперед. (Обратно в том смысле, что вызывающая сторона должна действительно отвечать за оценку этих зависимостей и передачу результата, а не сам класс, вызывающий вызов.)
Интерфейсы должны использоваться экономно, а инверсия управления и внедрение зависимостей также должны использоваться экономно. Если у вас их огромное количество, ваш код становится намного сложнее расшифровать.
источник