Как инверсия зависимостей связана с функциями высшего порядка?

41

Сегодня я только что увидел эту статью, в которой описана актуальность принципа SOLID в разработке F #.

F # и принципы дизайна - SOLID

И, обращаясь к последнему - «Принцип инверсии зависимостей», автор сказал:

С функциональной точки зрения эти контейнеры и концепции внедрения могут быть решены с помощью простой функции более высокого порядка или шаблона типа «дыра в середине», встроенного прямо в язык.

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

Gulshan
источник

Ответы:

38

Инверсия зависимости в ООП означает, что вы кодируете интерфейс, который затем предоставляется реализацией в объекте.

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

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

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

Пример в C #

Традиционный подход:

public IEnumerable<Customer> FilterCustomers(IFilter<Customer> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter.Matches(customer))
        {
            yield return customer;
        }
    }
}

//now you've got to implement all these filters
class CustomerNameFilter : IFilter<Customer> /*...*/
class CustomerBirthdayFilter : IFilter<Customer> /*...*/

//the invocation looks like this
var filteredDataByName = FilterCustomers(new CustomerNameFilter("SomeName"), customers);
var filteredDataBybirthDay = FilterCustomers(new CustomerBirthdayFilter(SomeDate), customers);

С функциями высшего порядка:

public IEnumerable<Customer> FilterCustomers(Func<Customer, bool> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter(customer))
        {
            yield return customer;
        }
    }
}

Теперь реализация и вызов становятся менее громоздкими. Нам больше не нужно предоставлять реализацию IFilter. Нам больше не нужно реализовывать классы для фильтров.

var filteredDataByName = FilterCustomers(x => x.Name.Equals("CustomerName"), customers);
var filteredDataByBirthday = FilterCustomers(x => x.Birthday == SomeDateTime, customers);

Конечно, это уже может быть сделано LinQ в C #. Я просто использовал этот пример, чтобы проиллюстрировать, что проще и более гибко использовать функции более высокого порядка вместо объектов, которые реализуют интерфейс.

сокол
источник
3
Хороший пример. Однако, как и Гулшан, я пытаюсь узнать больше о функциональном программировании, и мне было интересно, не жертвует ли этот вид «функционального DI» некоторой строгостью и значимостью по сравнению с «объектно-ориентированным DI». Сигнатура высшего порядка только говорит о том, что переданная функция должна принимать Customer в качестве параметра и возвращать bool, тогда как версия OO обеспечивает тот факт, что переданный объект является фильтром (реализует IFilter <Customer>). Это также делает понятие фильтра явным, что может быть полезно, если это базовая концепция домена (см. DDD). Что вы думаете ?
guillaume31
2
@ ian31: Это действительно интересная тема! Все, что передается в FilterCustomer, будет неявно работать как своего рода фильтр. Когда концепция фильтра является неотъемлемой частью домена и вам нужны сложные правила фильтрации, которые используются в системе несколько раз, лучше их инкапсулировать. Если нет или только в очень низкой степени, то я бы стремился к технической простоте и прагматизму.
Сокол
5
@ ian31: я полностью не согласен. Осуществление IFilter<Customer>не принуждение вообще. Функция высшего порядка гораздо более гибкая, что является большим преимуществом, а возможность писать их в строке - еще одно огромное преимущество. Лямбды также намного легче в состоянии захватывать локальные переменные.
DeadMG
3
@ ian31: функция также может быть проверена во время компиляции. Вы также можете написать функцию, назвать ее, а затем передать ее в качестве аргумента, если она полностью выполняет очевидный контракт (принимает клиента, возвращает bool). Вам не обязательно передавать лямбда-выражение. Таким образом, вы можете покрыть это отсутствие выразительности в определенной степени. Однако договор и его намерения не так четко выражены. Иногда это серьезный недостаток. В общем, это вопрос выразительности, языка и инкапсуляции. Я думаю, что вы должны судить каждый случай по отдельности.
Сокол
2
если вы сильно о разъяснении смыслового значения введенной функции, вы можете в подписях функции # имени С помощью делегатов: public delegate bool CustomerFilter(Customer customer). в чистых функциональных языках, таких как haskell, псевдонимы типов тривиальны:type customerFilter = Customer -> Bool
sara
8

Если вы хотите изменить поведение функции

doThis(Foo)

Вы могли бы передать другую функцию

doThisWith(Foo, anotherFunction)

который реализует поведение, которое вы хотите быть другим.

«doThisWith» - функция высшего порядка, потому что она принимает другую функцию в качестве аргумента.

Например, вы могли бы иметь

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
LennyProgrammers
источник
5

Краткий ответ:

Классическое внедрение / инверсия зависимостей использует классовые интерфейсы в качестве заполнителя для зависимых функций. Этот интерфейс реализован классом.

Вместо Interface / ClassImplementation многие зависимости могут быть легко реализованы с помощью функции делегата.

Вы можете найти пример для обоих в c # на ioc-factory-pros-and-contras-for-interface-versus-Delegates .

k3b
источник
0

Сравните это:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = new LinkedList<String>();
for (String name : names) {
    if (name.startsWith("S")) {
        namesBeginningWithS.add(name);
    }
}

с:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = names.stream().filter(n <- n.startsWith("S")).collect();

Вторая версия - это способ сокращения стандартного кода Java 8 (зацикливание и т. Д.) Путем предоставления функций более высокого порядка, например, filterпозволяющих передать минимальный минимум (т. Е. Зависимость, которую нужно ввести - лямбда-выражение).

Шридхар Сарнобат
источник
0

Копилка примера LennyProgrammers ...

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

Если вместо:

doThisWith(Foo, anotherFunction)

у нас (чтобы быть общепринятым в том, как обычно выполняется PFA) есть низкоуровневая рабочая функция как (порядок замены аргументов):

doThisWith( anotherFunction, Foo )

Затем мы можем частично применить doThisWith следующим образом:

doThis = doThisWith( anotherFunction )  // note that "Foo" is still missing, argument list is partial

Что позволит нам позже использовать новую функцию следующим образом:

doThis(Foo)

Или даже:

doThat = doThisWith( yetAnotherDependencyFunction )
...
doThat( Bar )

Смотрите также: https://ramdajs.com/docs/#partial

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

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

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

Roboprog
источник