Використання Func замість інтерфейсів для IoC


15

Контекст: я використовую C #

Я створив клас, а для того, щоб виділити його та полегшити тестування, я проходжу всі його залежності; він не має об'єктів миттєвого встановлення внутрішньо. Однак замість посилання на інтерфейси для отримання потрібних йому даних, я маю на ньому посилання на функцій загального призначення, які повертають потрібні йому дані / поведінку. Коли я ввожу її залежності, я можу це просто зробити за допомогою лямбда-виразів.

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

Однак я ніколи не бачив, щоб IoC робив такий спосіб раніше, що змушує мене думати, що є якісь потенційні підводні камені, які я можу бракувати. Єдине, про що я можу придумати, - це незначна несумісність з більш ранньою версією C #, яка не визначає Func, і це не проблема в моєму випадку.

Чи є якісь проблеми з використанням загальних функцій делегатів / вищого порядку, таких як Func для IoC, на відміну від більш конкретних інтерфейсів?


2
Те, що ви описуєте, - це функції вищого порядку, важлива грань функціонального програмування.
Роберт Харві

2
Я б скористався делегатом замість того, що Funcви можете назвати параметри, де ви можете вказати їх наміри.
Стефан Ханке

1
related: stackoverflow: ioc-factory-pro-and-contras-for-interface-vers-delegati . @TheCatWhisperer питання більш загальне, тоді як питання stackoverflow звужується до особливого випадку "фабрика"
k3b

Відповіді:


11

Якщо інтерфейс містить лише одну функцію, не більше, і немає поважних причин вводити два імені (ім'я інтерфейсу та ім'я функції всередині інтерфейсу), використовуючи Funcнатомість, можна уникнути непотрібного кодового коду і в більшості випадків бажано - так само, як коли ви починаєте розробляти DTO і визнаєте, йому потрібен лише один атрибут члена.

Я думаю, що більшість людей звикли до використання interfaces, тому що в той час, коли ін'єкція залежності та IoC ставали популярнішими, не було реального еквівалента Funcкласу на Java або C ++ (я навіть не впевнений, чи Funcбув доступний на той час у C # ). Тому багато навчальних посібників, прикладів чи підручників все ж віддають перевагу interfaceформі, навіть якщо використання Funcбуло б більш елегантним.

Ви можете заглянути в мою колишню відповідь про принцип сегрегації інтерфейсу та підхід Ралфа Вестфала Flow Design. Ця парадигма реалізує виключноFunc параметри DI з цією ж причиною, яку ви вже згадали самі (та ще деякі). Отже, як бачите, ваша ідея зовсім не нова, а навпаки.

І так, я використовував цей підхід власноруч, для виробничого коду, для програм, які потребували обробки даних у вигляді конвеєра з декількома проміжними етапами, включаючи тести одиниць для кожного з етапів. Тож я можу дати вам досвід з перших рук, що це може працювати дуже добре.


Ця програма навіть не була розроблена з урахуванням принципу сегрегації інтерфейсів, вони в основному просто використовуються як абстрактні класи. Це ще одна причина, чому я пішов з таким підходом, інтерфейси недостатньо продумані і здебільшого марні, оскільки лише один клас їх реалізує. Принцип сегрегації інтерфейсу не слід сприймати до крайності, особливо зараз, коли ми маємо функцію, але, на мою думку, це все ще дуже важливо.
TheCatWhisperer

1
Ця програма навіть не була розроблена з урахуванням принципу сегментації інтерфейсів - ну, відповідно до вашого опису, ви, напевно, просто не знали, що цей термін може бути застосований до вашого типу дизайну.
Док Браун

Док., Я мав на увазі код, зроблений моїми попередниками, який я переробляю, і від якого мій новий клас дещо залежить. Частина причини, по якій я пішов з таким підходом, полягає в тому, що я планую в майбутньому внести значні зміни до інших частин програми, включаючи залежності цього класу
TheCatWhisperer

1
"... У той час, коли ін'єкції залежності та IoC ставали популярними, не було справжнього еквівалента класу Func." Док, саме це я майже відповів, але насправді не відчував достатньо впевненості! Дякуємо, що підтвердили мою підозру щодо цього.
Грем

9

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

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

Однозначно є випадки, коли корисно просто мати делегата, який передається як залежність. Це судження про те, коли ви використовуєте делегата проти інтерфейсу з дуже малою кількістю членів. Якщо тільки не зрозуміло, яка мета аргументу, я зазвичай помиляюся на стороні створення коду самодокументування; тобто. написання інтерфейсу.


Мені довелося налагоджувати чийсь код, коли оригінального реалізатора вже не було, і я можу вам сказати з першого досвіду, що дізнатися, яка лямбда виконується там, де внизу стека, - це велика робота і справді неприємний процес. Якби оригінальний реалізатор використовував інтерфейси, було б дуже легко знайти помилку, яку я шукав.
обмін

клапоть, я відчуваю твій біль. Однак, в моєму конкретному виконанні, лямбда - це один вкладиш, який вказує на одну функцію. Вони також генеруються в одному місці.
TheCatWhisperer

На мій досвід, це лише питання інструментарію. ReSharpers Inspect-> Вхідні дзвінки справляють хорошу роботу в складі шляху вгору до функцій / лямбдів.
Крістіан

3

Чи є якісь проблеми з використанням загальних функцій делегатів / вищого порядку, таких як Func для IoC, на відміну від більш конкретних інтерфейсів?

Не зовсім. Func - це власний інтерфейс (в англійському значенні, а не в C #). "Цей параметр - це те, що постачає X, коли його запитують." Функція навіть має перевагу, коли ліниво надає інформацію лише за потреби. Я роблю це трохи і рекомендую це в помірних розмірах.

Щодо недоліків:

  • Контейнери IoC часто роблять якусь магію, щоб підключити залежності в каскадному вигляді, і, ймовірно, не будуть грати добре, коли деякі речі є, Tа деякі є Func<T>.
  • Функції мають деяку непрямість, тому їх можна трохи важче обґрунтувати та налагодити.
  • Функції затримують інстанціювання, тобто помилки виконання можуть з’являтися в дивні часи або взагалі не виникати під час тестування. Це також може збільшити шанси на порядок випусків операцій і залежно від використання, тупики в порядку ініціалізації.
  • Те, що ви переходите у Func, швидше за все, буде закриттям, з невеликими накладними витратами та ускладненнями.
  • Викликати функцію трохи повільніше, ніж безпосередньо отримувати доступ до об'єкта. (Мало того, що ви помітите в будь-якій нетривіальній програмі, але вона є)

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залежності за один раз, дозволяючи кожній лямбда вводитись самостійно, поки всі залежності не будуть задоволені. Це схоже на часткове застосування у ПП. Крім того, використання лямбда допомагає усунути неявний стан та приховане сполучення між залежностями.
Аарон М. Ешбах

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шаблон, а потім, можливо, він повинен прийняти попередньо викладений шаблон, а тоді, можливо, він повинен бути просто вкладеним.

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

Інтерфейси повинні використовуватися ощадливо, а інверсія контролю та введення залежності також повинна використовуватися нещільно. Якщо у вас є велика кількість, ваш код стає набагато складніше розшифрувати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.