Durante los últimos meses he estado trabajando en un proyecto utilizando Scala como lenguaje de programación principal. La mezcla de programación orientada a objectos y programación funcional que permite Scala resulta interesante, pero cada vez estoy más convencido de que la verdadera potencia del mismo se consigue cuando se aplica un estilo puramente funcional. Al fin y al cabo, si únicamente queremos un lenguaje con algunas características funcionales que hagan nuestro código un poco más claro y limpio, hay otras opciones que nos dan eso (véanse la última versión de Java o Kotlin), sin las complejidades con las que hay que lidiar en Scala.

Supongo que escribiré en el futuro un artículo más elaborado sobre el tema, pero dejadme que hoy me centre en algo que llevo viendo bastante cuando examino código en Scala, casi siempre cuando éste ha sido escrito por desarrolladoras que tienen amplia experiencia en algún lenguaje orientado a objetos (como Java) y empiezan a escribir código en Scala: cómo hacer inyección de dependencias.

Empezaré proponiendo un escenario muy básico para analizar el problema y algunas de las soluciones que proporciona el lenguaje. Supongamos que queremos crear un servicio de informes que nos devuelva los balances del último mes de todas las cuentas bancarias abiertas por un cliente. Nuestros clientes están relacionados con todas cuentas que tienen abiertas (serían nuestro Aggregate Root si hablásemos de Domain Driven Design) y se identifican por una secuencia alfanumérica de longitud diez. Dichas cuentas, mantienen todos los movimientos ordenados por su fecha de ejecución. Un balance contiene, en definitiva, el número que identifica a una cuenta, la fecha hasta donde el balance ha contabilizado y una cantidad de dinero.

De esta forma definimos nuestro servicios con la siguiente operación:

type Id = String

case class Balance(acNo: String, balanceAt: Date, amount: Money)

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): List[Balance]
   ...
}

Los clientes son accessibles dados sus identificadores vía un repositorio.

trait CustomerRepository {
   def retrieve(customerId: Id): Option[Customer]
   ...
}

Nuestra implementación del servicio de informes necesitará una instancia válida del repositorio de clientes para poder tener acceso al cliente solicitado y hacer el cáculo de los balances de sus cuentas. Hasta el momento, lo que he visto es que un porcentaje bastante algo de desarrolladores que vienen de otros lenguajes como Java harán algo así para implementar dicho servicio:

class ReportingInterpreter(repo: CustomerRepository) implements Reporting {
   override def monthlyBalance(customerId: Id, year: Int, month: Int): List[Balance] = //do something here
   ...
}

A muchos desarrolladores acostumbrados al paradigma orientado a objetos, esta implementación le parecerá bastante natural (y acertada). Sin embargo, desde el punto de vista de la programación funcional, tiene el principal inconveniente de que en ninguna parte se hace explícito que la operación monthlyBalance necesita de un repositorio de clientes. Alguien que utilice esta función nunca sabrá que, sin un repositorio de clientes, dicha operación no podrá ser ejecutada. El repositorio de clientes es parte del contexto de ejecución de la clase ReportingInterpreter, a quien se lo estamos inyectando a través de su constructor.

Esta situación puede mejorarse un poco aplicando un diseño algo más funcional a nuestra función monthlyBalance. ¿Por qué no pasarle explícitamente el repositorio de clientes como un párametro de entrada a dicha función? De esta forma tendríamos:

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int, repo: CustomerRepository): List[Balance]
   ...
}

Ahora ya hemos hecha explícita la dependencia de la función monthlyBalance con el repositorio de clientes y podemos decir que nuestro diseño es más funcional. Sin embargo, todavía hay algunas desventajas con esta solución. Una de ellas es que añadimos un parámetro extra a nuestra función, lo que hace que ésta sea más difícil de ser llamada. Si nuestro módulo Reporting crece y ofrece muchas otras funciones que también dependen de CustomerRepository (y es probable que dichas funciones también dependan de este repositorio), siempre tendremos que pasar éste como último parámetro en todas nuestras llamadas a funciones del módulo Reporting. Esto resulta especialmente engorroso si queremos componer funciones de dicho módulo para ofrecer nuevas funciones.

¿Cómo podemos mejorar esta situación? Teniendo en cuenta que intentamos utilizar un estilo de programación cada vez más funcional, podemos hacer uso de la idea de que en un lenguaje funcional las funciones son objetos de primera clase que pueden ser utilizados como cualquier otro tipo de objeto. Por ejemplo, pueden ser pasados como parámetros de entrada a otras funciones (creando de esta forma funciones de orden superior). Otra cosa que a veces olvidamos quienes venimos de lenguajes orientados a objetos y no tenemos demasiada experiencia en programación funcional, es que las funciones pueden devolver otras funciones. ¿Qué pasaría si hacemos que nuestra función monthlyBalance devolviera otra función que toma como único parámetro de entrada un repositorio de clientes y devuelve los balances mensuales de las cuentas de un cliente? Vemos cómo podríamos definir esto en Scala:

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): CustomerRepository => List[Balance]
   ...
}

