Dónde poner el conocimiento

En el que recurrimos a principios básicos de asignación de responsabilidades para averiguar qué objetos deberían saber qué cosas.

Notas de la segunda edición

En este capítulo introduciremos los principios GRASP, los cuales habíamos mencionado en la edición anterior, por lo que cambiamos casi por completo los ejemplos. Por otro lado, hemos eliminado la sección sobre el patrón Specification, que resulta demasiado sofisticado como para ser útil en el contexto de refactoring que estamos tratando.

Refactorizar para trasladar conocimiento

Solemos decir que refactorizar tiene que ver con el conocimiento y el significado. Fundamentalmente, porque lo que hacemos es aportar significado al código con el objetivo de que este represente de una manera fiel y dinámica el conocimiento cambiante que tenemos del negocio del que nos ocupamos. Y, por otro lado, porque lo que perseguimos con un refactoring continuado es representar nuestro entendimiento actual del negocio en el código con la mayor precisión posible.

En el código de una aplicación tenemos objetos que representan alguna de estas cosas:

  • Conceptos, ya sea en forma de entidades o de value objects. Las entidades representan conceptos que nos interesan por su identidad y tienen un ciclo de vida. Los value objects representan conceptos que nos interesan por su valor.
  • Relaciones entre esos conceptos, que suelen representarse en forma de agregados y que están definidas por las reglas de negocio.
  • Procesos que hacen interactuar los conceptos conforme a reglas de negocio también.

Uno de los problemas que tenemos que resolver al escribir código y al refactorizarlo es dónde poner el conocimiento y, más exactamente, las reglas de negocio.

Si hay algo que caracteriza al legacy es que el conocimiento sobre las reglas de negocio suele estar disperso a lo largo y ancho del código, en los lugares más imprevisibles y representado de las formas más dispares. El efecto de refactorizar este código es, esperamos, llegar a trasladar ese conocimiento al lugar donde mejor nos puede servir.

Pero incluso en código nuevo, el conocimiento puede estar disperso. Puede ser debido a que no conocemos bien nuestro negocio todavía, o porque no somos capaces de expresarlo mejor en un momento dado. Además, el código siempre va a tener un cierto desfase con lo que sabemos del negocio, porque el negocio cambia y nuestro conocimiento de él también.

Para saber donde colocar el conocimiento en el código podemos recurrir a varios principio y patrones.

Principios básicos

Principio de abstracción. Benjamin Pierce formuló el principio de abstracción en su libro Types and programming languages:

Each significant piece of functionality in a program should be implemented in just one place in the source code. Where similar functions are carried out by distinct pieces of code, it is generally beneficial to combine them into one by abstracting out the varying parts.

DRY. Por su parte, Andy Hunt y David Thomas, en The Pragmatic Programmer, presentan una versión de este mismo principio que posiblemente te sonará más: Don’t Repeat Yourself:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

En esencia, la idea que nos interesa recalcar es que cada regla de negocio estará representada en un único lugar y esa representación será la de referencia para todo el código.

Los principios que hemos enunciado se centran en el carácter único de la representación, pero no nos dicen dónde debe residir la misma. Lo cierto es que es un tema complejo, pues es algo que puede admitir varias interpretaciones y puede depender del estado de nuestro conocimiento actual del negocio.

Buscando dónde guardar el conocimiento: patrones GRASP

Los patrones GRASP son un conjunto de patrones de diseño que nos ayudan a asignar responsabilidades a los objetos de un sistema. Fueron introducidos por Craig Larman en Applying UML and Patterns, que identifica una serie de preguntas que nos podemos hacer para saber dónde colocar el conocimiento en un sistema orientado a objetos.

Regla general: en los objetos que tienen la información necesaria

Este patrón se llama Information Expert y es el más general de todos. Una responsabilidad se asignará al objeto que tenga la información necesaria para ejercerla.

En el contexto de refactoring, lo que nos dice este principio es que cuando estamos usando la información contenida en un objeto, ese uso o comportamiento tendría que estar en ese mismo objeto. Expresándolo en otras palabras, quiere decir que un objeto debe ser capaz de realizar todos los comportamientos que le sean propios, dentro del contexto de nuestra aplicación. Para ello no debería necesitar exponer sus propiedades internas o estado.

Por tanto, cuando preguntamos a un objeto sobre su estado y realizamos acciones basadas en la respuesta, lo suyo debería ser encapsular esas acciones en forma de comportamientos del objeto. Para ello, podemos seguir el principio Tell, don’t ask. Esto es, en lugar de obtener información de un objeto para operar con ella y tomar una decisión sobre ese objeto, le pedimos que lo haga él mismo y nos entregue un resultado si es adecuado para el contexto. Esto lo trataremos con más detalle en el siguiente capítulo.

Los value objects y entidades son lugares ideales para encapsular conocimiento de dominio.

Supongamos que en nuestro negocio estamos interesados en ofrecer productos o ventajas a usuarios cuya cuenta de correo pertenezca a ciertos dominios. Un ejemplo de esto son los programas de beneficios de algunas empresas. El correo electrónico es, pues, un concepto importante del negocio y lo representamos mediante un value object:

 1 class Email
 2 {
 3     private string $email;
 4 
 5     public function __construct(string $email)
 6     {
 7         $this->email = $email;
 8     }
 9     
10     public static function valid(string $email)
11     {
12         if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
13             throw new InvalidArgumentException(sprintf('%s is not valid\
14  email.', $email));
15         }
16         return new self($email)
17     }
18 
19     public function __toString(): string
20     {
21         return $this->$email;
22     }
23 }

