La idea sonaba bien. De hecho, sigue sonando bien. Tentadora. Pero como todo lo que suena demasiado bien, es fácil que se acabe corrompiendo y nos lleve a situaciones, digamos, complicadas. Me refiero al principio de persistence ignorance, o ignorancia de persistencia.
Qué es Persistence Ignorance
Hace ya unos cuantos años, el mensaje de la programación orientada a objetos empezó a calar y hacerse mainstream. Quizá no fuera la programación orientada a objetos «original» de Alan Kay y se tratase más bien del estilo C++/Java/C#, pero el hecho es que en un mundo que tradicionalmente había sido dominado por modelos anémicos y un enfoque muy data centric, surgió un estilo de programación que trataba de modelar más comportamiento que datos, y encapsular más la lógica de aplicación de forma explícita en el código en lugar de en la estructura de la base de datos.
Fue también la época en que se comenzaron a popularizar las técnicas de tests unitarios, arrastradas por conceptos como eXtreme Programming y el manifiesto ágil. Considerando que testear una base de datos tiene su complicación, lo de centrar la lógica en el modelo de objetos se hacía muy interesante.
En ese contexto, el peso de la aplicación pasó hacia un modelo de objetos donde resultaba más cómodo (natural, dirían algunos) modelar el comportamiento y que se prestaba muy bien a ser testeado con tests unitarios.
Si le unimos principios como la inversión e inyección de dependencias, ya tenemos el pack completo para poder empezar a pensar en arquitecturas donde el centro de todo es nuestro modelo de objetos y el resto de cosas (UI, persistencia, etc.) cuelga de él. Arquitectura hexagonal/clean/onion o cómo lo quieras llamar.
Viéndolo así y aplicando correctamente el principio de inversión de dependencias, que va más allá de tener un interfaz para IRepository
, llegamos a un escenario en que nuestro modelo de dominio no debería depender de nada externo. Debería ser algo puro, etéreo y grácil, no acoplado a conceptos mundanos como la forma en que lo voy a pintar en pantalla o en qué base de datos lo voy a persistir. La base de datos, que había sido el centro de todo en el enfoque data centric, pasaba a estar relegada a una esquinita del diagrama.
Y está bien. Tiene todo el sentido del mundo minimizar las dependencias en código entre unas cosas y otras, y te puede ayudar a conseguir un diseño más desacoplado y más solido.
Que tu modelo de dominio no sepa gran cosa (idealmente nada) de la forma en que se va a persistir es bueno. Eso es lo que se conoce como persistence ignorance: hacer que tu modelo de objetos no tenga que depender de la forma en que se persiste.
Eso te permitirá centrarte en resolver la parte importante del problema, que es modelarlo con objetos, y ya veremos cómo tratamos luego con la persistencia (o con el resto de factores externos al dominio).
Los problemas de ignorar la persistencia
En todo la argumentación anterior estamos partiendo de la premisa de que lo más importante de nuestra aplicación es un modelo de objetos, cosa que, en general, no es cierta.
El modelo de dominio sólo existe para soportar los requisitos que tenemos. Por eso no tiene sentido modelar La Realidad™, sino el problema concreto que tenemos que resolver. Dependiendo del tipo de problema, aunque el dominio sea (o parezca) el mismo, podremos tener distintos tipos de modelos más o menos adecuados. Una persona es una persona, pero la información que me interesa de ella no es la misma para una web de citas, que para una clínica dental.
Hacer que nuestro modelo de dominio sea independiente (ignore) el tipo de persistencia que vamos a emplear puede tener sentido técnicamente, ya sea para facilitar el desarrollo, para poder escribir mejores tests o por el motivo que más te guste. Pero quédate ahí: El modelo de dominio puede ignorar la persistencia, pero tú no.
Podemos ver el mecanismo de persistencia como algo que «sólo» nos sirve para guardar nuestros objetos, pero lo cierto es que distintos mecanismos de persistencia ofrecen distintas características que deben ser tenidas en cuenta, como vimos al considerar las implicaciones que tiene usar una base de datos o no en nuestras aplicaciones.
Tal vez nuestros usuarios estén muy felices porque ahora tenemos miles de tests unitarios que garantizan que nuestra aplicación funciona correctamente, pero creedme, si al final la información que tan cuidadosamente han introducido acaba perdiéndose porque nuestro sistema de persistencia no es durable, o corrompido porque no tenemos un buen soporte transaccional, eso va a ser un problema. Y grave.
Por supuesto, también podemos hablar del rendimiento. Es curioso que gente que se preocupa de usar un StringBuilder
en lugar de concatenar strings
para mejorar el rendimiento, luego no vea problema en cargar 500MB de datos en memoria para totalizar algo. No es cuestión de centrarnos en detalles, sino en rendimiento general basado en las expectativas de uso de la aplicación (aunque también es cierto que no eso es algo que no siempre es fácil de medir).
Podemos pensar que no pasa nada. Que está todo controlado. Que para eso estoy aplicando inversión de dependencias y tengo unos interfaces maravillosos que me permiten cambiar la forma en que trabajo si es necesario. Desgraciadamente, no todo se puede abstraer detrás de un interfaz, y si quiero un vaso de leche, aunque solo necesite invocar IMilkProvider.GetMilk()
, no es lo mismo si la implementación lo saca de mi nevera, que si tiene que ir a un centro comercial, o que si tiene que ordeñar la vaca. El tipo de errores que se pueden producir, el tiempo que tardará en ejecutarse… son factores que pueden invalidar por completo el algoritmo que usamos y que, supuestamente, es independiente de la implementación del interfaz. Pensar que por tener un montón de interfaces estamos a salvo de futuros cambios es demasiado optimista.
Algo similar ocurre con la gestión de concurrencia y las transacciones. ¿Qué carga de trabajo esperamos en la aplicación? ¿Cuántos usuarios concurrentes? ¿Qué datos se van a modificar simultáneamente?
Podemos diseñar nuestro modelo de objetos ignorando eso, pero antes o después chocaremos con la realidad. Si al elegir mis aggregate roots (o el equivalente en tu modelo) no he pensado un poco en esto, corro el riesgo de que cuando un usuario quiera hacer una operación, necesite cargar demasiada información en memoria para poder mantener los invariantes de mi modelo, lo que implica no sólo un problema de rendimiento, sino un potencial problema de (inter)bloqueos entre usuarios.
Conclusión
Que una parte de nuestra aplicación no tenga una dependencia explícita a nivel de código sobre otra no quiere decir que no siga teniendo una dependencia implícita. Toda la aplicación está acoplada de algún modo, aunque sea a nivel conceptual ya que está diseñada para resolver un problema concreto.
Es bueno intentar aislar técnicamente distintas partes de la aplicación para poder gestionarlas de forma más independiente y poder centrarnos en resolver problemas más pequeños en cada momento, pero nunca hay que perder de vista la visión global.
Puede que el código dentro del modelo de dominio no sepa nada de la persistencia, pero nosotros sí que deberíamos haber pensado en ella a la hora de decidir cómo diseñar ese modelo para que cumpla con los requisitos que tenemos.
Imagen de The Clean Architecture
Totalmente de acuerdo. Cuando te topas con aplicaciones que soportan decenas de sucursales, replicas, miles de transacciones diarias, entiendes la importancia que tiene un buen diseño de base de datos y todo lo que lo rodea..
Es curioso que gente que se preocupa de usar un StringBuilder en lugar de concatenar strings para mejorar el rendimiento, luego no vea problema en cargar 500MB de datos en memoria para totalizar algo
Para pararse y aplaudir de pie … lo he visto muchas veces. La última a finales del año pasado, donde se tomaron ciertas decisiones en la forma de utilizar una herramienta que exploto cuando ya se había hecho todo el desarrollo y se estaban haciendo las pruebas con los datos.
Mucha paja mental y poca experiencia, una combinación fatal.