Ahora podemos utilizar nuestra función para obtener el balance mensual sin tener que proporcionar el repositorio de clientes:

val reporter: Reporting = ...
val balance = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6)

Por supuesto, lo que obtenemos no es el balance todavía pues, para obtener el balance, necesitamos proporcionar el repositorio de clientes, sino una función capaz de devolver los balances cuando se le pasa un repositorio adecuado. De esta forma, hemos conseguido obtener una definición de nuestro balance sin tener que resolver la dependencia todavía y, de momento, no hemos ejecutado nada aún. Estamos aplicando un principio bastante interesante y repetido en programación funcional: define pronto, ejecuta tarde.

Supongamos que del balance queremos obtener la cantidad total, podemos aplicar composición de funciones para obtenerlo:

val amount = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6) _ andThen (map(_.amount))

Y, de nuevo, todavía no hemos proporcionado el repositorio de clientes ni hemos ejectuado nada. Cuando tengamos disponible una instancia válida de CustomerRepository, podemos hacer:

val repo: CustomerRepository = ...

print("this is the monthly balance amount: " + amount(repo)) //amount is a function

Por último, podemos valernos de una herramienta común en programación funcional: las mónadas. Más concretamente, la mónada Reader resuelve nuestro problema de una forma puramente funcional y muy elegante.

En primer lugar, debemos cambiar la definición de nuestra función para que devuelva una instancia de Reader en lugar de una función (podemos decir que, en cierta forma, Reader encapsula una función que, a partir de un contexto que se le pasa como parámetro de entrada, nos devuelve un resultado). En este ejemplo, utilizaré la implementación de Reader proporcionada en por la librería scalaz.

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): Reader[CustomerRepository, List[Balance]]
   ...
}

Ahora, nuestro método monthlyBalance nos devolverá una instancia de Reader. ¿Qué podemos hacer con ella? Muchas cosas, como, por ejemplo, extraer las cantidades de los balances de una forma más limpia que como hicimos anteriormente gracias a la función map:

val amount = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6).map(b => b.map(_.amount))

Como se trata de una mónada, podemos componer instancias de Reader dentro de una estructura for comprehension, por ejemplo, para obtener la evolución de los balances en los primeros tres meses del año:

val thisYear = ...
val firstQuarterReport = for {
   balances1 <- monthlyBalance(customerId, thisYear, 1)
   balances2 <- monthlyBalance(customerId, thisYear, 2)

   balances3 <- monthlyBalance(customerId, thisYear, 3)
} yield balances1 ++ balances2 ++ balances3

De nuevo, todavía no hemos ejecutado nada, solamente hemos proporcionado una definición de los balances del primer trimestre. ¿Cómo resolvemos la dependencia para tener el resultado final? Para ello, utilizamos el método run proporcionado por la mónada Reader:

val balancesQ1 = firstQuarterReport.run(customerRepository)

Es justo en ese momento cuando se ejecuta nuestro código y todo queda resuelto.

Hemos visto tres alternativas funcionales a hacer inyección de dependencias mediante el constructor de clase:

  • Proporcionar la dependencia a través de un parámetro extra añadido a nuestra función
  • Dividir la lista de parámetros para poder hacer currying, obteniendo una función a que devuelve otra función
  • Utilizar la mónada Reader como tipo de retorno

Todas ellas, alternativas mucho más funcionales que, además, nos permiten diferir la resolución de nuestra dependencia hasta justo el momento en el que resulta imprescindible.


En este artículo presentamos un ejemplo práctico de cómo llevar a cabo cambios importantes tanto en nuestro código como en la base de datos utilizada para mantener la persistencia de la informació sin dejar de proporcionar servicio. De esta forma, podemos hacer efectivo el paradigma de continuous delivery sin que los usuarios de nuestro sistema sufran paradas en el servicio debido a las actualizaciones que llevamos a cabo.

Presentación del ejemplo utilizado

Para mostrar de una forma efectiva las técnicas aqui expuestas, vamos a utilizar un servicio que gestiona información relativa a clientes expuesto a través de un API REST. Dicho servicio guarda la siguiente información sobre los clientes:
  • Nombre
  • Apellidos
  • Calle
  • Número
  • Código postal
  • Ciudad
Estos datos son almacenados en una única tabla dentro de una base de datos relacional con la siguiente estructura:


Este servicio ha sido completamente implementado en Java, utilizando JPA para la gestión del almacenamiento persistente y Jersey para la creación de la interfaz REST. Existe una clase que modela a nuestros clientes:

Un repositorio de clientes:

Y un servicio de clientes:

Con objeto de mantener el código lo más sencillo posible, he optado por no usar Spring (las principales dependencias siguen siendo inyectadas aunque esto lo hago de forma manual sin ayuda de ningún framework):

Puedes ver el detalle de cómo se ha implementado la persistencia con JPA echándole un vistazo al código de las classes CustomerRepositoryImpl y PersistenceManager. También puedes descargar el código inicial de este proyecto aquí (utiliza la rama master).

El diseño de nuestra solución está lejos de ser perfecto por muchos motivos pero digamos que, hasta el momento, ha sido adecuado para nuestro propósito y lo estamos utilizando en producción. El código puede echarse a correr utilizando un servidor Jetty embebido arrancado con Maven:

