¿Cómo se relaciona la inversión de dependencia con las funciones de orden superior?

41

Hoy acabo de ver este artículo que describe la relevancia del principio SOLID en el desarrollo de F #

F # y principios de diseño - SÓLIDO

Y al abordar el último: "Principio de inversión de dependencia", el autor dijo:

Desde un punto de vista funcional, estos contenedores y conceptos de inyección se pueden resolver con una función simple de orden superior, o con un patrón de tipo agujero en el medio que está integrado en el lenguaje.

Pero no lo explicó más. Entonces, mi pregunta es, ¿cómo se relaciona la inversión de dependencia con las funciones de orden superior?

Gulshan
fuente

Respuestas:

38

La inversión de dependencia en OOP significa que usted codifica contra una interfaz que luego es proporcionada por una implementación en un objeto.

Los lenguajes que admiten funciones de lenguaje superior a menudo pueden resolver problemas simples de inversión de dependencia al pasar el comportamiento como una función en lugar de un objeto que implementa una interfaz en el sentido OO.

En dichos lenguajes, la firma de la función puede convertirse en la interfaz y se pasa una función en lugar de un objeto tradicional para proporcionar el comportamiento deseado. El agujero en el patrón del medio es un buen ejemplo de esto.

Le permite lograr el mismo resultado con menos código y más expresividad, ya que no necesita implementar una clase completa que se ajuste a una interfaz (OOP) para proporcionar el comportamiento deseado para la persona que llama. En cambio, puede pasar una definición de función simple. En resumen: el código suele ser más fácil de mantener, más expresivo y más flexible cuando se utilizan funciones de orden superior.

Un ejemplo en C #

Enfoque tradicional:

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 funciones de orden superior:

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

Ahora la implementación y la invocación se vuelven menos engorrosas. Ya no necesitamos suministrar una implementación de IFilter. Ya no necesitamos implementar clases para los filtros.

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

Por supuesto, esto ya lo puede hacer LinQ en C #. Acabo de usar este ejemplo para ilustrar que es más fácil y más flexible usar funciones de orden superior en lugar de objetos que implementan una interfaz.

Halcón
fuente
3
Buen ejemplo Sin embargo, al igual que Gulshan, estoy tratando de obtener más información sobre la programación funcional y me preguntaba si este tipo de "DI funcional" no sacrifica cierto rigor y significado en comparación con la "DI orientada a objetos". La firma de orden superior solo indica que la función pasada debe tomar un Cliente como parámetro y devolver un valor bool, mientras que la versión OO impone el hecho de que el objeto pasado es un filtro (implementa IFilter <Cliente>). También hace explícita la noción de filtro, lo que podría ser algo bueno si es un concepto central del Dominio (ver DDD). Qué piensas ?
guillaume31
2
@ ian31: ¡Este es realmente un tema interesante! Cualquier cosa que se pase a FilterCustomer se comportará implícitamente como una especie de filtro. Cuando el concepto de filtro es una parte esencial del dominio y necesita reglas de filtro complejas que se utilizan varias veces en todo el sistema, es mejor encapsularlas. Si no, o solo en un grado muy bajo, apuntaría a la simplicidad técnica y al pragmatismo.
Falcon
55
@ ian31: Estoy completamente en desacuerdo. Implementar IFilter<Customer>no es hacer cumplir en absoluto. La función de orden superior es mucho más flexible, lo cual es un gran beneficio, y poder escribirlas en línea es otro gran beneficio. Las lambdas también son mucho más fáciles de capturar variables locales.
DeadMG
3
@ ian31: la función también se puede verificar en tiempo de compilación. También puede escribir una función, nombrarla y luego pasarla como argumento siempre que cumpla el contrato obvio (toma al cliente, devuelve bool). No es necesario que pase necesariamente una expresión lambda. Entonces puedes cubrir esa falta de expresividad hasta cierto punto. Sin embargo, el contrato y su intención no se expresa tan claramente. Esa es una gran desventaja a veces. En general, se trata de expresividad, lenguaje y encapsulación. Creo que tienes que juzgar cada caso por sí mismo.
Falcon el
2
si usted se siente fuertemente sobre aclarar el significado semántico de una función inyectada, puede en C # firmas de función nombre utilizando los delegados: public delegate bool CustomerFilter(Customer customer). en lenguajes funcionales puros como haskell, los tipos de alias son triviales:type customerFilter = Customer -> Bool
sara
8

Si quieres cambiar el comportamiento de una función

doThis(Foo)

podrías pasar otra función

doThisWith(Foo, anotherFunction)

que implementa el comportamiento que desea que sea diferente.

"doThisWith" es una función de orden superior porque toma otra función como argumento.

Por ejemplo, podrías tener

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
LennyProgrammers
fuente
5

Respuesta corta:

La inyección de dependencia clásica / inversión de control utiliza interfaces de clase como marcador de posición para la funcionalidad dependiente. Esta interfaz es implementada por una clase.

En lugar de Interface / ClassImplementation, muchas dependencias pueden implementarse más fácilmente con una función delegada.

Puede encontrar un ejemplo para ambos en c # en ioc-factory-pros-and-contras-for-interface-versus-delegates .

k3b
fuente
0

Compara esto:

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 segunda versión es la forma en que Java 8 reduce el código repetitivo (bucle, etc.) al proporcionar funciones de orden superior como las filterque le permiten pasar el mínimo básico (es decir, la dependencia a inyectar, la expresión lambda).

Sridhar Sarnobat
fuente
0

Piggy-backing del ejemplo de LennyProgrammers ...

Una de las cosas que los otros ejemplos perdieron es que puede usar funciones de orden superior junto con la aplicación de función parcial (PFA) para unir (o "inyectar") dependencias en una función (a través de su lista de argumentos) para crear una nueva función.

Si en lugar de:

doThisWith(Foo, anotherFunction)

nosotros (para ser convencionales en la forma en que generalmente se hace PFA) tenemos la función de trabajador de bajo nivel como (intercambio de orden arg):

doThisWith( anotherFunction, Foo )

Entonces podemos aplicar parcialmente doThisWith así:

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

Lo que nos permite más tarde utilizar la nueva función de esta manera:

doThis(Foo)

O incluso:

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

Ver también: https://ramdajs.com/docs/#partial

... y, sí, los sumadores / multiplicadores son ejemplos poco imaginativos. Un mejor ejemplo sería una función que toma mensajes y los registra o los envía por correo electrónico dependiendo de lo que la función "consumidor" haya pasado como dependencia.

Extendiendo esta idea, las listas de argumentos aún más largas se pueden reducir progresivamente a funciones cada vez más especializadas con listas de argumentos cada vez más cortas, y, por supuesto, cualquiera de estas funciones se puede pasar a otras funciones como dependencias para aplicar parcialmente.

OOP es bueno si necesita un conjunto de cosas con múltiples operaciones estrechamente relacionadas, pero se convierte en un trabajo para hacer un montón de clases cada una con un único método público de "hazlo", a la "El Reino de los sustantivos".

Roboprog
fuente