Использование Func вместо интерфейсов для IoC

15

Контекст: я использую C #

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

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

Тем не менее, я никогда не видел, чтобы IoC делал так раньше, что заставляет меня думать, что есть некоторые потенциальные ловушки, которые я могу упустить. Единственное, о чем я могу думать, это небольшая несовместимость с более ранней версией C #, которая не определяет Func, и это не проблема в моем случае.

Есть ли проблемы с использованием универсальных делегатов / функций более высокого порядка, таких как Func для IoC, в отличие от более конкретных интерфейсов?

TheCatWhisperer
источник
2
То, что вы описываете, это функции высшего порядка, важный аспект функционального программирования.
Роберт Харви
2
Я бы использовал делегата вместо a, Funcтак как вы можете назвать параметры, где вы можете указать их намерение.
Стефан Ханке
1
related: stackoverflow: ioc-фабрика-плюсы-и-контрас-для-интерфейса-против-делегатов . Вопрос @TheCatWhisperer носит более общий характер, в то время как вопрос о переполнении стека сводится к специальному случаю «фабрика»
k3b

Ответы:

11

Если интерфейс действительно содержит только одну функцию, не больше, и нет веских оснований вводить два названия (имя интерфейса и имя функции в интерфейсе), используя Funcвместо того, чтобы избежать ненужного кода шаблонного и в большинстве случаев предпочтительнее - точно так же, как когда вы начинаете разрабатывать DTO и узнавать, что ему нужен только один атрибут member.