mvn jetty:run -Djetty.port=[puerto donde queremos escuchar]

Si queremos que este servicio sea altamente disponible necesitaremos varias réplicas ejecutándose al mismo tiempo para que así un balanceador de carga pueda enviar las peticiones entrantes a cualquiera de las instancias en ejecución que considere oportuna (cómo configurar dicho balanceador de carga está fuera del alcance de esta entrada, aunque no es difícil).

Evolución del servicio de clientes

Una de las desventajas de esta solución es que solamente podemos conocer la dirección actual de un cliente, pero no las distintas direcciones que ha tenido a lo largo del tiempo.  Así que decidimos que, de ahora en adelante, nuestro sistema va a almacenar un registro de todas las direcciones que ha usado un cliente junto con la fecha en que cada dirección ha sido proporcionada.

Esto va a requerir modificar tanto nuestra aplicación como el esquema de la base de datos relacional que usa. Existen varias formas de hacer esto. Por ejemplo, podemos pensar en un modelo de datos nuevo que dé soporte a los nuevos requisitos:

Con una nueva tabla para direcciones (address) que tenga todos los campos actualmente contenidos en la tabla de cliente más una referencia (clave ajena) al cliente al que pertenece, modificamos nuestra aplicación y creamos un script de migración que cree la nueva tabla para direcciones con su correspondiente clave ajena a la tabla de clientes y migre los datos existentes, eliminando los campos de direcciones de la tabla de clientes ya que no son necesarios de ahora en adelante. Entonces paramos nuestros dos servidores en ejecución, actualizamos el código que ejecutan, actualizamos nuestra base de datos (cambio de esquema y migración de datos de clientes existentes) y volvemos a arrancar los servidores.

El principal inconveniente de esto es que nuestro servicio dejará de estar operativo el tiempo que dure la actualización de código y la migración de nuestra base de datos. Esto puede ser un gran inconveniente ya que, mientras la actualización del código tardará poco, la migración de los datos tendrá una duración variable, a menudo no conocida de antemano, y que dependerá en gran medida del volumen de datos que maneja nuestro servicio. Es decir, cuanto más datos tengamos en nuestra base de datos más tardaremos en migrarlos a la nueva estructura que le da soporte a los nuevos requisitos.

Es importante resaltar que, debido a la naturaleza del cambio que se quiere hacer, no es posible actualizar uno de los dos servidores y dejar el otro funcionando para mantener el servicio operativo durante la migración de datos. Sencillamente, el código nuevo no puede funcionar con el antiguo esquema de datos porque necesita la nueva tabla de direcciones, del mismo modo que el código antiguo no es compatible con el nuevo esquema porque necesita los campos de direcciones que hay en la tabla de clientes.

Por lo tanto, tenemos que adoptar un enfoque diferente si queremos hacer este cambio si queremos que siempre haya, al menos, un servidor en ejecución para poder garantizar que nuestro servicio está disponible. Para ello, debemos construir una secuencia de cambios compatibles que nos permitan mantener una versión antigua y otra actualizada de nuestro código en ejecución al mismo tiempo.

Cambiando nuestro código de forma compatible

Vamos a empezar añadiendo a la tabla de clientes una nueva columna para guardar la fecha de cambio de la dirección. Vamos a hacer que dicha columna pueda contener valores nulos y ni siquiera vamos a usarla desde nuestro código todavía. Simplemente, vamos a preparar nuestra base de datos para cuando queramos empezar a utilizarla. El cambio sería muy sencillo (y pequeño):

Una de las utilidades que hemos añadido a nuestro código para hacer los cambios más rápidos es la capacidad de actualizar el esquema de datos a través del plugin de liquibase para Maven.

De forma, nuestro proceso de actualización de servidores consistirá en:

  1. Parar un servidor
  2. Actualizar el código (git pull)
  3. Ejecutar el comando para migrar nuestra base de datos (mvn liquibase:update)
  4. Arrancar el servidor (mvn jetty:run -Djetty.port=7070)
  5. Parar el segundo servidor
  6. Actualizar el código (git pull)
  7. Arrancar el servidor (mvn jetty:run -Djetty.port=9090)

Cuando solamente hemos actualizado un servidor, podemos hacer varias llamadas a cada uno de los servidores (el que ya ha sido actualizado y el que todavía no lo ha sido) para comprobar que ambos funcionan correctamente, es decir, que el cambio es compatible con la versión antigua del código. Esto no es ninguna sorpresa ya que, hasta ahora, no hemos cambiado nada en el código, solamente hemos añadido una nueva columna a una tabla en nuestra base de datos.

curl http://localhost:7070/api/customers/1

{"currentAddress":{"city":null,"postalCode":null,"streetName":null,"streetNumber":null},"firstName":"Frank","lastName":"Freundlich"}

curl http://localhost:9090/api/customers/1

{"currentAddress":{"city":null,"postalCode":null,"streetName":null,"streetNumber":null},"firstName":"Frank","lastName":"Freundlich"}

