miércoles, 12 de diciembre de 2018

The big idea is messaging

Esta es la continuación de la entrada enterior.

Pues sí, mensajes; el concepto más importante de la P.O.O. y el más ignorado y olvidado tanto por desarrolladores como por los diseñadores de lenguajes de programación O.O. Y eso es una pena porque los mensajes aportan un cosa imprescindible: Contexto. Y ese contexto es lo que nos permite diseñar agregados que no tengan que ser elementos persistibles y que no filtren detalles de su implementación.


El primer mensaje es bastante obvio; lo que recibimos de la interfaz de usuario o de otro sistema es un Comando; el propio comando indica qué se solicita hacer y la información que contiene el comando nos indica con qué y en qué se solicita.

Partimos del desproposito anterior:

public class ApplicationServices.MyService{
  public void DoSomething(...){

     MyAggregate agg = myAggRepository.Get(aggId);

    agg.DoSomething(...); //cambia su estado interno con el nuevo estado del sistema

    myAggRepository.Save(agg); //coge su estado interno y lo persiste
  }
}

Lo modificamos para que acepte un comando:

public class ApplicationServices.MyService{
  public void DoSomething(myCommand command)


Este comando (simple DTO) debe contener toda la información necesaria para aplicar la operación del servicio.

Ya tenemos un mensaje con contexto: nuestro sistema sabe exáctamente que acción se ha solicitado independientemente de en qué función y/o objeto nos encontremos. Podemos modelar el repositorio para que monte el agregado necesario para esa operación sin que el servicio tenga que usar explícitamente una función del repositorio ni el repositorio tenga que "saber" que está siendo usado por la función "DoSomething".

public class ApplicationServices.MyService{

  public void DoSomething(myCommand command){

    MyAggregate agg = myAggRepository.Handle(command);
  }

}

Lo mismo podemos hacer con el agregado. Modifiquémoslo para que gestione el comando:

public class ApplicationServices.MyService{

  public void DoSomething(myCommand command){

    IMyAggregate agg = myAggRepository.Handle(command);
    agg.Handle(command);
  }

}

Y ahora viene la pieza gorda de puzle que hace que todo encaje de una forma tan bonita que no tengo palabras. Vamos a hacer el agregado inmutable; el estado interno del agregado no cambia. Ese estado interno sólo se usa para comprobar las invariantes del dominio y aplicar las reglas y los cálculos de los cambios. Los cambios sufridos en el dominio serán retornados por el agregado en forma de otro mensaje que en este caso lo vamos a contextualizar como un evento de un hecho pasado.

public class ApplicationServices.MyService{

  public void DoSomething(myCommand command){

    IMyAggregate agg = myAggRepository.Handle(command);
    SomethingHanppened myEvent = agg.Handle(command);
  }

}

En resumen, se expresan los cambios sufridos en el dominio en forma de evento.

Este evento se utilizará para persistir los cambios en BD y para notificar ese cambio a los demás componentes del sistema interesados.

public class ApplicationServices.MyService{

