Construyendo un Bus de Comandos

Con seguridad existen varios paquetes disponibles que implementan un bus de comandos con distinto nivel de complejidad y funcionalidad, sin embargo si deseas implementar algo sencillo o tener este componente totalmente en tu control, a continuación mostraré un ejemplo que puede servir como base.

La infraestructura de un bus de comandos propuesta estará compuesta por tres componentes principales:

  1. Comandos, que son objetos de datos que se pasan al bus de comandos
  2. Controladores (Handlers), que se ocupan de receptar un comando y completar la tarea requerida.
  3. Bus de comandos, que es responsable de despachar comandos a los controladores (handlers) y crear instancias de los objetos necesarios.

Está implmenetación no es muy diferente a la implementación de un bus de eventos, sin embargo la gran diferencia es que los comandos tienen un solo controlador, mientras que los eventos tienen uno o varios controladores.

La interfaz Command

Lo primero que necesitamos crear es la interfaz Command

type Command interface {
}

La interfaz Command no tiene definiciones de métodos, dado que por ahora un Comando es básicamente un objeto plano que contiene datos.

Pero, ¿entonces, para qué creamos la interfaz? Existe mucho valor en implementar interfaces, incluso si la interfaz no contiene ningún método dado que hace que el código sea mucho más legible y fácil de entender para otros desarrolladores.

La interfaz Handler

Ahora, es necesario definir la interfaz Handler:

type Handler interface {
 handle(command Command)
}

Cada controlador requiere implementar un método handle(), que debería aceptar un comando (Command). Realmente no nos importa cómo maneja el controlador al comando, solo nos importa decirle qué hacer.

El contenedor

El bus de comandos será responsable de crear una nueva instancia de un controlador particular para cada comando que reciba, esto significa que el bus de comandos requiere una manera de crear instancias de otros objetos.

Vamos a definir una interfaz Container que esté en nuestro control con la finalidad de poder realizar implementaciones distintas que satisfagan nuestro contrato.

type Container interface {
 make(typeName string)
}

El inflector

El bus de comandos también puede ser responsable de asignar los comandos con su respectivo controlado, sin embargo en lugar de acoplar esta responsabilidad en el bus de comandos, podemos escribir un inflector que pueda hacer el trabajo por nosotros.

type Inflector interface {
 inflect(command Command)
}

Una implementación sencilla del inflector podría ser la siguiente:

func (ni *NameInflector) Inflect(command Command) string {
 t := reflect.TypeOf(command)
 h := t.Elem().Name()
 return strings.Replace(h, "Command", "Handler", -1)
}

Implementacion del bus de comandos

Finalmente vamos a implementar el bus de comandos. Al bus de comandos se le deben inyectar instancias de una implementación de Container y otra de una implementación de Inflector, adicionalmente debe tener un método execute() que acepte un Command.

type CommandBus struct {
 Container Container
 Inflector Inflector
}
func (cb *CommandBus) handler(c Command) Handler {
 name := cb.Inflector.Inflect(c)
 return cb.Container.make(name).(Handler)
}
func (cb *CommandBus) Execute(c Command) {
 cb.handler(c).Handle(c)
}

Como se puede ver, el método execute() no devuelve ningún valor. Adicionalmente contiene un método privado para determinar qué handler utilizar y luego buscarlo en el contenedor.

Notas finales

La capa de aplicación en DDD es una parte más de nuestra arquitectura. Esta capa maneja la comunicación con el mundo exterior aceptando solicitudes y respondiendolas. Al actuar con un componente delimitador en realidad no le importa de dónde viene la solicitud o hacia dónde va.

Hay un par de enfoques frecuentemente utilizados para construir la capa de aplicación. Dos opciones populares son los Servicios de Aplicación o el uso de Comandos y Manejadores. Ambos enfoques tienen aspectos positivos y negativos, por lo que depende de la necesidad decidir cuál es el adecuado para el problema en cuestión.

Este artículo muestra una implementación simple del bus de comandos, sin embargo puede ser útil como base para robustecer una solución más específica o simplemente para entender los conceptos relacionados. 

 

Bus de Servicios

En términos simples, un bus de servicio es un mecanismo para intercambiar mensajes entre componentes. Los mensajes son DTOs (Data Transfer Object / Objectos de transferencia de datos) que contienen información relevante que nos permite interactuar sobre dicha información.

Existe un componente conocido como “emisor” cuya responsabilidad es la creación del mensaje y su entrega al bus. Por otro lado, existe un segundo componente conocido como “receptor” que le especifica al bus qué tipo de mensajes le interesa recibir.

Cuando el bus recibe un mensaje, envía el mensaje a los receptores, entonces, en realidad el bus actúa como el límite entre los distintos componentes creando desacoplamiento en nuestra solución de software. Tanto los emisores, como los receptores, desconocen la existencia de otros componentes.

Debido a este desacoplamiento, un bus de servicio puede permitir que diferentes componentes trabajen juntos de manera eficiente. Dado que el bus se encuentra en el intermedio de todos los mensajes, puede agregar funcionalidad a todos estos mensajes sin cambiarlos. Un ejemplo puede ser el registros de todos los mensajes en un sistema de logs o su encolamiento en una cola de mensajes.

Tipos de bus

En los párrafos anteriores se describió un bus de servicio de maneral general, un bus que únicamente despacha mensajes. No restringe a los mensajes o a los manejadores de mensajes de manera alguna.