A continuación, modificamos nuestra clase de clientes para, ahora sí empezar a utilizar el nuevo campo que habíamos creado en la tabla de clientes:

El servicio que actualiza los datos de clientes también es adaptado:

Volvemos a actualizar nuestros dos servidores de forma secuencial, siempre dejando, al menos, uno en ejecución. Podemos probar, por ejemplo, que los dos servidores siguien funcionando cuando uno ya ha sido actualizado y el otro todavía no (obviamente, sólo uno de ellos almacenará la fecha y hora de cambio de la dirección de un cliente, pero los dos funcionarán).

Lo siguiente que haremos es publicar los datos acerca de cuándo ha cambiado una dirección en nuestro servicio REST. Este cambio no debería representar ningún problema, ya que consiste en añadir un nuevo campo en nuestro objecto DTO que transporta la información relativa a direcciones y hacer un pequeño ajuste en la clase que hace las transformaciones entre nuestras entidades y nuestro DTO:

Ahora sí podemos empezar a hacer los grandes cambios que requiere el mantenimiento de un historial de direcciones. Será bastante complicado hacer esto manteniendo toda la información relativa a las direcciones en la misma clase que modela a cliente y almacenando dicha información en la misma tabla en nuestra base de datos.

Vamos a optar por crear una nueva clase que modele direcciones (Address) y persistirla en nuestra base de datos en una tabla independiente, distinta de la tabla de clientes. Sin embargo, como ya vimos antes, no podemos hacer todos los cambios al mismo tiempo si queremos mantener la compatibilidad entre distintas versiones que se estén ejecutando en nuestros servidores al mismo tiempo (recordemos que, durante el proceso de actualización de nuestros servidores, habrá un periodo de tiempo en el que un servidor ya ha sido actualizado y el otro todavia no, por lo que ambas versiones deben ser compatibles). Por ello, vamos a proceder del siguiente modo: primero haremos los cambios necesarios en nuestras clases (objetos en memoria) y, a continuación, los cambios necesarios en nuestras tablas (datos persistentes).

Así que vamos a crear una nueva clase para direcciones y vamos a hacer que la clase de clientes empiece a a trabajar con dichas direcciones manteniendo un historial de las mismas:

Como podemos ver, el cambio todavía no es completo, pues el historial de direcciones no se está guardando realmente de forma persistente, pero nuestro modelo en memoria ya está preparado para gestionar un historial de direcciones correctamente y, aún más importante, es totalmente compatible con el modelo que tenemos en nuestra base de datos.


Ahora vamos a crear la estructura necesaria en nuestra base de datos para persistir el historial de direcciones con una nueva tabla:

También necesitamos adaptar nuestro código:

Como queremos que los cambios sean compatibles entre distintas versiones, debemos aún mantener la antigua estructura conviviendo con la nueva al mismo tiempo. Esto lo conseguimos gracias a la implementación que hemos hecho del método updateAddress:

Esto significa que cuando una dirección cambia, dicho cambio se guarda en dos sitios al mismo tiempo:
  • En la nueva tabla de direcciones que guarda nuestro historial
  • En los antiguos campos para direcciones que había en la tabla de clientes
¿Por qué hacemos esto? Porque es la forma en que garantizamos que dos versiones de nuestra aplicación podrán acceder a la base de datos para consultar la dirección de un cliente y que siempre podrán encontrar dicha información.

Imaginemos qué pasaría si no lo hiciéramos así. Supongamos que no tenemos ninguna dirección almacenada para un cliente y recibimos una llamada al servicio que actualiza los datos de un cliente proporcionando una dirección. Si esta llamada fuera atendida por la versión nueva del sistema y únicamente almacenara la dirección en la nueva tabla, ¿qué pasaría si la siguiente consulta sobre dicho cliente fuera atendida por la versión antigua del sistema? Pues que buscaría la dirección del cliente en los campos dentro de la tabla de clientes y no encontraría nada. Por ello, la versión nueva del código debe preocuparse de guardar las direcciones en los dos sitios a la vez.

Otro aspecto importante que debemos destacar es que el sistema empezará a guardar las direcciones en la nueva tabla, pero seguirá utilizando como fuente de datos primaria para la consulta de direcciones los campos que hay en la tabla de clientes. Es decir, la nueva versión que estamos preparando guarda los datos en el antiguo formato y en el nuevo, pero los devuelve utilizando el antiguo método. Esta forma de hacer cambios corresponde a una técnica más general que podemos aplicar cuando queremos sustituir un sistema (o subsistema) minimizando riesgos. El procedimiento, de forma general, sería:
  1. Implantar el nuevo sistema que va a sustituir a uno ya existente
  2. Empezar a utilizar el nuevo sistema al mismo tiempo que seguimos usando el antiguo. Usar el antiguo sistema como fuente primaria (el nuevo sistema se mantiene en la sombra)
  3. Hacer el nuevo sistema nuestra fuente primaria. Seguimos utilizando el antiguo sistema, siendo éste el que ahora está en la sombra.
  4. Cuando estamos seguros de que el nuevo sistema funciona correctamente y ya no necesitamos el antiguo, prescindimos de él y eliminamos todas las referencias al mismo.