  public void DoSomething(myCommand command){

    IMyAggregate agg = myAggRepository.Handle(command);
    SomethingHanppened myEvent = agg.Handle(command);
    IPersistence.Handle(myEvent);
    IMessageBus.Handle(myEvent);
    }

}

Como la capa de persistencia (IPersistence) tiene una implementación concreta para Handle(SomethingHappened); esta sabe perfectamente qué tiene que hacer para persistir los cambios puesto que sabe lo que está pasando, tiene mensaje, tiene contexto. Puede tener que actualizar 2 campos de una tabla, otro campo de otra, una inserción de una fila entera en una tercera tabla, etc. Un campo puede necesitar ser actualizado aunque no sea necesaria su lectura primeramente para que el agregado realice su trabajo y viceversa; un campo puede necesitar ser leído para que el agregado haga su trabajo pero no es necesario persistirlo porque esa operación nunca cambia el valor de ese campo. 

Hemos independizado y quitado paja del agregado y de la capa de persistencia puesto que el agregado puede retornar eventos con "campos" a persistir que ni siquiera necesita tener en su estado interno (van a cambiar sí o sí al aplicar la operación, independientemente del valor que contengan) y el agregado puede tener "campos" en su estado interno que no es necesario persistir (sólo se leyeron para aplicar las invariantes del dominio y/o en cálculos de los cambios) y son, por lo tanto, innecesarios para la capa de persistencia.

Hemos eliminado el desajuste por impedancia existente entre el estado de un agregado y la persistencia de los cambios.

Dejo para otra entrada posterior los detalles de las tripas de un agregado inmutable.

PD: ¿Os habéis fijado lo que se parecen los paradigmas O.O. y funcionales siempre y cuando se haga bien el diseño O.O.? La guerra no debería ser "O.O. vs Func"; debería ser simplemente "no hagas mierdacas en O.O. que te astillo. ¡Gañan!"

Edit 13/12/2018

Lectores de este blog me han comentado en persona que eso de que todo se llame "Handle" no es buena idea porque pierdes la "pista" sobre qué está haciendo el objeto ya que la función no tiene un nombre especifico estilo "HazEstaAccionConEstosDatos". No estoy de acuerdo. Si tienes bien diseñada y segregada la responsabilidad ÚNICA del objeto no hay problemas. Centrémonos, por ejemplo, en el Repositorio. Su única responsabilidad es construir agregados a partir de los datos de persistencia y el input de entrada. La firma de la función especifica qué agregado retorna según el input de entrada; que, al ser un comando, nos indica explícitamente cual es la operación que queremos realizar; por lo que se convierte en treméndamente obvio lo que hace la función "Handle" de un repositorio.

Es cuando tienes un mal diseño y mensajes churros de datos sin contexto cuando tienes que empezar a crear funciones con nombres increíblemente explícitos; ya que como el objeto no tiene una responsabilidad única y el mensaje churro de datos no significa ni representa nada concreto (falta de contexto) hay que darle un significado a la operación a través del nombre de la función.


Edit 26/12/2018

Otra razón por la que me gusta que las funciones se llamen "DoYourFuckinWorkMadaFacka" "Handle" es porque, si estamos intentando crear un sistema con abstracción de implementación y bajo acoplamiento, el llamador de la función no tiene que saber qué hace la función llamada; más allá de que retorna un tipo. Volvamos al Repositorio. Si llamamos a la función "ObtenerAgregadoPara(Comando)"; ¿Que hacemos si el Repositorio, ademas de obtener el Agregado, tiene que escribir en un Log o tiene que aplicar algún modelo de seguridad o de segregación multitenant? ¿Llamamos a la función ObtenerAgregadoConLogYSegregacionMultitenantPara(comando) o lo dejamos como está?

Si cambiamos el nombre estamos filtrando detalles de la implementación y si dejamos el nombre actual estamos creando un sesgo cognitivo que nos "chirria" en la cabeza puesto que si, para escribir código decente, las funciones deberían hacer sólo una cosa y la función se llama "ObtenerAgregado" ¿Qué hace el Repositorio enredando con logs y con segregaciones multitenant?

La función se llama "Handle" porque es justamente lo que hace: Manejar el comando coordinando una serie de funcionalidades y como es un Repositorio pues retorna un agregado. Fijaos que el retorno de la función, un Agregado, está más asociado a la responsabilidad única de la Clase que a la función en si.

2 comentarios:

  1. Hombre, yo estoy de acuerdo con tus criticos compañeros. Handle es muy mal nombre. Es mejor DoItYouMadaFacka. Más claro.

    ResponderEliminar
    Respuestas
    1. Hmmm me gusta...

      Ctrl+R Ctrl+R "Handle" -> "DoYourFuckinWorkMadaFacka"

      Eliminar