¿La programación funcional es una alternativa viable a los patrones de inyección de dependencia?

21

Recientemente he estado leyendo un libro titulado Programación funcional en C # y se me ocurre que la naturaleza inmutable y sin estado de la programación funcional logra resultados similares a los patrones de inyección de dependencia y es posiblemente un mejor enfoque, especialmente en lo que respecta a las pruebas unitarias.

Le agradecería que alguien que tenga experiencia con ambos enfoques pueda compartir sus pensamientos y experiencias para responder a la pregunta principal: ¿ es la programación funcional una alternativa viable a los patrones de inyección de dependencia?

Matt Cashatt
fuente
10
Esto no tiene mucho sentido para mí, la inmutabilidad no elimina las dependencias.
Telastyn
Estoy de acuerdo en que no elimina las dependencias. Probablemente entiendo que es incorrecto, pero hice esa inferencia porque si no puedo cambiar el objeto original, es necesario que lo pase (lo inyecte) a cualquier función que lo utilice.
Matt Cashatt
55
También hay Cómo engañar a los programadores de OO para que hagan una programación funcional amorosa , que es realmente un análisis detallado de DI desde una perspectiva de OO y FP.
Robert Harvey
1
Esta pregunta, los artículos a los que enlaza y la respuesta aceptada también pueden ser útiles: stackoverflow.com/questions/11276319/… Ignora la palabra aterradora Mónada. Como Runar señala en su respuesta, no es un concepto complejo en este caso (solo una función).
itsbruce

Respuestas:

27

La gestión de dependencias es un gran problema en OOP por las siguientes dos razones:

  • El fuerte acoplamiento de datos y código.
  • Uso ubicuo de los efectos secundarios.

La mayoría de los programadores de OO consideran que el acoplamiento estrecho de datos y código es totalmente beneficioso, pero tiene un costo. Administrar el flujo de datos a través de las capas es una parte inevitable de la programación en cualquier paradigma. El acoplamiento de sus datos y código agrega el problema adicional de que si desea utilizar una función en un determinado punto, debe encontrar una manera de llevar su objeto a ese punto.

El uso de efectos secundarios crea dificultades similares. Si usa un efecto secundario para alguna funcionalidad, pero desea poder cambiar su implementación, prácticamente no tiene otra opción que inyectar esa dependencia.

Considere como ejemplo un programa de spammer que raspa las páginas web para direcciones de correo electrónico y luego las envía por correo electrónico. Si tiene una mentalidad DI, en este momento está pensando en los servicios que encapsulará detrás de las interfaces y en qué servicios se inyectarán dónde. Dejaré ese diseño como ejercicio para el lector. Si tiene una mentalidad FP, en este momento está pensando en las entradas y salidas para la capa más baja de funciones, como:

  • Ingrese una dirección de página web, envíe el texto de esa página.
  • Ingrese el texto de una página, envíe una lista de enlaces desde esa página.
  • Ingrese el texto de una página, envíe una lista de direcciones de correo electrónico en esa página.
  • Ingrese una lista de direcciones de correo electrónico, envíe una lista de direcciones de correo electrónico con los duplicados eliminados.
  • Ingrese una dirección de correo electrónico, envíe un correo electrónico no deseado para esa dirección.
  • Ingrese un correo electrónico no deseado, envíe los comandos SMTP para enviar ese correo electrónico.

Cuando piensa en términos de entradas y salidas, no hay dependencias de funciones, solo dependencias de datos. Eso es lo que los hace tan fáciles de realizar pruebas unitarias. Su próxima capa organiza la salida de una función para alimentarla a la entrada de la siguiente, y puede intercambiar fácilmente las diversas implementaciones según sea necesario.

En un sentido muy real, la programación funcional naturalmente lo impulsa a invertir siempre sus dependencias de funciones, y por lo tanto, generalmente no tiene que tomar ninguna medida especial para hacerlo después del hecho. Cuando lo haga, las herramientas como funciones de orden superior, cierres y aplicaciones parciales hacen que sea más fácil lograrlo con menos repetitivo.

Tenga en cuenta que no son las dependencias mismas las que son problemáticas. Son las dependencias las que señalan el camino equivocado. La siguiente capa puede tener una función como:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Está perfectamente bien que esta capa tenga dependencias codificadas de esta manera, porque su único propósito es unir las funciones de la capa inferior. Cambiar una implementación es tan simple como crear una composición diferente:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Esta fácil recomposición es posible por la falta de efectos secundarios. Las funciones de la capa inferior son completamente independientes entre sí. La siguiente capa puede elegir cuál processTextse usa realmente en función de alguna configuración de usuario:

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