Podemos hacer la prueba de que todo funciona correctamente, manteniendo la compatilibidad entre versiones parando uno de los servidores, actualizando el esquema de nuestra base de datos y arrancando el servidor de nuevo. Las peticiones que se hagan a servidor con el nuevo código que actualicen la dirección de un cliente guardarán los nuevos datos tanto en las columnas de la tabla de clientes como en la nueva tabla de direcciones que hemos creado. Las que se dirijan al servidor con el antiguo código guardarán las nuevas direcciones únicamente en los campos de la tabla de clientes. Ambos servidores devolverán la información más actual que exista de la dirección de un cliente utilizando los campos de la tabla de clientes.

Después de esta comprobación, podemos actualizar el segundo servidor para completar el despliegue de la nueva versión. Como resultado, ahora nuestro sistema siempre guarda las direcciones nuevas en la tabla de direcciones, manteniendo un registros histórico de las direcciones utilizadas por un cliente (también almacena, por motivos de compatibilidad, dicha información en los antiguos campos existentes en la tabla de clientes).

Todo esto está muy bien, pero todavía existen direcciones que únicamente están almacenadas en los campos de la tabla de clientes (se crearon antes de que la nueva versión se pusiera en producción y no han sido actualizados desde entonces). Éste es el motivo por el que debemos seguir tomando los campos en la tabla de clientes como la fuente primaria cuando atendemos una consulta de datos sobre un cliente. Ahora debemos migrar la información acerca de direcciones a la nueva tabla. Dependiendo de con cuánta frecuencia se hayan actualizado las direcciones de clientes desde que la nueva versión que usa la nueva tabla de direcciones, el número de direcciones almacenadas únicamente en el antiguo formato será mayor o menor y, por lo tanto, el proceso de migración tardará más o menos. Nuestro objetivo, en todo caso, es que dicho proceso se haga, como siempre de una forma que no implique dejar de prestar nuestro servicio.

Podemos distinguir dos casos:
  • clientes con direcciones almacenadas únicamente en los antiguos campos exisistentes en la tabla de clientes
  • clientes con direcciones almacenadas tanto en los campos de la tabla de clientes como en la nueva tabla de direccciones
En el primer caso, basta con crear una nueva entrada en la tabla de direcciones con la misma información que teníamos en los campos de la tabla de clientes. Esto se puede hacer fácilmente con el siguiente script SQL:

¿Qué problema podemos tener con los clientes que ya tengan datos en la nueva tabla de direcciones? En principio, podría parecer que, si esto es así, dicha información tiene que ser la más actual, ya que ha sido creada por una versión del sistema más nueva. Sin embargo, debemos recordar que, durante un tiempo, hemos mantenido dos versiones de nuestro sistema en ejecución al mismo tiempo. Por lo tanto, es posible (y, dependiendo del tráfico que recibamos, puede que incluso probable) que haya habido casos como el siguiente:
  1. un cliente no tiene ninguna dirección
  2. recibimos una petición para actualizar su dirección
  3. dicha petición es atendida por una versión actualizada del sistema
  4. la dirección se guarda en la nueva tabla de direcciones así como en los campos existentes en la tabla de clientes
  5. recibimos una nueva petición para actualizar la dirección del mismo cliente
  6. dicha petición es atendida por la versión antigua del sistema
  7. la dirección se guarda únicamente en los campos existentes en la tabla de clientes
  8. la dirección del cliente no vuelve a actualizarse más
Como vemos, es posible que haya direcciones en los campos de la tabla de clientes que sean más actuales que los datos existentes en la tabla de direcciones. ¿Cómo podemos migrar los datos en estos casos? En este caso es donde se muestra realmente útil el haber introducido, con nuestro primer cambio, el campo con la fecha de actualización de la dirección en la tabla de clientes. Gracias a que tenemos las fechas en las que una dirección ha sido actualizada en ambas tablas (clientes y direcciones), es sencillo determinar cuál de ellas es la más actual en caso de que sean distintas. Basta comparar la fecha de cambio de dirección en la tabla de clientes con la fecha de cambio de dirección de la más reciente entrada en la tabla de direcciones:

Ahora tenemos todos nuestros servidores ejecutando la última versión, que guarda las direcciones en la tabla de direcciones, y hemos migrado los datos de direcciones a la tabla address, incluso aquellos que, estando únicamente en la tabla customer, pudieran ser más actuales. Este es el momento en el que podemos tomar la información de la tabla de direcciones como fuente primaria cuando atendemos consultas de datos de clientes.

Podemos ver cómo ahora devolvemos las direcciones utilizando únicamente la información de la tabla de direcciones. Aún así, seguimos almacenando las direcciones tanto en la tabla de direcciones como en las columnas de la tabla de clientes. Podemos decir que ahora es el antiguo sistema que almacenaba las direcciones en la tabla de clientes el que ha pasado a estar en la sombra (aunque sigue funcinando). No es una mala idea mantener el sistema antiguo en la sombra durante un tiempo antes de eliminarlo definitivamente. Algunas ventajas de hacerlo así son:
  • podemos comparar los resultados con ambos sistemas (si vemos discrepancias podría ser síntoma de que algo no funciona bien)
  • si algo sale muy mal con el nuevo sistema, resulta fácil volver a utilizar el sistema antiguo
  • puede haber otros equipos utilizando el sistema antiguo que necesiten algún tiempo hasta que se adapten al nuevo (de esta forma, tienen más tiempo para llevar a cabo su migración particular)
