No dejes que tu caso específico aumente la complejidad de tu código: Special case pattern

En nuestro día a día tenemos nuestra aplicación funcionando cuando de repente aparece un nuevo requerimiento. Empezamos a picar más código para responder a esta petición. Si empezamos a teclear sin prestar suficiente atención, podemos acabar añadiendo más complejidad de la necesaria. Antes de llegar a este punto, a veces revisitar los patrones de diseño puede ayudarte a encontrar una mejor solución que se ajuste a tus necesidades y así evitar bugs potenciales o problemas relacionados con el nuevo código.

Ejemplo: Blog Application

Empecemos con un blog que tiene dos casos de uso (no nos importan los demás):

  • BlogPostApplicationService
    • Crear post del blog (title, text, authorId, date)
    • Ordenar los posts por fecha
  • AuthorApplicationService
    • Lista con los autores (name, last name, picture)

Nuestro PO pide una nueva funcionalidad: algunos artículos se crearán automáticamente por un bot de scraping.

  • Este bot creará nuevos artículos
  • Estos artículos aparecerán al listar todos los posts
  • El bot no dispondrá de perfil

¿El problema? Cada artículo necesita un authorId y el bot no es un autor.

Posible Solución: Nuestro primer acercamiento puede ser el de marcar el authorID como opcional.

Esta solución no encaja demasiado bien. Para solucionar un problema específico (un caso en el que no tenemos authorID), modificamos una norma importante de nuestro dominio (todos los artículos precisan de un autor). Además, si hacemos authorID opcional, deberemos comprobar en varias ocasiones si es null (estamos repitiendo constantemente el mismo código –> es decir, estamos generando un log de duplicación).

Además, al escribir las queries para obtener todos los posts con la información del autor, se debe usar un left join (no un inner join) ya que las entradas del bot no deben volver ninguna información de autor.

Otra solución posible: definir el bot con una constante en nuestro código (con un 0, por ejemplo.) Haciendo esto podríamos evitar tener el authorID como nullable (ahora todas las entradas tendrían un autor) pero esa id constante no tendría ningún significado en nuestro dominio. Acabaríamos con nuestro código lleno de ‘if ($authorId === BOT_ID)’ solo para satisfacer este caso específico. Tampoco es esta una solución óptima.

Con estas soluciones simples aumentamos la complejidad de nuestro código solo para satisfacer un caso específico. Añadimos multitud de null checkings a nuestro código, y ya sabemos que pasa con los nulls (ver el famoso “billion dollar mistake“). Es hora de comprobar si existe algún patrón que nos pueda ayudar con este problema.

Special Case Pattern

Una subclase que proporciona un comportamiento especial a ciertas clases. Por Martin Fowler (https://martinfowler.com/eaaCatalog/specialCase.html)

special case pattern

El Special Case Pattern es un patrón que se ajusta bien a nuestro problema. La idea es crear una subclase de Author llamada Bot para gestionar nuestro caso especial: nuestro bot debería ser capaz de escribir un post como autor pero con sus propias especificaciones, por ejemplo no vamos a mostrar su página de perfil. Esta solución está bien porque no nos obliga a cambiar nuestro código (Bot ya es un Author).

Una vez detectado el patrón adecuado, ¿Cómo podemos implementarlo en nuestro proyecto?

Single Table Inheritance

Representa una jerarquía heredada de clases en forma de tabla que tiene columnas para cada campo de las distintas clases. Por Martin Fowler (https://martinfowler.com/eaaCatalog/singleTableInheritance.html)

single table inheritance

¿Cómo podemos implementar esta herencia en nuestra base de datos? Las bases de datos relacionales no soportan las herencias, pero podemos usarla a través de ORMs que implementen las herencias (en doctrine por ejemplo)

Usando single table inheritance, las distintas clases (en nuestro caso Author y Bot) están mapeadas a una tabla con una columna (en la imagen, ‘type’) donde define el tipo de clase para cada entrada. Cuando recuperamos esta entrada, el ORM la mapeará a su propia entidad, escondiendo la implementación final.

Vamos a implementarlo todo:

Usando doctrine, creamos la tabla Writer donde se almacenarán los Bots y Authors.

/**
 * @ORM\Table(name="writer")
 * @ORM\Entity()
 * @InheritanceType("SINGLE_TABLE")
 * @DiscriminatorColumn(name="type", type="string")
 */

class Author
{
    protected $id;
    protected $name;
    protected $lastName;
    protected $picture;
}

/**
 * @ORM\Entity()
 */

class Bot extends Author
{
}

Podríamos añadir una migración de bases de datos para crear un usuario bot en nuestro sistema con una id predefinida:

$bot = new Bot("BOT_ID", "Bot", "", "");

De ahora en adelante, el bot usará esta id constante (“BOT_ID”) y los problemas mencionados más arriba se solucionan:

  • No más comprobaciones tipo “if author is null”
  • Cuando hagamos querie de la bases de datos, todas las entradas tendrán información válida de autor.

Para mostrarle al cliente la diferencia entre autores, cuando mapeamos las entidades a los DTOs podemos fácilmente añadir un flag:

class AuthorDto
{
    public $id;
    public $firstName;
    public $lastName;
    public $picture;
    public $system = false;
}

class BotUserDto extends AuthorDto
{
    public $system = true;
}

Si system es true, el frontend puede ahorrarse añadir un link al perfil del bot ya que no es uno de nuestros requerimientos.

Conclusión

Hemos evitado que el caso especial de nuestra aplicación aumente la complejidad del código (con los bugs y riesgo que eso conllevaría) y mediante un par de patrones nuestro código se ve más limpio. Además, hemos evitado los problemas derivado de la nullability (por ahora, por lo menos).