Es normal que distintos tipos de mensajes requieran distinta lógica gestión, es por ello que se puede hablar de tipos de bus y hoy me interesa comentar sobre los siguientes tres:

Bus de comandos

Se caracteriza por:

  • Los mensajes (comandos) señalan la intención del usuario, por ejemplo: CrearSolicitud o RegistrarUsuario
  • Un comando es manejado por exactamente un controlador (handler)
  • Un comando no retorna valor alguno.

Bus de consultas

En ingles conocido como Query bus, se caracteriza por:

  • Los mensajes (consultas / queries) identifican una pregunta, que no está relacionada necesariamente con una consulta a base de datos (sql query). Algunos ejemplos serían: UltimasSolicitudes o ComentariosDeUnArticulo
  • Una consulta es manejada por exactamente un controlador (handler)
  • Las consultas retornan datos
  • Las consultas no deben cambiar el estado de la aplicación

Bus de eventos

Un bus de eventos se caracteriza por:

  • Los mensajes (eventos) indican que ha sucedido un evento, como por ejemplo: ArticuloCreado. UsuarioRegistrado o SolicitudFormalizada
  • Un evento puede ser manejado por cualquier numero de controladores / handlers ([0, inf])
  • Solo contiene un conjunto de valores primitivos (cadenas de texto, enteros, booleanos), no clases completas.
  • Los eventos no deben devolver valores

Consideraciones importantes

Validaciones

Los mensajes siempre deben ser válidos. Esto significa que el objeto / estructura que contiene el mensaje debe validar cada una de sus propiedades. De esta forma, solo se despachan mensajes válidos. Sin embargo, hay un límite para esto.

Por ejemplo, el comando RegistrarUsuario puede requerir (entre otras cosas) un nombre de usuario. El comando debe validar que el nombre de usuario sea una cadena de texto con una longitud entre 6 y 100 caracteres. Si el nombre de usuario es único o no, probablemente no debería ser validado por el comando y quien asumirá dicha responsabilidad es el controlador (handler)

Patrones de diseño

La implementación de comandos y consultas es parte (commands & queries) son parte del patrón CQRS. Sin embargo se puede utilizar buses de servicio sin aplicar CQRS.

Los comandos y eventos a menudo se utilizan juntos, entonces, cuando se ejecuta el comando RegistrarUsuario se activa el evento UsuarioRegistrado.

 

Complejidad esencial y complejidad adicional

Cuando desarrollamos software, podemos decir que nos enfocamos principalmente en resolver problemas y al resolver dichos problemas nos podemos encontrar con dos tipos de complejidad: la complejidad esencial y la complejidad adicional.

Complejidad esencial

Se refiere a la complejidad propia de construir una característica del software

Complejidad adicional

Se refiere a la complejidad que agregamos por nuestra cuenta mientras construimos la característica del software, complicando la resolución del problema por distintos factores.

Al inicio de la construcción de un sistema, su complejidad suele ser igual a la complejidad esencial y conforme pasa el tiempo la complejidad del sistema es el resultado de la suma de la complejidad esencial y complejidad adicional.

Una buena arquitectura de software debe pretender que el peso de la complejidad adicional no se incremente en demasía.

Como ejemplos podemos pensar imaginar:

  • una aplicación que como backend tiene un solo endpoint, su complejidad adicional será menor que una que tenga un backend con muchos endpoints acoplados
  • una organización en la cual durante un sprint se desarrollen varias reuniones poco productivas le agrega complejidad adicional al desarrollo.
  • evitar implementar un correcto sistema de despliegue en un inicio por ganar tiempo agrega complejidad adicional cuando el desarrollo involucre a muchos desarrolladores desplegando código.

Como podemos observar, la complejidad adicional no solo tiene que ver con el código que escribimos, la misma incluye todo el entorno en el cual desarrollamos el software, sin embargo en cuanto a código también podemos agregar gratuitamente complejidad adicional cuando el mismo no incluye mejores prácticas, estándares de desarrollo o el uso de clean code.

 

Qué es arquitectura de software

La arquitectura de software son las reglas autoimpuestas al definir como diseñamos software.

La arquitectura de software no incluye el tratamiento de asuntos relacionados al hardware de manera directa.

Respecto al diseño, existen los enfoques de micro-diseño y macro-diseño.

Por ejemplo, el micro-diseño hace referencia al diseño que realizamos cuando probamos el código de una función en un desarrollo dirigido por pruebas y el macro-diseño va más alla de la función que estamos implementando, tiene que ver con modelamos nuestro dominio a un nivel más alto, como dividimos nuestra aplicacion por capas, servicios, etc.

 

Cambiar puerto en politicas SELinux (SELinux policy) en CentOS – RedHat 6.4 – Fedora

Para permitir utilizar el puerto 443 (HTTPs) en el servicio sshd con SELinux habilitado, nos podemos basar en la siguiente guía

Listado de puertos utilizados por el servicio http

semanage  port -l | grep http_port_t

Eliminamos del servicio http y lo agregamos al servicio ssh

semanage port -d  -t http_port_t -p tcp 443
semanage port -m -t ssh_port_t -p tcp 443

Habilitamos el servicio SSH para que escuche por el puerto 443

vi /etc/ssh/sshd_config

Agregamos el puerto en el archivo

Port 443

Reiniciamos los servicios

/etc/init.d/sshd restart /etc/init.d/sshd status