Por supuesto, en nuestro caso, el precio que tiene mantener los datos de direcciones tanto en la tabla de clientes como en la nueva tabla de direcciones es que nuestro código será más complejo, además de la duplicidad innecesaria de datos en la base de datos. Se trata, en todo caso, de un estado temporal que nos proporciona el beneficio de poder construir la funcionalidad del historial de direcciones, que requería de una refactorización grande en nuestro código y modelo de datos, sin haber dejado de prestar el servicio ni un solo minuto.

Ahora sí podemos dejar de usar las columnas de la tabla de clientes. Con objeto de hacer el cambio compatible entre distintas versiones para poder tener siempre, al menos, un servidor en ejecución, empezamos eliminando las referencias a dichas columnas en nuestro código:

Y después, cuando este cambio haya sido implantado en todos los servidores, podemos definitivamente eliminar las columnas relativas a direcciones de la tabla de clientes.

Ahora sí los cambios necesarios para mantener un historial de direcciones han sido completados. Hemos visto cómo podemos conseguir hacer cambios grandes en nuestro código sin necesidad de parar nuestro servicio, lo que ayuda a conseguir una alta disponibilidad.

Reflexión final

¿Merece la pena dar tantas vueltas para hacer un cambio que podríamos haber implementado en un único paso? La respuesta, como casi siempre, es "depende". Se trata de valorar qué es más importante para nosotros en cada momento. Posiblemente si estamos trabajando con un sistema todavía inmaduro en el que experimentamos constantemente con nuevas funcionaliades que requieren cambios muy grandes, quizá lo que más nos interese es que los cambios estén completamente implementados y en producción lo antes posible, aunque eso signifique que el sistema esté indisponible por algún tiempo cada vez que implantamos un cambio nuevo. Por otro lado, si no queremos que nuestros usuarios experimienten ninguna caída del servicio, o no nos los podemos permitir (por ejemplo, si es un API pública que ofrecemos a terceros y tiene que estar siempre disponible), posiblemente sea una buena idea tomar el camino no tan directo pero siempre compatible que nos garantiza que podemos actualizar nuestros servidores de forma escalonada.

Referencias

Puedes encontrar el código utilizado como ejemplo en este artículo aquí.


Supongamos que estamos construyendo un sistema para la gestión de clientes donde, entre otras cosas, vamos a registrar las compras que realizan. Empezamos con un modelo muy básico, creando una clase para nuestros clientes que nos ofrecerá la funcionalidad necesaria para cumplir nuestra primera regla de negocio:

  • Son clientes senior aquellos que tienen más de año de antiguedad
Aquí tenemos algo de código:

Hemos empezado con un test que falla y continuamos desarrollando la implementación que hace que el test pase:

Seguimos con nuestro modelo. Como dijimos, los clientes compran productos, para los que tenemos un código y la fecha de compra (dicha fecha se establecerá en el momento en que la compra se haga efectiva, no pudiendo modificarse posteriormente). Así que introducimos una nueva clase para productos que ofrecerá una funcionalidad, bastante básica también, con otra regla de negocio:

  • Un producto ha sido comprado recientemente si la compra se produjo en el último mes
Y creamos un test para dicha funcionalidad:

A continuación, añadimos la lógica que hace que nuestro test pase:

Ahora que tenemos clientes y productos, vamos a añadir la lógica que permitirá a los clientes comprar productos. Empezamos, como siempre, por un test que falla:

Y, después de haber creado el test, implementamos dicha lógica y hacemos que el test pase:

Con el código que tenemos a estas alturas, podemos empezar a hacer cosas más interesantes. Nos proponemos hacer ofertas especiales a algunos clientes y de ahí sale nuestra tercera regla de negocio:

  • Sólo los clientes senior pueden recibir ofertas especiales.
Así que añadimos una nueva funcionalidad en nuestra clase de clientes, empezando con tests que fallan:

Y, a continuación, implementamos esta funcionalidad de acuerdo con nuestra regla de negocio para hacer que los tests pasen:

Nuestro código pasa a produción, la gente empieza a comprar y nosotros hacemos ofertas a nuestros clientes para aumentar las ventas. Al cabo del tiempo, decidimos cambiar las condiciones para hacer ofertas a clientes y modificamos nuestra tercera regla de negocio con una nueva versión:

  • Sólo los clientes senior que hayan realizado alguna compra recientemente pueden recibir ofertas especiales.