Nuevamente, no es un problema porque todas las dependencias apuntan en una dirección. No necesitamos invertir algunas dependencias para que todas apunten de la misma manera, porque las funciones puras ya nos obligaron a hacerlo.

Tenga en cuenta que puede hacer esto mucho más acoplado pasando configa la capa más baja en lugar de verificarlo en la parte superior. FP no le impide hacer esto, pero tiende a hacerlo mucho más molesto si lo intenta.

Karl Bielefeldt
fuente
3
"El uso de efectos secundarios crea dificultades similares. Si usa un efecto secundario para alguna funcionalidad, pero desea poder cambiar su implementación, prácticamente no tiene otra opción que inyectar esa dependencia". No creo que los efectos secundarios tengan nada que ver con esto. Si desea intercambiar implementaciones en Haskell, aún tiene que hacer una inyección de dependencia . Descarta las clases de tipos y estás pasando una interfaz como primer argumento para cada función.
Doval
2
El quid de la cuestión es que casi todos los lenguajes te obligan a codificar referencias a otros módulos de código, por lo que la única forma de intercambiar implementaciones es usar el despacho dinámico en todas partes, y luego estás atascado resolviendo tus dependencias en tiempo de ejecución. Un sistema de módulos le permitiría expresar el gráfico de dependencia en el momento de la verificación de tipo.
Doval
@ Doval: gracias por sus comentarios interesantes y estimulantes. Puede que te haya entendido mal, pero estoy en lo correcto al inferir de tus comentarios que si tuviera que usar un estilo funcional de programación sobre un estilo DI (en el sentido tradicional de C #), entonces evitaría posibles frustraciones de depuración asociadas con el tiempo de ejecución resolución de dependencias?
Matt Cashatt
@MatthewPatrickCashatt No es una cuestión de estilo o paradigma, sino de características del lenguaje. Si el lenguaje no admite módulos como cosas de primera clase, tendrá que hacer algún envío dinámico e inyección de dependencias para intercambiar implementaciones, porque no hay forma de expresar las dependencias de forma estática. Para decirlo de manera diferente, si su programa C # usa cadenas, tiene una dependencia codificada System.String. Un sistema de módulos le permitiría reemplazarlo System.Stringcon una variable para que la elección de la implementación de la cadena no esté codificada, sino que aún se resuelva en el momento de la compilación.
Doval
8

¿Es la programación funcional una alternativa viable a los patrones de inyección de dependencia?

Esto me parece una pregunta extraña. Los enfoques de programación funcional son en gran medida tangenciales a la inyección de dependencia.

Claro, tener un estado inmutable puede empujarlo a no "hacer trampa" al tener efectos secundarios o usar el estado de clase como un contrato implícito entre funciones. Hace que el paso de datos sea más explícito, lo que supongo es la forma más básica de inyección de dependencia. Y el concepto de programación funcional de pasar funciones hace que sea mucho más fácil.

Pero no elimina las dependencias. Sus operaciones aún necesitan todos los datos / operaciones que necesitaban cuando su estado era mutable. Y todavía necesita obtener esas dependencias allí de alguna manera. Por lo tanto, no diría que los enfoques de programación funcional reemplazan a la DI, por lo que no hay ningún tipo de alternativa.

En todo caso, acaban de mostrarle lo mal que el código OO puede crear dependencias implícitas de lo que los programadores rara vez piensan.

Telastyn
fuente
Gracias de nuevo por contribuir a la conversación, Telastyn. Como ha señalado, mi pregunta no está muy bien construida (mis palabras), pero gracias a los comentarios aquí estoy empezando a comprender un poco mejor qué es lo que está provocando en mi cerebro sobre todo esto: todos estamos de acuerdo (Creo) que las pruebas unitarias pueden ser una pesadilla sin DI. Desafortunadamente, el uso de DI, especialmente con los contenedores IoC, puede crear una nueva forma de pesadilla de depuración gracias al hecho de que resuelve dependencias en tiempo de ejecución. Similar a DI, FP hace que las pruebas unitarias sean más fáciles, pero sin los problemas de dependencia del tiempo de ejecución.
Matt Cashatt
(continúa desde arriba). . Este es mi entendimiento actual de todos modos. Avíseme si me estoy perdiendo la marca. ¡No me importa admitir que soy un simple mortal entre los gigantes aquí!
Matt Cashatt
@MatthewPatrickCashatt - DI no necesariamente implica problemas de dependencia de tiempo de ejecución, que como notas, son horribles.
Telastyn
7

La respuesta rápida a su pregunta es: n .

Pero como otros han afirmado, la pregunta se casa con dos conceptos, algo no relacionados.

Hagamos esto paso a paso.

DI da como resultado un estilo no funcional

En el núcleo de la programación de funciones hay funciones puras: funciones que asignan entrada a salida, por lo que siempre obtienes la misma salida para una entrada determinada.

DI generalmente significa que su unidad ya no es pura ya que la salida puede variar dependiendo de la inyección. Por ejemplo, en la siguiente función:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(una función) puede variar produciendo diferentes resultados para la misma entrada dada. Esto también lo hace bookSeatsimpuro.

Hay excepciones para esto: puede inyectar uno de los dos algoritmos de clasificación que implementan el mismo mapeo de entrada-salida, aunque utilizando diferentes algoritmos. Pero estas son excepciones.

Un sistema no puede ser puro

El hecho de que un sistema no puede ser puro se ignora igualmente como se afirma en las fuentes de programación funcional.

Un sistema debe tener efectos secundarios con los ejemplos obvios que son:

  • UI
  • Base de datos
  • API (en arquitectura cliente-servidor)

Entonces, parte de su sistema debe involucrar efectos secundarios y esa parte también puede involucrar un estilo imperativo o estilo OO.

El paradigma del núcleo del shell

Tomando prestados los términos de la excelente charla de Gary Bernhardt sobre límites , una buena arquitectura de sistema (o módulo) incluirá estas dos capas:

  • Núcleo
    • Funciones puras
    • Derivación
    • Sin dependencias
  • Cáscara
    • Impuro (efectos secundarios)
    • Sin ramificación
    • Dependencias
    • Puede ser imperativo, implicar estilo OO, etc.

La conclusión clave es 'dividir' el sistema en su parte pura (el núcleo) y la parte impura (el caparazón).

Aunque ofrece una solución (y conclusión) ligeramente defectuosa, este artículo de Mark Seemann propone el mismo concepto. La implementación de Haskell es particularmente perspicaz, ya que muestra que todo se puede hacer con FP.

DI y FP

Emplear DI es perfectamente razonable incluso si la mayor parte de su aplicación es pura. La clave es confinar el DI dentro del caparazón impuro.

Un ejemplo serán los apéndices de API: desea la API real en producción, pero use apéndices en las pruebas. Adherirse al modelo de núcleo de shell ayudará mucho aquí.

Conclusión

Entonces FP y DI no son exactamente alternativas. Es probable que tenga ambos en su sistema, y ​​el consejo es garantizar la separación entre la parte pura e impura del sistema, donde residen FP y DI respectivamente.

Izhaki
fuente
Cuando se refiere al paradigma del núcleo del shell, ¿cómo podría uno no lograr ramificaciones en el shell? Puedo pensar en muchos ejemplos en los que una aplicación necesitaría hacer una cosa impura u otra en función de un valor. ¿Es aplicable esta regla de no ramificación en lenguajes como Java?
jrahhali
@jrahhali Consulte la charla de Gary Bernhardt para obtener más detalles (vinculada en la respuesta).
Izhaki
otra serie Seemann relativa blog.ploeh.dk/2017/01/27/…
jk.
1

Desde el punto de vista de OOP, las funciones pueden considerarse interfaces de un solo método.

La interfaz es un contrato más fuerte que una función.

Si está utilizando un enfoque funcional y realiza una gran cantidad de DI, en comparación con el uso de un enfoque OOP obtendrá más candidatos para cada dependencia.

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

vs

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.
Guarida
fuente
3
Cualquier clase puede ajustarse para implementar la interfaz, de modo que el "contrato más fuerte" no sea mucho más fuerte. Más importante aún, dar a cada función un tipo diferente hace que sea casi imposible hacer la composición de la función.
Doval
La programación funcional no significa "Programación con funciones de orden superior", se refiere a un concepto mucho más amplio, las funciones de orden superior son solo una técnica útil en el paradigma.
Jimmy Hoffa