Я предполагаю, что многие люди более привыкли к использованию interfaces, потому что в то время, когда внедрение зависимостей и IoC становились популярными, не было реального эквивалента Funcклассу в Java или C ++ (я даже не уверен, Funcбыл ли доступен в то время в C # ). Поэтому многие учебники, примеры или учебники по-прежнему предпочитают interfaceформу, даже если использование Funcбудет более элегантным.

Вы можете посмотреть мой предыдущий ответ о принципе разделения интерфейсов и подходе Flow Design от Ralf Westphal. Эта парадигма реализует DI исключительно с Funcпараметрами , по той же причине, о которой вы уже упоминали (и некоторые другие). Итак, как вы видите, ваша идея не совсем новая, совсем наоборот.

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

Док Браун
источник
Эта программа даже не была разработана с учетом принципа сегрегации интерфейсов, в основном они просто используются как абстрактные классы. Это еще одна причина, почему я пошел с этим подходом, интерфейсы плохо продуманы и в основном бесполезны, так как в любом случае их реализует только один класс. Принцип сегрегации интерфейса не нужно доводить до крайности, особенно сейчас, когда у нас есть Func, но, на мой взгляд, он все еще очень важен.
TheCatWhisperer
1
Эта программа даже не была разработана с учетом принципа сегрегации интерфейса - ну, согласно вашему описанию, вы, вероятно, просто не знали, что этот термин можно применить к вашему виду дизайна.
Док Браун
Док, я имел в виду код, созданный моими предшественниками, который я рефакторинг и от которого мой новый класс несколько зависит. Одна из причин, по которой я выбрал этот подход, заключается в том, что в будущем я планирую внести серьезные изменения в другие части программы, включая зависимости этого класса
TheCatWhisperer
1
«... В то время, когда внедрение зависимостей и IoC становились популярными, не было никакого реального эквивалента классу Func» Док, это именно то, на что я почти ответил, но на самом деле не чувствовал себя достаточно уверенно! Спасибо за проверку моего подозрения на этот счет.
Грэм
9

Одно из основных преимуществ, которые я нахожу с IoC, заключается в том, что он позволяет мне называть все мои зависимости посредством именования их интерфейса, и контейнер будет знать, какой из них предоставить конструктору, сопоставляя имена типов. Это удобно и позволяет гораздо больше описательных имен зависимостей, чем Func<string, string>.

Я также часто обнаруживаю, что даже с одной простой зависимостью иногда требуется иметь более одной функции - интерфейс позволяет группировать эти функции вместе, что позволяет самодокументироваться, по сравнению с несколькими параметрами, которые все читаются как Func<string, string>, Func<string, int>,

Определенно бывают моменты, когда полезно просто иметь делегата, который передается как зависимость. Это суждение о том, когда вы используете делегат против интерфейса с очень небольшим количеством участников. Если не совсем понятно, какова цель аргумента, я обычно ошибаюсь в создании самодокументируемого кода; то есть. написание интерфейса.

Erik
источник
Мне пришлось отлаживать чей-то код, когда исходного разработчика больше не было, и я могу сказать вам по первому опыту, что выяснение того, какая лямбда запускается в стеке, - это большая работа и очень неприятный процесс. Если бы оригинальный разработчик использовал интерфейсы, было бы легко найти ошибку, которую я искал.
Свап
Черт, я чувствую твою боль. Тем не менее, в моей конкретной реализации, лямбды - это все строки, указывающие на одну функцию. Они также все генерируются в одном месте.
TheCatWhisperer
По моему опыту, это всего лишь вопрос инструментов. ReSharpers Inspect-> Incoming Calls хорошо справляется со сложением пути вверх по funcs / lambdas.
Кристиан
3

Есть ли проблемы с использованием универсальных делегатов / функций более высокого порядка, таких как Func для IoC, в отличие от более конкретных интерфейсов?

На самом деле, нет. Func - это своего рода интерфейс (в английском значении, а не в C #). «Этот параметр предоставляет X, когда его спрашивают». Func даже имеет преимущество ленивой подачи информации только по мере необходимости. Я делаю это немного и рекомендую это в меру.

Что касается недостатков:

  • Контейнеры IoC часто делают некоторую магию, чтобы связать зависимости каскадным способом, и, вероятно, не будут играть хорошо, когда некоторые вещи есть, Tа некоторые есть Func<T>.
  • Функции имеют некоторую косвенность, поэтому может быть немного сложнее рассуждать и отлаживать.
  • Функции задерживают создание экземпляров, а это означает, что ошибки во время выполнения могут появляться в странные моменты времени или вообще не появляться во время тестирования. Это также может увеличить вероятность проблем с порядком операций и, в зависимости от вашего использования, взаимоблокировки в порядке инициализации.
  • То, что вы передаете в Func, вероятно, будет закрытием, с небольшими накладными расходами и осложнениями, которые влекут за собой.
  • Вызов Func немного медленнее, чем прямой доступ к объекту. (Недостаточно того, что вы заметите в любой нетривиальной программе, но она есть)
Telastyn
источник
1
Я использую IoC-контейнер для создания фабрики традиционным способом, затем фабрика упаковывает методы интерфейсов в лямбды, чтобы обойти проблему, о которой вы упоминали в первом пункте. Хорошие моменты.
TheCatWhisperer
1

Давайте рассмотрим простой пример - возможно, вы вводите средства регистрации.

Впрыскивать класс

class Worker: IWorker
{
    ILogger _logger;

    Worker(ILogger logger)
    {
        _logger = logger;
    }
    void SomeMethod()
    {
        _logger.Debug("This is a debug log statement.");
    }
}        

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

container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();

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

Если разработчику нужна более сложная логика, у него есть весь интерфейс для работы:

    void SomeMethod()
    { 
       if (_logger.IsDebugEnabled) {
           _logger.Debug("This is a debug log statement.");
       }
    }

Внедрение метода

class Worker
{
    Action<string> _methodThatLogs;

    Worker(Action<string> methodThatLogs)
    {
        _methodThatLogs = methodThatLogs;
    }
    void SomeMethod()
    {
        _methodThatLogs("This is a logging statement");
    }
}        

Во-первых, обратите внимание, что параметр конструктора теперь имеет более длинное имя methodThatLogs. Это необходимо, потому что вы не можете сказать, что Action<string>делать. С интерфейсом это было совершенно ясно, но здесь мы должны прибегнуть к использованию именования параметров. По своей природе это кажется менее надежным и трудным для применения во время сборки.

Теперь, как мы вводим этот метод? Ну, контейнер IoC не сделает это за вас. Таким образом, вы оставляете инъекцию явно при создании экземпляра Worker. Это поднимает пару проблем:

  1. Это больше работы, чтобы создать экземпляр Worker
  2. Разработчики, пытающиеся выполнить отладку Worker, обнаружат, что сложнее понять, как вызывается конкретный экземпляр. Они не могут просто обратиться к корню композиции; им придется отслеживать через код.

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

var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );

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

Краткое изложение различий:

  1. Внедрение только метода затрудняет вывод цели, в то время как интерфейс четко сообщает цель.
  2. Внедрение только метода предоставляет меньшую функциональность классу, получающему инъекцию. Даже если вам это не нужно сегодня, оно может понадобиться завтра.
  3. Вы не можете автоматически внедрить только метод, использующий контейнер IoC.
  4. Вы не можете определить из корня композиции, какой конкретный класс работает в конкретном случае.
  5. Это проблема для модульного тестирования самого лямбда-выражения.

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

Джон Ву
источник
Я должен был указать, что класс, который я делаю, является классом логики процесса. Он полагается на внешние классы для получения данных, необходимых для принятия обоснованного решения, а побочный эффект, вызывающий побочный эффект, такой регистратор, не используется. Проблема с вашим примером заключается в том, что это плохое ОО, особенно в контексте использования контейнера IoC. Вместо использования оператора if, который добавляет дополнительную сложность классу, ему просто нужно передать инертный логгер. Кроме того, введение логики в лямбды действительно усложнило бы тестирование ... отказавшись от цели его использования, именно поэтому оно не сделано.
TheCatWhisperer
Лямбды указывают на метод интерфейса в приложении и создают произвольные структуры данных в модульных тестах.
TheCatWhisperer
Почему люди всегда сосредоточены на мелочах того, что явно является произвольным примером? Хорошо, если вы настаиваете на том, чтобы говорить о правильной регистрации, инертный регистратор был бы ужасной идеей в некоторых случаях, например, когда регистрируемая строка дорогая для вычисления.
Джон Ву
Я не уверен, что вы подразумеваете под "лямбда-указателями на метод интерфейса". Я думаю, что вам нужно внедрить реализацию метода, который соответствует подписи делегата. Если метод принадлежит интерфейсу, это является случайным; нет никакой проверки во время компиляции или выполнения, чтобы гарантировать, что это делает. Я неправильно понимаю? Возможно, вы могли бы включить пример кода в свой пост?
Джон Ву
Я не могу сказать, что согласен с вашими выводами здесь. Прежде всего, вы должны связать свой класс с минимальным количеством зависимостей, поэтому использование лямбда-выражений гарантирует, что каждый внедренный элемент - это ровно одна зависимость, а не сочетание нескольких вещей в интерфейсе. Вы также можете поддерживать шаблон построения жизнеспособной Workerодной зависимости за один раз, позволяя каждой лямбда-инъекции вводиться независимо, пока все зависимости не будут удовлетворены. Это похоже на частичное применение в FP. Кроме того, использование лямбда-выражений помогает устранить неявное состояние и скрытую связь между зависимостями.
Аарон Эшбах
0

Рассмотрим следующий код, который я написал давно:

public interface IPhysicalPathMapper
{
    /// <summary>
    /// Gets the physical path represented by the relative URL.
    /// </summary>
    /// <param name="relativeURL"></param>
    /// <returns></returns>
    String GetPhysicalPath(String relativeURL);
}

public class EmailBuilder : IEmailBuilder
{
    public IPhysicalPathMapper PhysicalPathMapper { get; set; }
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templateRelativeURL, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(this.PhysicalPathMapper.GetPhysicalPath(templateRelativeURL));

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

Он занимает относительное расположение в файле шаблона, загружает его в память, отображает тело сообщения и собирает объект электронной почты.

Вы можете посмотреть IPhysicalPathMapperи подумать: «Есть только одна функция. Это может быть Func». Но на самом деле проблема в том, что IPhysicalPathMapperэтого не должно быть. Гораздо лучшим решением является просто параметризация пути :

public class EmailBuilder : IEmailBuilder
{
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templatePath, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(templatePath);

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

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

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

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

jpmc26
источник