¿Qué son y en qué nos beneficia? ¿es la evolución en el diseño de aplicaciones?

En los últimos años, y debido a la evolución del hardware, la baja latencia de red, la evolución de las tecnologías y la cada vez mayor exigencia de los usuarios en la respuesta rápida y sin errores de los sistemas que usan (ya sea una red social o cualquier app móvil), las nuevas arquitectura están necesitando cumplir una serie de requisitos (composición, tolerancia a fallos, responsiveness, etc.) que permitan cumplir las exigencias de los usuarios y poder aprovechar toda la potencia de la que disponemos en cuanto al hardware (no nos olvidemos del Big Data (https://es.wikipedia.org/wiki/Big_data ) y del cloud computing (https://es.wikipedia.org/wiki/Computacion_en_la_nube), que aprovechan aún más nuestros recursos).

Además, se está cambiando la forma de desarrollar software orientándolo mucho más a la colaboración con el cliente y no tanto a requisitos técnicos del sistema. Estamos hablando de las metodologías ágiles, cada vez más implantadas hoy día.

Con esta evolución, tenemos que empezar a pensar que los modelos tradicionales de desarrollo de software y arquitecturas clásicas (programación declarativa, orientación a objetos, objetos mutables, monolitos, etc.) tengan que ir por un camino que encajen con los nuevos modelos y arquitecturas (sistemas reactivos, microservicios, toolkits, etc.) y que cumplan con los requisitos citados anteriormente.

¿Pero realmente los modelos de dominios funcionales nos ayudan a resolver estos problemas?

Imaginemos que tenemos ante nosotros un problema dentro de una empresa, que se traduzca a un proyecto/MVP o a una prueba de concepto. Dejemos de pensar por un momento en requisitos de hardware, programación y arquitecturas. Centrémonos en el propio problema y en la mejor forma de resolverlo, desde un punto de vista de negocio.

En esto se centran modelos como DDD, que nos ayudan a enfocarnos en el problema que queremos solucionar, y nos define una serie de elementos comunes y patrones de diseño (entidades, value objects, factories, repositories, etc.) que nos facilitarán poder definir nuestras entidades dentro del dominio de nuestro negocio y separar las capas de nuestra aplicación, para que sean lo más aisladas posibles. En el caso de un banco, por ejemplo, empezaremos a hablar de entidades como: Cuenta, Transferencia, Préstamo, Hipoteca, Cliente, etc.

¿No es más sencillo para todos (stakeholders, cliente final, arquitectos, desarrolladores) hablar de los mismos conceptos desde el principio? ¿No vemos que estamos rompiendo la barrera más grande de cualquier organización hoy día que es justamente unir negocio e IT para que hablen un mismo idioma? A este concepto en modelos de dominios se le llama ‘ubiquitous language’ o ‘idioma unificado’.

Si nos metemos en el marco de las metodologías ágiles, estos modelos encajan mucho mejor dentro del ciclo de vida ágil, ya que facilitan varios de los principios del ‘agile thinking’, donde negocio y desarrollo se unen proporcionando una visión única del ámbito del proyecto y de las necesidades de negocio que cubre.

¿Y la parte funcional?

La parte funcional que completa el título de este artículo y en general todo el paradigma funcional nos ofrece una serie de beneficios que harán que podamos diseñar nuestras aplicaciones cubriendo los requisitos anteriores, y aportando una mayor flexibilidad, seguridad y entendimiento del código, y facilitando el mantenimiento del mismo.

El paradigma funcional se basa en el concepto de la inmutabilidad y la composición. Con el primero conseguimos evitar los grandes problemas que conlleva guardar el estado de nuestras entidades durante la vida de nuestra aplicación: Concurrencia y condiciones de carrera en uso de recursos compartidos (aunque esto no se resolverá del todo de forma eficiente hasta aplicar un modelo reactivo, véase modelo de actores) y nos garantiza poder componer funciones para poder abordar operaciones más complejas y llevar un grado de paralelismo mucho más óptimo en sistemas distribuidos.

Aplicaremos el concepto de definir el ‘qué’ y no el ‘cómo’ a la hora de desarrollar nuestras aplicaciones, eliminando la barrera de las semánticas complejas en nuestro código y entender la mejor forma de resolver nuestros problemas. Simplemente cuando desarrolles tu código piensa en qué tiene que hacer la función que estás desarrollando. Es una forma mucho más sencilla y práctica de programar, uniendo mucho mejor conceptos como: TDD, BDD (basados en el Given-When-Then, centrándonos en las capabilities de comportamiento y negocio), Single Responsibility Principle (aislar la responsabilidad de cada uno de los componentes o módulos de nuestra aplicación y evitar los temidos side-effects), extensibilidad de nuestra aplicación, etc.

Pensad en una operación en programación funcional como una función matemática f(x) = y que puede componerse con otras funciones (o primitivas que forman un álgebra). De esta forma, por composición de funciones, se va a ir construyendo un modelo cada vez más complejo que no sea más que un conjunto de operaciones anidadas. Esto traducido a funciones es f ( g ( h ( x ) ) ) = v. Con esta base nos va a facilitar tener las siguientes ventajas en nuestro modelo:

  • Código con un bajo nivel de acoplamiento
  • Búsqueda de la inmutabilidad evitando guardar el estado de los componentes, pilar fundamental de la programación funcional. Simplemente, tu código no guarda el estado de ninguna entidad (ya veremos más adelante que realmente lo que devolvemos en nuestras funciones son copias de objetos)
  • Componentes fáciles de escalar y transportar en arquitecturas distribuidas
  • Facilidad a la hora de desarrollar test unitarios, ya que nuestra funcionalidad está aislada en una función, y de aplicar TDD / BDD.

Esto parece abstracto… ¿podemos verlo en código?

Existen lenguajes de programación que cubren la parte funcional y de dominio de una forma muy sencilla. Lenguajes como Scala han sido desarrollados teniendo en mente el paradigma funcional y nos ayudan a realizar la composición de funciones de forma sencilla y legible, definir nuestros elementos de dominio con una serie de capabilities por defecto sin que necesitemos escribir más código ‘boilerplate’ del necesario, además de tener todas las ventajas de un lenguaje basado en la JVM. Más info sobre Scala: (https://www.scala-lang.org/)

Con esto, podemos poner un ejemplo basándonos en un modelo bancario, en el que se definen las operaciones de débito y crédito sobre las entidades de negocio Balance y Cuenta. Veremos después del código algunas anotaciones que nos permitirán aclararlo.

Comentemos el código:

  • Línea 2: Scala define una serie de clases que nos van a permitir poder abstraer y manejar las excepciones y los errores no controlados desde un punto de vista funcional. Las excepciones son algo ‘impuro’ para la programación funcional, ya que no se pueden componer de forma sencilla. Con esta abstracción se soluciona, de forma que se pueden componer Try[_] con otras funciones.
  • Líneas 5 y 6: Aquí definimos lo que se denomina un ADT (Tipo de dato algebraico), es decir, un tipo de dato de composición o dicho de otra forma, un tipo de dato formado por la combinación de otros tipos de dato. Para lograr que un ADT sea inmutable, Scala nos proporciona los case class. Esto en nuestro modelo de dominio encaja perfectamente con los llamados ‘value objects’, u objetos inmutables. Imaginaos por ejemplo tener un objeto que representa una dirección postal: Si cambio cualquiera de los datos de una dirección… ya estoy hablando de otra dirección distinta, ¿cierto?. Estos objetos inmutables existen en todos los dominios y se modelan de esta forma.

Hasta la línea 7 estamos definiendo el estado de nuestro modelo. Hablemos ahora del comportamiento del mismo

  • Líneas 8-17: Nuestro servicio de dominio de cuentas es el encargado de definir las funciones base que aisladas podrían tener sentido en algún contexto, pero que seguramente serán compuestas en nuestro modelo en operaciones más complejas (operaciones de negocio) o agregados (map, flatMap, etc.). Viendo el código:
    • Hacemos uso del objeto Try para encapsular los errores que ocurran en nuestra implementación.
    • Las funciones cumplen con el principio de referencial transparency de programación funcional, es decir, que para una misma serie de parámetros estamos devolviendo siempre el mismo resultado. Esto trasladado a nuestro modelo de dominio, siempre estamos devolviendo una nueva cuenta a partir de una cuenta y una cantidad.
  • Línea 18: Aquí definimos una instancia concreta del servicio.
  • Línea 22: Por último, invocamos a una función pura: Devuelve un Try[Account]

¡Pero el mundo real es mucho más complejo! ¿Cómo manejamos los side-effects?

¿Qué ocurre cuando tienes un problema de base de datos y se produce una SQLException? ¿O intentas acceder a un fichero y devuelve un IOException? El mundo real siempre es más complejo e impredecible que las cosas que ocurren dentro de nuestra aplicación y se producen los side-effects o efectos secundarios.

Diseñar una aplicación confiando en que el mundo exterior va a funcionar correctamente es una mera ilusión. Siempre diseña para el fallo, y gestiona los errores que ocurran siempre en código distinto al código de negocio de tu aplicación.

En el siguiente caso tenemos que verificar si el cliente es correcto o no. Si lo es, abrimos la cuenta. Si no, lanzamos una excepción controlada.

Definimos el trait ‘AccountService’ con la operación de verificación de cliente:

Definimos el objeto ‘AccountService’ que extiende del trait:

El uso del servicio es como sigue:

Los side-effects merman la capacidad de composición de las funciones en un paradigma funcional, cosa que intentamos cumplir durante todo el diseño de la aplicación. Si no tenemos composición, ya tenemos que ir lidiando con código ‘pegamento’ o ‘boilerplate’ -> difícil de extender y mantener.

Conclusiones

Hay mucho por comentar de los modelos de dominio funcionales, aunque espero haberos acercado un poco a ellos; Nos ayudan a definir una estructura de aplicación orientada a negocio, en el que stakeholders y desarrolladores hablan el mismo idioma, facilitando el uso de las metodologías ágiles. Además nos ofrece una serie de patrones y elementos comunes que nos ayuda a definir las capas de nuestra aplicación, y generar una arquitectura desacoplada y fácilmente mantenible / extensible.

Este tipo de modelos no son una sustitución de los modelos actuales; Todos los modelos tienen sus ventajas e inconvenientes, y según en qué ámbitos, modelos tradicionales podrían tener más sentido. Como siempre es escoger la mejor de las opciones según convenga 🙂

Referencias

https://en.wikipedia.org/wiki/Functional_programming

https://en.wikipedia.org/wiki/Domain-driven_design

Functional and reactive domain modeling – Debasish Ghosh (Manning)

Podéis seguir más artículos, noticias y datos interesantes en https://www.ingtechit.es y en mi cuenta de Twitter.