Quel est le lien entre l'inversion de dépendance et les fonctions d'ordre supérieur?

41

Aujourd'hui, je viens de lire cet article décrivant la pertinence du principe SOLID dans le développement de F # -

F # et principes de conception - SOLID

Et en abordant le dernier - "Principe d'inversion de dépendance", l'auteur a déclaré:

D'un point de vue fonctionnel, ces conteneurs et concepts d'injection peuvent être résolus avec une simple fonction d'ordre supérieur, ou un modèle de type trou-dans-le-milieu, intégré au langage.

Mais il ne l'expliqua pas davantage. Ma question est donc la suivante: comment l’inversion de dépendance est-elle liée aux fonctions d’ordre supérieur?

Gulshan
la source

Réponses:

38

L'inversion de dépendance dans la POO signifie que vous codez contre une interface qui est ensuite fournie par une implémentation dans un objet.

Les langages prenant en charge des fonctions de langage supérieures peuvent souvent résoudre des problèmes simples d'inversion de dépendance en transmettant le comportement comme une fonction au lieu d'un objet qui implémente une interface dans le sens OO.

Dans de tels langages, la signature de la fonction peut devenir l'interface et une fonction est transmise à la place d'un objet traditionnel pour fournir le comportement souhaité. Le trou au milieu est un bon exemple de cela.

Cela vous permet d'obtenir le même résultat avec moins de code et plus d'expression, car vous n'avez pas besoin d'implémenter une classe entière conforme à une interface (OOP) pour fournir le comportement souhaité à l'appelant. Au lieu de cela, vous pouvez simplement passer une définition de fonction simple. En bref: le code est souvent plus facile à maintenir, plus expressif et plus flexible lorsque l’on utilise des fonctions d’ordre supérieur.

Un exemple en C #

Approche traditionnelle:

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);

Avec des fonctions d'ordre supérieur:

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

Maintenant, l'implémentation et l'invocation deviennent moins lourdes. Nous n'avons plus besoin de fournir une implémentation IFilter. Nous n'avons plus besoin d'implémenter des classes pour les filtres.

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

Bien sûr, cela peut déjà être fait par LinQ en C #. Je viens d'utiliser cet exemple pour illustrer le fait qu'il est plus facile et plus flexible d'utiliser des fonctions d'ordre supérieur au lieu d'objets implémentant une interface.

Faucon
la source
3
Bel exemple. Cependant, comme Gulshan, j'essaie d'en savoir plus sur la programmation fonctionnelle et je me demandais si ce type de "DI fonctionnelle" ne sacrifiait pas une certaine rigueur et une signification par rapport à la "DI orientée objet". La signature d'ordre supérieur indique uniquement que la fonction transmise doit prendre un client en tant que paramètre et renvoyer une valeur bool, tandis que la version OO impose le fait que l'objet transmis est un filtre (implémente IFilter <Client>). Cela rend également explicite la notion de filtre, ce qui pourrait être une bonne chose s’il s’agit d’un concept fondamental du domaine (voir DDD). Qu'est-ce que tu penses ?
guillaume31
2
@ ian31: C'est vraiment un sujet intéressant! Tout ce qui est passé à FilterCustomer se comportera implicitement comme une sorte de filtre. Lorsque le concept de filtre est une partie essentielle du domaine et que vous avez besoin de règles de filtrage complexes utilisées plusieurs fois sur le système, il est préférable de les encapsuler. Si ce n’est pas le cas ou très peu, je privilégierais la simplicité technique et le pragmatisme.
Falcon
5
@ ian31: je suis complètement en désaccord. La mise en œuvre IFilter<Customer>n'est pas une application du tout. La fonction d'ordre supérieur est beaucoup plus flexible, ce qui représente un avantage considérable, et le fait de pouvoir les écrire en ligne est un autre avantage considérable. Les Lambda sont également beaucoup plus faciles à capturer des variables locales.
DeadMG
3
@ ian31: La fonction peut également être vérifiée lors de la compilation. Vous pouvez également écrire une fonction, la nommer, puis la passer sous forme d'argument tant qu'elle remplit le contrat évident (prend client, renvoie bool). Vous n'avez pas nécessairement besoin de passer une expression lambda. Vous pouvez donc couvrir ce manque d’expression dans une certaine mesure. Cependant, le contrat et son intention ne sont pas exprimés aussi clairement. C'est un inconvénient majeur parfois. Dans l'ensemble, c'est une question d'expressivité, de langage et d'encapsulation. Je pense que vous devez juger chaque cas seul.
Falcon
2
si vous vous sentez fortement à clarifier la signification sémantique d'une fonction injectée, vous pouvez dans les signatures de fonction C # nom en utilisant les délégués: public delegate bool CustomerFilter(Customer customer). dans les langages fonctionnels purs comme haskell, le repliement des types est trivial:type customerFilter = Customer -> Bool
sara
8

