En Scala, a la hora de trabajar con objetos, puede ser normal tener la necesidad, y junto a esta necesidad, la posibilidad o la capacidad ligada de modificar o editar una propiedad intrínseca o un atributo propio de ese objeto, o incluso el mismo objeto en sí tras haberlo instanciado; consiguiendo realizar una serie de transformaciones más o menos secuencial de alguna forma más o menos sencilla e intituiva.
Sin embargo, aunque fuera natural tener esa necesidad, no es tan trivial tener esa posibilidad, al menos si se quiere hacer de manera correcta.
Lo cierto es, que para clases ya predefinidas, Scala suele disponibilizar un mecansimo más o menos inmediato para hacer esto. Pero, ¿Qué pasa para aquellos objetos que son instancias de nuestras propias clases? ¿Tenemos ese mecanismo? Si es así, ¿Modifica lo que queremos que modifique de la forma en la que nos gustaria que lo hiciera? O por el contrario, debemos incluirlo nosotros mismos. ¿De qué forma se incluye esto?
La manera más evidente y natural es definir dicha propiedad o capacidad de atributo modificable mediante un var y, a posteriori, mutar su valor.
Este es el caso más natural para un programador (sobre todo si eres recien llegado), dónde se dispone de un punto de guardado o checkpoint de algun valor y a lo largo de la ejecución se va modificando. Sin embargo, en este blog somos alérgicos a lo mutable, nos surje una sensación de urticaria aguda y se nos empieza desorbitar los ojos sólo de pensar en escribir un var.
No sólo por los principales inconvenientes de la programación imperativa (como por ejemplo, que se defina una variable mutable global y que en mitad de un proceso esperes un 4 pero te salta una excepción porque ahi venia un -10. Sino porque, si además damos el salto a sistemas concurrentes y reactivos, ya casi que mejor apagamos y nos vamos.
Efectivamente, necesitamos inmutabilidad.
Porque seamos sincero, ¿Qué es un poco más de código y unos cuantos truquitos de diseños de patrón, para poder conseguir seguridad, corrección y testeabilidad?
Para ello, empecemos hablando de una técnica muy común como es la de sobreescribir esa propiedad en el momento de la creación de la nueva instancia de nuestra clase mediante el uso del override.
Así es, una técnica muy común es crear una nueva instancia y sobrescribir (override) esa propiedad o atributo en el momento de la nueva instanciación.
Sin embargo esto puede generar boilerplate en nuestro código cada vez que tengamos que hacer modificación de una propiedad. Y más aún si necesitamos crear una copia de una instancia para modificar varios valores y que además nos interesa mantener varios atributos concretos cuyo valor no es el de por defecto de su clase o álgebra.
Tendríamos que ir tomando cada uno de estos atributos que queremos mantener, crear una nueva instancia e ir sobrescribiendolos (para matenerlos en la nueva instancia) y por último añadir/sobreescribir (override) el atributo en cuestión objeto de interés que desde un comienzo queriamos modificar.
Por ello, escribo este artículo para proporcionar un punto de vista de como abordar este problema.
Lo que vamos a hacer, por resumir, es darle a nuestra clase la capacidad de copiarse modificando algún atributo suyo. Esto de dar la capacidad de, es un concepto bastante interesante e importante. Puesto que de la forma en la que vamos a dar esta solución, se puede escalar a cualquier tipo de capacidad o caracteristica que se quiera añadir: capacidad de mostrarse (Show), capacidad de compararse (Eq), capacidad de printearse, de exportarse, de transformarse a otra cosa, etc.
Además, desarrollaremos esta idea de manera bastante modular, intentando evitar a toda costa patrones de tarta o cake patterns.
Es cierto que existen otras técnicas como el uso de lentes; pero cuando lo que se pretende es algo muy concreto, para un atributo o dos como mucho, no veo la necesidad de usarlas.
Para nuestro relato, partiremos del siguiente caso de uso: nos gustaria desarrollar un sistema que sea capaz de gestionar objetos Config de configuración. Y que además desde este, se extienda varios subtipos (hijos) concretos que puedan aplicarse para entornos (environments) distintos.
Vamos primeramente a definir la copiabilidad como tal, es decir, vamos a proveerle de la capcidad de copiarse (todavia sin tener la posibilidad de modificarse y generar una mutación).
Haciendo composición entre nuestras clases, podemos incorporar y definir el método copy() para cada una de los subtipos.
Pero nuestra principal motivación no era crear copias, sino más bien generar nuevas instancias que mantuvieran los atributos de la instancia original, con atributos modificados de manera inmutable. La idea es básicamente, a partir de nuestro objeto original, crear instancias nuevas pero cambiando el valor de algun atributo en tiempo de creación de esa instancia.
Podemos entonces replantearnos y redefinir nuestro método copy() añadiendole funcionalidad mediante parámetros:
Con esto, hemos conseguido una forma de poder mutar modificando atributos pero sin sobreescribir el valor original, sino haciendo una copia de nuestro objeto mediante copy() y pasándole por parámetros el atributo que queremos modificar.