Это Funcцелесообразно использовать в качестве аргумента ctor при применении Dependency Injection? - PullRequest
13 голосов
/ 27 ноября 2011

Пример:

public class BusinessTransactionFactory<T>  where T : IBusinessTransaction
{
    readonly Func<Type, IBusinessTransaction> _createTransaction;

    public BusinessTransactionFactory(Func<Type, IBusinessTransaction> createTransaction)
    {
        _createTransaction = createTransaction;
    }

    public T Create()
    {
        return (T)_createTransaction(typeof(T));
    }
}

Код установки контейнера, использующий то же самое:

public class DependencyRegistration : Registry
{
    public DependencyRegistration()
    {
        Scan(x =>
        {
            x.AssembliesFromApplicationBaseDirectory();
            x.WithDefaultConventions();
            x.AddAllTypesOf(typeof(Repository<>));
            x.ConnectImplementationsToTypesClosing(typeof(IRepository<>));
        });

        Scan(x =>
        {
            x.AssembliesFromApplicationBaseDirectory();
            x.AddAllTypesOf<IBusinessTransaction>();
            For(typeof(BusinessTransactionFactory<>)).Use(typeof(BusinessTransactionFactory<>));
            For<Func<Type, IBusinessTransaction>>().Use(type => (IBusinessTransaction)ObjectFactory.GetInstance(type));
        });


        For<ObjectContext>().Use(() => new ManagementEntities());
    }
}

Что ты думаешь?

Ответы [ 7 ]

25 голосов
/ 28 ноября 2011

Механика

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

Концепции

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

Независимо от того, достигается ли эта цель путем введения делегатов вместоИнтерфейсы сомнительны.

Делегаты как зависимости

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

Например, один тип сам по себе не имеет большого смысла здесь:

public BusinessTransactionFactory(Func<Type, IBusinessTransaction> createTranscation)

К счастью, имя createTranscation все еще подразумевает роль, которую играет делегат, но просто подумайте (ради аргумента), насколько читабельным был бы этот конструктор, если бы автор был менее осторожен:

public BusinessTransactionFactory(Func<Type, IBusinessTransaction> func)

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

Обнаруживаемость и сопоставимость

Еще одна проблема связана с обнаруживаемостью и сопоставимостью.когда дело доходит до типов, реализующих зависимости.В качестве примера, оба они реализуют общедоступные Func<Type, IBusinessTransaction>:

t => new MyBusinessTransaction()

и

public class MyBusinessTransactionFactory
{
    public IBusinessTransaction Create(Type t)
    {
        return new MyBusinessTransaction();
    }
}

Однако, в случае класса, почти случайно, что конкретный не виртуальныйCreate метод соответствует желаемому делегату. Это очень компонуемо, но не очень доступно для обнаружения .

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

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

1: 1 интерфейсы по сравнению с RAP

Некоторые люди заметили, чтопри попытке использовать DI они получают множество интерфейсов 1: 1 (например, IFoo и соответствующий класс Foo).В этих случаях интерфейс (IFoo) кажется избыточным, и кажется заманчивым просто избавиться от интерфейса и использовать вместо него делегат.

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

Заключение

Интерфейсы - это не просто механика.Они явно моделируют роли в базе кода приложения.Центральные роли должны быть представлены интерфейсами, тогда как одноразовые фабрики и их аналог могут использоваться и реализовываться как делегаты.

5 голосов
/ 27 ноября 2011

Мне лично не нравится видеть Func<T> аргументы в качестве зависимостей в моих классах, потому что я думаю, что это делает код менее читабельным, но я знаю, что многие разработчики не согласны со мной. Некоторые контейнеры, такие как Autofac, даже позволяют разрешать Func<T> делегатов (возвращая () => container.Resolve<T>()).

Однако, есть два случая, когда я не возражаю вводить Func<T> делегатов:

  1. Когда класс, который принимает аргумент конструктора Func<T>, находится в Root Composition , потому что в этом случае это часть конфигурации контейнера, и я не рассматриваю эту часть дизайна применение.
  2. Когда это Func<T> вводится в приложении одного типа. Когда в зависимости от того же Func<T> запускается больше служб, для удобства чтения было бы лучше создать специальный интерфейс I[SomeType]Factory и внедрить его.
4 голосов
/ 27 ноября 2011

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

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

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

3 голосов
/ 27 ноября 2011

@ Ответ Стивена очень хорошо продуман; лично я нахожу ограниченное использование Func<T> аргументов для чтения.

Чего я не понимаю, так это значения BusinessTransactionFactory<T> и IBusinessTransactionFactory<T>. Это просто обертки вокруг делегата. Предположительно, вы вводите IBusinessTransactionFactory<T> где-то еще. Почему бы просто не ввести туда делегата?

Я думаю о Func<T>Func<T, TResult> и т. Д.) Как о фабриках. Для меня у вас есть фабричный класс, реализующий фабричный интерфейс, обертывающий фабричный делегат, и это кажется излишним.

2 голосов
/ 29 ноября 2011

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

В случае делегата Func вы объявляете зависимость от определенной структуры метода, но не более того. Вы не можете определить диапазон допустимых входных значений (предварительные условия), ограничения ожидаемого результата (постусловия) или то, что они точно означают, глядя на эту структуру. Например, является ли null приемлемым возвращаемым значением? И если это так, то как это следует интерпретировать?

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

1 голос
/ 27 ноября 2011

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

0 голосов
/ 29 ноября 2011

ЕСЛИ вы вводите Func, я думаю, что код будет ленивым, поэтому в некоторых случаях, если ваш класс зависит от множества других зависимостей, может быть, лучше ввести Funct в этом случае. Я прав.

Извините за мой плохой английский

...