In che modo l'inversione di dipendenza è correlata a funzioni di ordine superiore?


41

Oggi ho appena visto questo articolo che descriveva la rilevanza del principio SOLID nello sviluppo di F #

F # e principi di progettazione - SOLID

E affrontando l'ultimo - "Principio di inversione di dipendenza", l'autore ha detto:

Da un punto di vista funzionale, questi contenitori e concetti di iniezione possono essere risolti con una semplice funzione di ordine superiore, o un modello di tipo hole-in-the-middle che sono integrati nel linguaggio.

Ma non lo ha spiegato ulteriormente. Quindi, la mia domanda è: in che modo l'inversione di dipendenza è correlata a funzioni di ordine superiore?

Risposte:


38

Inversione di dipendenza in OOP significa che si codifica su un'interfaccia che viene quindi fornita da un'implementazione in un oggetto.

Le lingue che supportano funzioni linguistiche più elevate possono spesso risolvere semplici problemi di inversione di dipendenza trasmettendo il comportamento come funzione anziché come oggetto che implementa un'interfaccia in senso OO.

In tali lingue, la firma della funzione può diventare l'interfaccia e una funzione viene passata al posto di un oggetto tradizionale per fornire il comportamento desiderato. Il buco nel modello centrale è un buon esempio per questo.

Ti consente di ottenere lo stesso risultato con meno codice e più espressività, in quanto non è necessario implementare un'intera classe conforme a un'interfaccia (OOP) per fornire il comportamento desiderato per il chiamante. Invece, puoi semplicemente passare una semplice definizione di funzione. In breve: il codice è spesso più facile da mantenere, più espressivo e più flessibile quando si usano funzioni di ordine superiore.

Un esempio in C #

Approccio tradizionale:

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

Con funzioni di ordine superiore:

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

Ora l'implementazione e l'invocazione diventano meno ingombranti. Non è più necessario fornire un'implementazione di IFilter. Non abbiamo più bisogno di implementare le classi per i filtri.

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

Naturalmente, questo può essere già fatto da LinQ in C #. Ho appena usato questo esempio per illustrare che è più semplice e flessibile utilizzare funzioni di ordine superiore anziché oggetti che implementano un'interfaccia.


3
Bell'esempio Tuttavia, come Gulshan, sto cercando di saperne di più sulla programmazione funzionale e mi chiedevo se questo tipo di "DI funzionale" non sacrifica un certo rigore e significato rispetto a "DI orientato agli oggetti". La firma di ordine superiore indica solo che la funzione passata deve prendere un cliente come parametro e restituire un valore booleano mentre la versione OO impone il fatto che l'oggetto passato sia un filtro (implementa IFilter <Customer>). Rende anche esplicita la nozione di filtro, che potrebbe essere una buona cosa se si tratta di un concetto chiave del dominio (vedi DDD). Cosa pensi ?
guillaume31,

2
@ ian31: questo è davvero un argomento interessante! Tutto ciò che viene passato a FilterCustomer si comporterà implicitamente come una sorta di filtro. Quando il concetto di filtro è una parte essenziale del dominio e sono necessarie regole di filtro complesse che vengono utilizzate più volte nel sistema, è sicuramente meglio incapsularle. Se non fosse o solo in misura molto bassa, punterei alla semplicità tecnica e al pragmatismo.
Falcon,

5
@ ian31: non sono completamente d'accordo. L'implementazione IFilter<Customer>non è affatto un'applicazione. La funzione di ordine superiore è di gran lunga più flessibile, il che è un grande vantaggio e poter scrivere in linea è un altro enorme vantaggio. Le lambda sono anche molto più facili da catturare per le variabili locali.
DeadMG

3
@ ian31: la funzione può anche essere verificata in fase di compilazione. Puoi anche scrivere una funzione, nominarla e quindi passarla come argomento purché riempia il contratto ovvio (prende il cliente, restituisce valore). Non devi necessariamente passare un'espressione lambda. Quindi puoi coprire quella mancanza di espressività fino a un certo punto. Tuttavia, il contratto e le sue intenzioni non sono così chiaramente espressi. Questo è uno svantaggio maggiore a volte. Tutto sommato è una questione di espressività, linguaggio e incapsulamento. Penso che devi giudicare ogni caso da solo.
Falcon,

2
se si sente fortemente circa chiarire il significato semantico di una funzione iniettato, è possibile in C # firme funzione nome utilizzando i delegati: public delegate bool CustomerFilter(Customer customer). in linguaggi puramente funzionali come l'hashell, i tipi di aliasing sono banali:type customerFilter = Customer -> Bool
Sara

8

Se si desidera modificare il comportamento di una funzione

doThis(Foo)

potresti passare un'altra funzione

doThisWith(Foo, anotherFunction)

che implementa il comportamento che vuoi essere diverso.

"doThisWith" è una funzione di ordine superiore perché accetta un'altra funzione come argomento.

Ad esempio potresti averlo

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)

5

Risposta breve:

Iniezione / inversione di controllo di dipendenza classica utilizza interfacce di classe come segnaposto per funzionalità dipendente. Questa interfaccia è implementata da una classe.

Invece di Interface / ClassImplementation, molte dipendenze possono essere implementate più facilmente con una funzione delegata.

Puoi trovare un esempio di entrambi in c # su ioc-factory-pros-and-contras-for-interface-versus-delegates .


0

Confronta questo:

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

con:

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

La seconda versione è il modo di Java 8 di ridurre il codice boilerplate (looping ecc.) Fornendo funzioni di ordine superiore come quelle filterche consentono di passare il minimo indispensabile (ovvero la dipendenza da iniettare - l'espressione lambda).


0

Esecuzione del backup dell'esempio di LennyProgrammers ...

Una delle cose che mancano agli altri esempi è che puoi usare le funzioni di ordine superiore insieme all'applicazione di funzioni parziali (PFA) per associare (o "iniettare") dipendenze in una funzione (tramite il suo elenco di argomenti) per creare una nuova funzione.

Se invece di:

doThisWith(Foo, anotherFunction)

noi (per essere convenzionali nel modo in cui viene fatto PFA) abbiamo la funzione di lavoratore di basso livello come (scambiando l'ordine arg):

doThisWith( anotherFunction, Foo )

Possiamo quindi applicare parzialmente doThisWith in questo modo:

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

Il che ci consente in seguito di utilizzare la nuova funzione in questo modo:

doThis(Foo)

O anche:

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

Vedi anche: https://ramdajs.com/docs/#partial

... e sì, gli additivi / moltiplicatori sono esempi privi di fantasia. Un esempio migliore sarebbe una funzione che accetta i messaggi e li registra o li invia tramite e-mail a seconda di ciò che la funzione "consumatore" ha passato come dipendenza.

Estendendo questa idea, gli elenchi di argomenti ancora più lunghi possono essere progressivamente ridotti a funzioni sempre più specializzate con elenchi di argomenti sempre più brevi e, naturalmente, una di queste funzioni può essere passata ad altre funzioni come dipendenze da applicare parzialmente.

OOP è utile se hai bisogno di un insieme di cose con più operazioni strettamente correlate, ma si trasforma in un lavoro di creazione per creare un gruppo di classi ognuna con un singolo metodo "do it" pubblico, alla "The Kingdom of Nouns".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.