Como siempre, empezamos por los tests, así que creamos un nuevo test que nos asegure que esta nueva versión de nuestra regla de negocio se cumple. Pero, con el código de test que hemos creado hasta ahora, nos encontramos con la primera dificultad. La clase Product fue diseñada para ser inmutable, por lo que ni el código del producto comprado ni obviamente la fecha de compra deberían poder ser cambiados. Con nuestro código actual, necesitamos establecer una fecha de compra con más de un mes de antelación para poder construir un test que compruebe que los clientes que

  • tienen más de un año de antigüedad pero
  • no han comprado recientemente
no reciben ofertas especiales.

Aquí empiezan las dificultades pues tenemos que, para poder probar nuestro sistema, hay que violar el diseño que nosotros mismos habíamos definido. No pasa nada, hacemos un pequeño ajuste en la clase Product y, a partir de ahí, resulta mucho más fácil probarlo todo:

Ahora ya podemos implementar nuestra nueva regla en la clase Customer:

Y esto es lo que llaman Design for testability, ¿verdad? Hemos modificado nuestro diseño para poder probar el código correctamente. ¡Falso! Esto no es más que un diseño pobre que hemos enmendado de mala manera para poder crear unas mínimas pruebas unitarias, pero dista mucho de ser una buena solución y el problema no hará más que agravarse con el tiempo. El diseño para la prueba no tiene nada que ver con romper el principio de encapsulación y exponer el estado interno de un objeto permitiendo que actores externos puedan alterarlo sin ningún control.

Por supuesto, podríamos intentar usar algún tipo de objeto mock para los productos que compran los clientes, lo cual requeriría configurar dichos mocks para devolver las fechas de compra que necesitamos en nuestra prueba y modificar la propia clase Customer para poder inyectar dichos mocks como productos comprados por el cliente. Sin embargo, esto sería un engorro que nos va a hacer perder mucho tiempo cada vez que queramos probar algo y ya sabemos que hacer que las pruebas sean complicadas de programar supone abandonar la práctica de programar pruebas automáticas.

El problema no está en si la clase Product es inmutable o no, en cómo modificar la fecha de compra de los productos o en cómo injectar instancias mocks de productos en la clase Customer. El auténtico fallo de diseño en nuestro código está en que hemos dejado en manos de la clase Cutomer la creación de la fecha de compra de los productos. Es decir, cuando un cliente compra un producto, establecemos la fecha de compra del product (que debe ser el instance actual, es decir, ahora) y dejamos que sea la propia clase Customer la que decida cuándo es ahora.

El verdadero problema está en que la clase Customer es  responsable de determinar cuándo es ahora, es decir, de instanciar Date.
Un verdadero diseño que facilite las pruebas consiste en delegar la lógica que determina cuándo es ahora, es decir, crear una nueva instancia de la clase Date en el instante actual, a una clase externa que sirva como proveedor de tiempo. Con esta idea, la implementación no podría ser más sencilla:

Ahora ya no es Customer la clase responsable de crear objetos Date sino que los solicita a una clase externa, TimeProvider. De hecho, a partir de ahora ninguna clase en nuestro sistema debería jamás crear sus propias instancias de Date sino pedirlas siempre a TimeProvider.

Habréis notado algo raro en esta implementación de TimeProvider. ¿Qué es ese campo offset y para qué lo queremos? Offset no es ni más ni menos que nuestra implementación del concepto de design for testability. Veamos cómo funciona.

Supongamos, como caso más sencillo, que offset toma un valor de 0. A continuación, solicitamos a TimeProvider la fecha y hora actual llamando al método now(). Pero supongamos que offset tuviera como valor el incremento de tiempo correspondiente a tres días (en milisegundos) y le volvemos a pedir a TimeProvider la fecha y ahora actual, es decir, volvemos a llamar a now(). ¿Qué recibimos? La fecha y hora de dentro de tres días. ¿Y si todo nuestro sistema estuviera conectado a TimeProvider y cada vez que requiriese la hora actual llamara al método now()? Entonces nuestro sistema estaría viviendo en el futuro, tres días en el futuro, para ser exactos.

Para conseguir esto, vamos a construir una clase que solamente pueda ser utilizada por nuestro código de pruebas. Dicha clase nos permitirá ajustar el valor de offset dentro de TimeProvider de forma que siempre sume (o reste) una constante de tiempo al instante actual. Así es como acabamos de crear la máquina del tiempo:

Básicamente esta clase sirve para establecer el valor de offset en TimeProvider. Además le he añadido algunos métodos adicionales para permitir especificar el instante de tiempo al que queremos viajar como Date, LocalDateTime, etc. pero la idea básica es esa: cambiar el offset de TimeProvider. Ahora podemos reescribir nuestro código de pruebas para la nueva versión de nuestra regla de negocio de una forma más clara y sencilla:

Ya no necesitamos cambiar la fecha de compra de los productos ya que ésta se va a establecer en el instante correcto cuando llamamos al método buy(). Nuestra clase Product vuelve a ser inmutable tal y como queríamos. De hecho, todos los tests que hemos creado hasta ahora pueden ser reescritos usando TimeMachine, lo que nos va a ayudar a hacerlos más legibles:

Sólo hay un detalle final que tenemos que tener en cuenta. Como debemos mantener las ejecuciones de los distintos tests independientes unas de otras, es preciso que offset vuelva a cero cada vez que terminamos con una prueba (y también asegurarnos de que es cero cuando una prueba comienza). Para eso, TimeMachine nos proporciona un método reset() que se encarga de ello.