En un momento dado nos puede interesar saber si un empleado tiene acceso a un beneficio concreto, cosa que vamos a controlar obteniendo la lista de dominios corporativos que lo ofrecen. Podríamos hacerlo de esta manera:

 1 class CanSeeBenefit
 2 {
 3     public function byCorporateEmail(Email $email)
 4     {
 5         [, $domain] = explode('@', (string)$email);
 6 
 7         if (!in_array($domain, $this->getBenefitDomains->execute(), tru\
 8 e)) {
 9             return false;
10         }
11         
12         return true
13     }
14 }

Como se puede ver, estamos pidiendo su valor a $email para poder extraer el dominio y compararlo con la lista de dominios. Por definición, sabemos que un email se compone de un nombre de usuario y un dominio, así que lo lógico sería preguntarle a $email por su dominio y no calcularlo fuera de él.

 1 class Email
 2 {
 3     private string $email;
 4 
 5     public function __construct(string $email)
 6     {
 7         $this->email = $email;
 8     }
 9     
10     public static function valid(string $email)
11     {
12         if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
13             throw new InvalidArgumentException(sprintf('%s is not valid\
14  email.', $email));
15         }
16         return new self($email)
17     }
18 
19     public function domain(): string
20     {
21         [, $domain] = explode('@', $this->email);
22 
23         return $domain;
24     }
25 
26     public function __toString(): string
27     {
28         return $this->$email;
29     }
30 }

Este es un primer paso para trasladar el conocimiento al lugar donde mejor se puede usar.

 1 class CanSeeBenefit
 2 {
 3     public function byCorporateEmail(Email $email)
 4     {
 5         if (!in_array($email->domain(), $this->getBenefitDomains->execu\
 6 te(), true)) {
 7             return false;
 8         }
 9         
10         return true
11     }
12 }

Pero en el fondo esto no soluciona completamente el problema. Ahora podemos obtener el dominio de un email y, aunque se obtenga de un cálculo, no deja de ser el acceso a una propiedad. Cierto es que lo hemos implementado de tal forma que necesitamos calcular el dominio, pero podría no ser así. Mira, por ejemplo, esta versión:

 1 class Email
 2 {
 3     private string $username;
 4     private string $domain;
 5 
 6     private function __construct(string $username, string $domain)
 7     {
 8         $this->username = $username;
 9         $this->domain = $domain;
10     }
11 
12     public static function valid(string $email): self
13     {
14         if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
15             throw new InvalidArgumentException(sprintf('%s is not a val\
16 id email.', $email));
17         }
18 
19         [$username, $domain] = explode('@', $email);
20         return new self($username, $domain);
21     }
22 
23     public function domain(): string
24     {
25         return $this->domain;
26     }
27 
28     public function __toString(): string
29     {
30         return $this->username . '@' . $this->domain;
31     }
32 }

Entonces, ¿qué podríamos hacer? Pues la respuesta es invertir la cuestión, En lugar de extraer si el dominio de Email para mirar si está en la lista que tenemos, lo suyo es pasarle la lista para que nos diga si su dominio está en ella:

 1 class Email
 2 {
 3     private string $username;
 4     private string $domain;
 5 
 6     private function __construct(string $username, string $domain)
 7     {
 8         $this->username = $username;
 9         $this->domain = $domain;
10     }
11 
12     public static function valid(string $email): self
13     {
14         if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
15             throw new InvalidArgumentException(sprintf('%s is not a val\
16 id email.', $email));
17         }
18 
19         [$username, $domain] = explode('@', $email);
20         return new self($username, $domain);
21     }
22 
23     public function belongsToOneOfThisDomains(array $domains): bool
24     {
25         return in_array($this->domain, $domains, true);
26     }
27 
28     public function __toString(): string
29     {
30         return $this->username . '@' . $this->domain;
31     }
32 }

Ahora podemos usarlo así:

1 class CanSeeBenefit
2 {
3     public function byCorporateEmail(Email $email)
4     {
5         return $email->belongsToOneOfThisDomains($this->getBenefitDomai\
6 ns->execute());
7     }
8 }

Con este cambio resulta que Email ya no tiene que mostrar ninguna de sus propiedades. Esto nos da libertad para cambiar su implementación sin tener que cambiar el código que lo usa. Y, por otro lado, expone comportamiento que puede ser usado por otros objetos interesados.

Quien se ha de encargar de construir un objeto

Este patrón se llama Creator y nos dice que la responsabilidad de crear un objeto debe recaer en aquel que tenga la información necesaria para hacerlo, o bien que coleccione o agrupe los objetos que se van a crear.

El ejemplo paradigmático de este patrón es el de un objeto que representa una factura y sus líneas. La responsabilidad de crear una línea de factura debería recaer en la propia factura, ya que aunque no tenga la información necesaria para crearla, sí que agrupa las líneas de factura. De hecho, las líneas de factura no tienen sentido fuera de una factura.

1 class Invoice
2 {
3     private array $lines = [];
4 
5     public function addLine(string $description, float $amount): void
6     {
7         $this->lines[] = new InvoiceLine($description, $amount);
8     }
9 }
 1 class InvoiceLine
 2 {
 3     private string $description;
 4     private float $amount;
 5 
 6     public function __construct(string $description, float $amount)
 7     {
 8         $this->description = $description;
 9         $this->amount = $amount;
10     }
11 }

Resumen del capítulo

En este capítulo introducimos algunos patrones útiles para decidir donde poner las responsabilidades en un sistema de software. Estos patrones son los GRASP, que nos ayudan a asignar responsabilidades a los objetos de un sistema. En concreto, hemos visto el patrón Information Expert y el patrón Creator.