Como sabemos, un parámetro es todo valor que le proporcionemos a un método para que con él realice su cometido. El patrón Parameter Object surge de la necesidad de agrupar varios parámetros que suelen utilizarse juntos a lo largo de un programa. Estos parámetros se utilizan juntos por una coherencia que existe entre ellos (caso contrario, no estamos ante un candidato a Parameter Object). No debemos caer en la trampa de agrupar bajo un mismo objeto cualquier conjunto de valores: si agrupamos dos conceptos prematuramente, no será fácil desagruparlos luego.
El código fuente del video (separado en diferentes commits según el incremento) está disponible en Github para su consulta.
Un Parameter Object debe ser inmutable, ya que una vez definido el conjunto de valores, el objeto que los reúna no debería ser capaz de cambiarlos por ninguna razón.
La coherencia entre los parámetros seleccionados para conformar el Parameter Object es de primordial importancia, ya que una buena elección de parámetros nos permite hacer crecer dichos objetos en forma encapsulada (es decir, agregando la funcionalidad que dichos atributos puedan requerir a lo largo de nuestro programa).
Los ejemplos clásicos que se encuentran en la literatura y en diversas fuentes, generalmente construyen un objeto del tipo “rango”, que tiene tanto un conjunto de fechas, o de valores numéricos, o iniciales. Aquí vemos uno:
class SavingsAccount
# ...
def movements_between(start_date, end_date); end
def income_between(start_date, end_date); end
def expenses_between(start_date, end_date); end
def interests_between(start_date, end_date); end
end
Este caso puede reescribirse de la siguiente manera, evitando pasar dos parámetros cada vez, y reutilizando el objeto creado para tal fin:
class DateRange
attr_reader :start_date, :end_date
def initialize(start_date, end_date)
@start_date = start_date
@end_date = end_date
end
end
class SavingsAccount
# ...
def movements_between(date_range); end
def income_between(date_range); end
def expenses_between(date_range); end
def interests_between(date_range); end
end
Como podemos ver en el fragmento anterior, la necesidad de utilizar una fecha de inicio y una fecha de fin para diversas operaciones sugiere la introducción de un parámetro que permita modelar este tipo de datos.
El código fuente conforma un medio de comunicación de ideas en sí. La introducción explícita de un Parameter Object, más allá de las ventajas que analizaremos aquí, está enviando un mensaje al lector. Le dice “este conjunto de datos tiene sentido en forma agrupada, y creemos que seguirá utilizándose de esta manera a lo largo de todo el programa… usá esta abstracción que te va a facilitar el desarrollo”.
Adicionalmente, ¿por qué querríamos agrupar parámetros? Una vez que se identifica la existencia de este tipo de objetos, se comienza a detectar funcionalidades que podrían acompañar a este grupo de atributos. Es así que en el ejemplo anterior se podría pensar en agregar operaciones que permitieran hallar el solapamiento entre rangos de fechas, o la inclusión de cierta fecha en cierto rango, ahorrandonos tener ese código desperdigado por diversos lugares entre los fuentes del programa.
Asimismo la lectura del código representa una carga mental mucho menor. Echemos un vistazo a este ejemplo:
account.movements_between(start_date, end_date)
Y ahora comparemoslo con el siguiente:
account.movements_between(date_range)
El concepto de “rango” ya tiene significado en sí mismo para nosotros, humanos. Su semántica permite dar por sentado que un rango de fechas tiene un inicio y un final: no necesitamos construir mentalmente ese artefacto, desde sus bases, sino que deducimos las bases desde el concepto general.
Es más, la primera versión de ese código nos plantea la pregunta de “¿habrá una fecha de fin, o será hasta la fecha actual?”. En la segunda versión, sin embargo, no hay dudas de que el rango tiene ambos extremos definidos.
Más allá de que una mala implementación puede proporcionarnos un Magic Container (es decir, un contenedor que agrupa valores no cohesivos pero que parece correcto), tenemos que abordar las desventajas de implementar el patrón correctamente.
Por empezar, la implementación debe ser ordenada. Si no hubiera un conjunto de pruebas que cubra suficientemente el código podemos encontrarnos con errores introducidos al intentar hacer este cambio. En el video que acompaña la nota vemos un ejemplo de introducción de Parameter Object ordenada, pero no siempre es así de inmediato.
Luego, tendremos un objeto más. Agregar un concepto al sistema aumenta la carga cognitiva del análisis del programa. Es por ello que dicho objeto debe pagar por sí mismo más de lo que cuesta introducirlo: debemos preguntarnos si es preferible tener un objeto más, o seguir manejando N parámetros en forma individual.
También podemos encontrarnos con que la existencia de este nuevo objeto nos presenta la tentación de agregarle funcionalidad, creyendo que estamos encapsulando pero simplemente introduciendo indirecciones que entorpecen la codificación de la solución. En el video que acompaña la nota podemos ver un ejemplo de esto, con el método
def >= other
# ...
end
Dicho método surge de la incorporación de cierta lógica en el objeto nuevo, pero falla en el concepto de uniformidad: no todos los atributos pueden analizarse con comparaciones por todo el grupo, sino que algunos casos se comprueban mediante las Estadísticas, y otros mediante un análisis individual de los atributos (es el caso del Arma y la Poción).