En resumen, hemos llegado a una solución con un mejor reparto de responsabilidades entre las distintas clases del sistema. Esta solución nos ha ayudado a mantener el diseño inmutable que queríamos para nuestra clase Product y ha conseguido que los tests sean más fáciles de escribir y de leer.

Incluso en el caso de que no estemos demasiado interesados en la inmutabilidad de objetos, seguir principios de Design Driven Domain o sencillamente no nos importe llenar nuestras clases de métodos de acceso getters y setters (bueno, hay gente para todo), el tener un proveedor centralizado de fecha y hora para todo el sistema nos puede resultar muy útil cuando estamos construyendo lógica fuertemente ligada a una cronología de eventos y acontecimientos (por ejemplo, cuando estamos implementando un flujo de trabajo y queremos probarlo de forma integral).

Aquí termina la presentación de mi máquina del tiempo particular, una ligera variación del arcano conocimiento que me fue transmitido por Dmytro Iaroslavskyi, un auténtico fenómeno. Espero que os haya resultado interesante esta solución. Por supuesto, cualquier comentario sobre cómo podéis aplicarla, qué modificaciones haríais, qué puntos débiles le veis o cualquier otra cosa que os venga a la mente será más que bienvenida.

Tenéis el código fuente utilizado como ejemplo para esta entrada en github.



Os presento una situación bastante corriente: hemos conseguido un contrato para el mantenimiento de una aplicación web Java, con almacenamiento en una base de datos Oracle, donde se hace un amplio uso de procedimientos almacenados, y un carga fuerte de JavaScript en la capa de presentación. Se trata de un proyecto largo (más de un año) para un equipo pequeño (digamos que el presupuesto no es muy amplio). Hacemos algunos cálculos, pensamos en los escenarios más probables que nos vamos a encontrar y llegamos a la conclusión de que vamos a dimensionar el equipo con tres presonas.

Tenemos ahora que pensar en quiénes formarán parte del equipo. ¿Sabéis una cosa? Lo más probable es que la inmensa mayoría de los responsables de proyecto o, al menos, los que suelen completar sus proyectos con éxito, decidirán tener en su equipo a:

* Un desarrollador Java
* Un experto en bases de datos Oracle
* Un programador web con amplios conocimientos de JavaScript

Parece una decisión bastante lógica, ¿no? Pues bien, no es más que la inversión de una regla bastante antigua aunque, a veces, desconocida en el desarrollo de software: la ley de Conway.

Cualquier organización que afronte el proceso de diseñar un sistema de información diseñará uno que copie las estructuras de comunicación de la organización.

O, dicho de otra forma:

La arquitectura de un sistema refleja la estructura del equipo que la diseñó.

Junta a un desarrollador Java con un programador JavaScript y un experto Oracle y tendrás una aplicación en tres capas, posiblemente con procedimientos almacenados en la base de datos y una interfaz rica en el navegador. ¿No quieres procedimientos almacenados? Quita al experto en Oracle del equipo. Organiza un equipo de tres desarrolladores para construir un compilador y tendrás un compilador en tres fases. Estos diseños serán utilizados incluso aunque no sean los más apropiados para el sistema que queremos construir.

Las decisiones sobre la arquitectura del sistema se empiezan a tomar incluso antes de que nadie haya dibujado el primer diagrama ni escrito la primera línea de código: las tomamos cuando decidimos el equipo que queremos para nuestro proyecto.

Se toman cuando menos información tenemos sobre el sistema (justo al principio del proyecto) y las toman las personas que menos conocimientos tienen para hacerlo (los jefes de proyecto).

¿Cómo podemos evitar esto?


En primer lugar, intenta empezar el proyecto con el equipo más pequeño posible. Ten en cuenta que cada persona que añadas al equipo del proyecto supone una carga más en la arquitectura del sistema. Y demasiada arquitectura puede ser incluso más perjudicial para el resutado que tener muy poca arquitectura. Formando un equipo pequeño al principio, estarás tomando las mínimas decisiones sobre la arquitectura final del proyecto.

En segundo lugar, busca los perfiles más generalistas a la hora de formar el equipo en vez de meter a personas con una única (o un reducido número de) habilidades. En este caso, es mejor ese desarrollador Java al que no le importa trabajar en la base de datos y que puede defenderse a la hora de hacer interfaces web que un gurú de Oracle o un ninja del JavaScript. Puede que, con el tiempo, llegues a necesitas esos perfiles, pero te aseguro que es mejor retrasar su incorporación hasta que la arquitectura esté completamente clara.

Como podéis ver, todos los proyectos tienen, al menos, un arquitecto. Uno que decide sobre el diseño del sistema de una forma muy peculiar: sin diagramas ni documentos de arquitectura sino asignando personas al equipo que se va a encargar de construirlo.

Mi recomendación para cualquier jefe de proyecto es que evite en la medida de lo posible convertirse en ese arquitecto. Por el bien de su proyecto.

Bibliografía