Si vous voulez changer le comportement d'une fonction

doThis(Foo)

vous pourriez passer une autre fonction

doThisWith(Foo, anotherFunction)

qui met en œuvre le comportement que vous voulez être différent.

"doThisWith" est une fonction d'ordre supérieur, car elle prend une autre fonction en tant qu'argument.

Par exemple, vous pourriez avoir

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
LennyProgrammers
la source
5

Réponse courte:

L'injection de dépendance classique / Inversion of Control utilise une interface de classe comme espace réservé pour les fonctionnalités dépendantes. Cette interface est implémentée par une classe.

Au lieu de Interface / ClassImplementation, de nombreuses dépendances peuvent être plus facilement implémentées avec une fonction de délégué.

Vous trouvez un exemple à la fois dans c # à ioc-factory-pros-and-contras-for-interface-versus-delegues .

k3b
la source
0

Comparez ceci:

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

avec:

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

La deuxième version est un moyen utilisé par Java 8 pour réduire le code passe-partout (boucles, etc.) en fournissant des fonctions d'ordre supérieur telles filterque celles qui permettent de passer le minimum strict (c'est-à-dire la dépendance à injecter - l'expression lambda).

Sridhar Sarnobat
la source
0

Piggy-backing exemple de LennyProgrammers ...

Parmi les autres exemples manqués, vous pouvez utiliser des fonctions d'ordre supérieur avec une application de fonction partielle (PFA) pour lier (ou "injecter") des dépendances dans une fonction (via sa liste d'arguments) afin de créer une nouvelle fonction.

Si au lieu de:

doThisWith(Foo, anotherFunction)

nous (pour être conventionnel dans la façon dont la PFA est habituellement faite) avons la fonction de travailleur de bas niveau comme (permutation d'argument):

doThisWith( anotherFunction, Foo )

On peut alors appliquer partiellement doThisWith comme suit:

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

Ce qui nous permet d'utiliser plus tard la nouvelle fonction comme suit:

doThis(Foo)

Ou même:

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

Voir aussi: https://ramdajs.com/docs/#partial

... et, oui, les additionneurs / multiplicateurs sont des exemples sans imagination. Un meilleur exemple serait une fonction qui prend des messages et les enregistre ou les envoie par courrier électronique en fonction de la fonction "consommateur" transmise en tant que dépendance.

En élargissant cette idée, des listes d'arguments encore plus longues peuvent être progressivement réduites à des fonctions de plus en plus spécialisées avec des listes d'arguments de plus en plus courtes. Bien entendu, chacune de ces fonctions peut être transférée à d'autres fonctions sous forme de dépendances à appliquer partiellement.

La programmation orientée objet est pratique si vous avez besoin d'un ensemble d'éléments comportant plusieurs opérations étroitement liées, mais il s'avère également indispensable de créer un ensemble de classes comportant chacune une méthode "do it" publique, comme le "Royaume des noms".

Roboprog
la source