Como funciona un bus de mensajes

por Fran Iglesias

Hace un tiempo que se me ocurrió la idea de explicar algunos componentes relativamente comunes de las aplicaciones construyéndolos. Y me apetecía hacerlo con los buses de mensajes.

Creo que la inspiración lejana de esto viene de una serie de artículos de Fabien Potencier, el creador de Symfony, sobre cómo construir tu propio framework. No pretendo llegar a tanto, pero es verdad que es una forma genial de entender cómo funcionan las cosas.

Hablemos de patrones

El patrón Command

Para empezar hay que hablar del patrón Command.

Este patrón resuelve el problema de separar el momento de constatar la necesidad de realizar una operación determinada, del momento o el lugar en que se ejecuta dicha operación.

Es decir, tenemos un módulo emisor (invoker o sender) interesado en que ocurra algo, para lo cual encapsula la petición en forma de un objeto comando (command). Este comando se pasa a un receptor (receiver) que ejecuta el comando cuando sea oportuno. El emisor o invocador no sabe quién, ni cómo, va a ejecutar la acción, y el receptor no sabe quién se lo ha pedido.

El patrón command es muy usado para ejecutar acciones solicitadas desde la interfaz de usuario. Un controlador recibe un evento o petición del mundo real (un clic en un botón, seleccionar un item en un menú, etc.), encapsula esa petición en un comando y se lo pasa a un receptor que lo ejecuta o lo encola.

En general, es un patrón que nos sirve para decidir en una capa, contexto o módulo de software, algo que se deberá ejecutar en otro lugar del código, sin acoplarlos.

Voy a intentar explicarlo con la creación de un reloj despertador que nos saluda por la mañana y nos desea buenas noches al acostarnos. Para saber cómo saludar, vamos a usar una “librería” que nos proporcione expresiones adecuadas en un idioma determinado. De momento, solo español.

Aquí las librerías.

class Spanish
  def morning
    puts "Buenos días"
  end

  def night
    puts "Buenas noches"
  end
end

Aquí tenemos AlarmClock. Se configura con la hora de levantarse y la de acostarse. En nuestro ejemplo simularemos un día recorriendo las 24 horas, mostrando el mensaje adecuado a los momentos especiales de levantarse y acostarse.

AlarmClock tiene un AlarmClockDisplay que es el que genera el output en la pantalla. AlarmaClock decide qué comando pasarle a AlarmClockDisplay para que este muestra el mensaje correcto en función del momento del día. Además, si no se trata de un momento especial, se envía un comando para mostrar la hora.

AlarmClock sería el invoker, que es quien decide qué comando se debe ejecutar en función del contexto. AlarmClockDisplay es el receiver, que ejecuta el comando solicitado porque es quien sabe como ejecutarlo. En este caso, mostrando la información en la consola.

class AlarmClock
  def initialize(awake_at, sleep_at)
    @display = AlarmClockDisplay.new
    @awake_at = awake_at
    @sleep_at = sleep_at
  end

  def run
    24.times do |hour|
      command = case hour
                when @sleep_at
                  NightCommand.new(Spanish.new, hour)
                when @awake_at
                  MorningCommand.new(Spanish.new, hour)
                else
                  ShowTimeCommand.new(hour)
                end
      @display.show(command)
    end
  end
end

Podríamos decir que AlarmClock está pendiente de tres eventos: que sea la hora de levantarse, que sea la hora de acostarse y que sea cualquier otra hora del día. Cuando suceden estos eventos, le envía a AlarmClockDisplay el mensaje adecuado.

Este por su parte se limita a ejecutar el comando que le pasan el comando configurado para cada uno de los mensajes

class AlarmClockDisplay
  def show(command)
    command.execute
  end
end

Los comandos implementan una interfaz Command que tiene un método execute. Gracias a esto, AlarmClockDisplay sabe que solo tiene que enviar el mensaje execute a los objetos que le pasen.

class Command
  def execute()
    raise NotImplementedError.new
  end
end

Y aquí están todos los comandos que definen nuestro sistema. Es interesante señalar que cada uno de los comandos recibe por construcción la información o dependencias que necesita para poder ejecutarse. En este ejemplo, la dependencia vendría representada por los idiomas.

class MorningCommand < Command
  def initialize(language, time)
    @language = language
    @time = time
  end

  def execute
    print "#{@time}:00 -> "
    @language.morning
  end
end

class NightCommand < Command
  def initialize(language, time)
    @language = language
    @time = time
  end

  def execute
    print "#{@time}:00 -> "
    @language.night
  end
end

class ShowTimeCommand < Command
  def initialize(time)
    @time = time
  end

  def execute
    puts "#{@time}:00"
  end
end

Otra cosa interesante en la que fijarse es que cada comando es muy simple, no tiene que tomar decisiones basadas en la hora del día, lo que hace que sean fáciles de testear o de reemplazar. Esas decisiones se han tomado antes, cuando se decide qué comando crear.

Y aquí tendríamos un ejemplo de funcionamiento.

clock = AlarmClock.new(7, 22)
clock.run

Al ejecutarlo se genera este resultado:

0: 
1: 
2: 
3: 
4: 
5: 
6: 
7: Buenos días
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: Buenas noches
23: 

Si queremos dar soporte a nuevos eventos, no tenemos más que añadir los comandos necesarios y modificar el bloque case para que nos los proporcione.

En resumen, en el patrón comando, el objeto comando representa una intención (algo que se quiere hacer) y contiene todo lo necesario para poder hacerlo.

Separando los datos

Si examinamos alguno de los comandos anterior podemos observar algunos detalles interesantes:

class NightCommand < Command
  def initialize(language, time)
    @language = language
    @time = time
  end

  def execute
    print "#{@time}:00 -> "
    @language.night
  end
end

Fijémonos en los parámetros de construcción:

language representa una dependencia, un colaborador en el que el comando delegará parte de la ejecución. De hecho, esto es similar a lo que haríamos con un patrón Adapter. Como tal dependencia, podríamos inyectarla en tiempo de configuración del sistema. Además, no necesitamos una instancia específica, sino que nos vale con la misma instancia para todos los comandos u otros objetos que la puedan necesitas.

time, por su parte, representa un parámetro que el comando necesita para ejecutarse. No podemos conocerlo hasta el momento en que se genera, por lo que tiene que inyectarse en tiempo de ejecución. Se trata, además, de una instancia específica que, en este caso, se ha creado como respuesta a un evento.

Esto nos lleva a pensar en la posibilidad de separar el comando en dos partes:

  • la parte que expresa la intención: el propio comando conteniendo los datos necesarios.
  • la parte que la implementa: el ejecutor del comando, que implementa la intención y usa sus datos. Es frecuente llamarlo también handler.

En esta implementación del patrón el comando es un DTO, un objeto de transporte de datos. Esto es ventajoso porque, como señalamos más arriba, el patrón comando suele implicar trabajo en varias capas (como capa de UI y capa de dominio, por ejemplo). Un DTO es un tipo de objeto ideal para mover información entre capas o sistemas, no require nada especial para su creación ni tiene dependencias. Además, según los lenguajes, nos permite implementarlos mediante tipos de datos como Structs, que simplifican su instanciación y su uso.

Por otro lado, el ejecutor o handler, vive únicamente en la capa donde se realiza el comportamiento y se ocupa de los detalles de implementación.

Esto se puede ver más fácilmente en este ejemplo en el que vamos a reescribir el comando anterior:

Aquí el comando. Como se puede ver, el comando ya no implementa una interfaz. Es un DTO. En Ruby lo podemos implementar como un objeto que expone accesores a sus propiedades.

class NightCommand
  attr_reader :time

  def initialize(time)
    @time = time
  end
end

En este caso es ventajoso utilizar structs, ya que nos proporciona acceso público a las propiedades y soporta la igualdad. En Ruby, al igual que en Java, cada objeto tiene identidad, lo que resulta bastante incómodo a la hora de hacer comparaciones.

NightCommand = Struct.new(:time)

Por su parte, aquí está el ejecutor,

class NightCommandExecutor
  def initialize(language)
    @language = language
  end

  def execute(command)
    print "#{command.time}:00 -> "
    @langugage.sleep
  end
end

Esta separación convierte al comando en un mensaje de tipo imperativo. El invoker solo tiene que instanciar el mensaje deseado y pasarlo al receiver, que se deberá ocupar de encontrar el ejecutor adecuado para cada comando.

Si bien esto simplifica las cosas en el lado del invoker, las complica un poco en el lado del receiver. Vamos a ver cómo cambian. Para el invoker es algo más fácil decidir e instanciar los comandos, puesto que solo necesitan los datos que ya tiene.

class AlarmClock
  def initialize(awake_at, sleep_at)
    @display = AlarmClockDisplay.new
    @awake_at = awake_at
    @sleep_at = sleep_at
  end

  def run
    24.times do |hour|
      command = case hour
                when @sleep_at
                  NightCommand.new(hour)
                when @awake_at
                  MorningCommand.new(hour)
                else
                  ShowTimeCommand.new(hour)
                end
      @display.show(command)
    end
  end
end

AlarmClockDisplay necesita algo que le permita obtener los ejecutores o handlers que correspondan a los comandos.

class AlarmClockDisplay
  def show(command)
    executor = case command.class.name
               when "MorningCommand"
                 MorningHandler.new(Spanish.new)
               when "NightCommand"
                 NightHandler.new(Spanish.new)
               else
                 ShowTimeHandler.new
               end
    executor.execute(command)
  end
end

Finalmente, aquí tenemos los comandos y sus ejecutores. Lo más interesante es ver que ahora cada ejecutor recibe un comando con el mensaje execute. Lógicamente, sería conveniente verificar que el comando pasado es del tipo adecuado.

MorningCommand = Struct.new(:time)

class MorningHandler
  def initialize(language)
    @language = language
  end

  def execute(command)
    raise ArgumentError.new "invalid command" if !command.is_a? MorningCommand

    print "#{command.time}:00 -> "
    @language.morning
  end
end

NightCommand = Struct.new(:time)

class NightHandler
  def initialize(language)
    @language = language
  end

  def execute(command)
    raise ArgumentError.new "invalid command" if !command.is_a? NightCommand

    print "#{command.time}:00 -> "
    @language.night
  end
end

ShowTimeCommand = Struct.new(:time)

class ShowTimeHandler
  def initialize() end

  def execute(command)
    raise ArgumentError.new "invalid command" if !command.is_a? ShowTimeCommand

    puts "#{command.time}:00"
  end
end

Esta implementación es un poco más sofisticada y, al menos aparentemente, complica un poco las cosas, pero no demasiado.

El nuevo diseño aporta algunas ventajas que se manifiestan mejor en proyectos más grandes. A pesar de que tenemos más objetos, cada uno tiene sus responsabilidades más acotadas. Y, en ese sentido, el cambio más interesante es que los comandos ahora se han convertido en mensajes.

Bueno, todo ha mejorado, menos en el receiver

Resolver

Hay un tema un poco molesto aquí. El receiver ahora tiene que averiguar cuál es el ejecutor del comando, a fin de enviárselo.

class AlarmClockDisplay
  def show(command)
    executor = case command.class.name
               when "MorningCommand"
                 MorningHandler.new(Spanish.new)
               when "NightCommand"
                 NightHandler.new(Spanish.new)
               else
                 ShowTimeHandler.new
               end
    executor.execute(command)
  end
end

Podemos separarlo en otra clase, que llamaremos Resolver

class AlarmClockDisplay
  def initialize
    @resolver = Resolver.new
  end

  def show(command)
    executor = @resolver.executor_for(command)
    executor.execute(command)
  end
end

class Resolver
  def executor_for(command)
    case command.class.name
    when 'MorningCommand'
      MorningHandler.new(Spanish.new)
    when 'NightCommand'
      NightHandler.new(Spanish.new)
    else
      ShowTimeHandler.new
    end
  end
end

Ahora todo está un poco mejor, ¿no? Por supuesto, se podría objetar que no estoy inyectando algunas dependencias, lo que puede a la larga puede traer algunas complicaciones. Así que dejemos las cosas bien.

class AlarmClockDisplay
  def initialize(resolver)
    @resolver = resolver
  end

  def show(command)
    executor = @resolver.executor_for(command)
    executor.execute(command)
  end
end
class AlarmClock
  def initialize(display, awake_at, sleep_at)
    @display = display
    @awake_at = awake_at
    @sleep_at = sleep_at
  end

  def run
    24.times do |hour|
      command = case hour
                when @sleep_at
                  NightCommand.new(hour)
                when @awake_at
                  MorningCommand.new(hour)
                else
                  ShowTimeCommand.new(hour)
                end
      @display.show(command)
    end
  end
end

En consecuencia, para montar y ejecutar nuestra aplicación, el código sería este:

resolver = Resolver.new
display = AlarmClockDisplay.new(resolver)
clock = AlarmClock.new(display,7, 22)

clock.run

Como decíamos al principio, el patrón Command nos permite separar la decisión de qué necesitamos hacer, del momento y lugar en que se hace. Esto se acentúa al separar la parte de datos y la parte de ejecución.

Los invokers tan solo tienen que saber instanciar los comandos que necesiten, sin preocuparse de cómo se implementan los ejecutores.

En realidad casi hemos llegado sin querer al Command Bus. Fíjate qué ocurre con AlarmClockDisplay: recibe un comando, localiza el ejecutor del mismo y se lo pasa.

Pues eso es lo que hace un Command Bus.

Command Bus

El Command Bus es básicamente un receiver universal que acepta comandos y se los pasa a los ejecutores adecuados.

Una comparación bastante buena es pensar en una camarera o camarero de un buen restaurante. Recibe peticiones de los clientes y su misión es llevarlas a los distintos servicios del restaurante:

  • La comanda de platos a la cocina
  • Bebidas a la bodega
  • Cafés, licores, cócteles…, al bar
  • La cuenta y el pago a la caja

La analogía hay que tomarla con pinzas porque la realidad del mundo de la hostelería no suele estar tan compartimentada, pero puede resultar bastante útil.

Al Command Bus como tal le da igual el significado o implementación de los comandos que gestiona. Sencillamente, lo único que necesita saber es a qué ejecutor se lo debe pasar.

Para conseguirlo, el Command Bus necesita alguna estrategia de mapeo o resolución. La más básica es algo como lo que acabamos de ver: una asociación explícita entre cada comando y su ejecutor.

Debería ser obvio, sin embargo, es que esta es una mala estrategia, ya que tendríamos que cambiar el mapeador cada vez que queramos añadir un comando al sistema, lo que sería una violación del principio Open/Closed.

Pero no nos adelantemos. Lo mejor es verlo en acción. Sin embargo, nuestro diseño actual del AlarmClock no es bueno para esto. Así que vamos a plantearlo de otra manera.

Nuestro reloj despertador tendrá dos componentes: la pantalla y un generador de sonido. La pantalla puede mostrar cualquier texto que le pasemos, mientras que el generador de sonido puede producir una señal horaria o una alarma.

Al igual que en los ejemplos anteriores, invocaremos ciertos comandos para ejecutar ciertas acciones según la hora del día:

  • A la hora de levantarse, se mostrará un saludo y sonará una alarma.
  • A la hora de acostarse, se mostrará una despedida.
  • A cada hora se emitirá una pequeña señal horaria.

En general, el diseño del sistema va a tener bastantes diferencias respecto a lo que hemos visto en los otros ejemplos. No estoy muy seguro de por donde empezar, pero como el artículo va sobre Command Buses, creo que puedo empezar por aquí. Muy simple:

class CommandBus
  def initialize(resolver)
    @resolver = resolver
  end

  def execute(command)
    executor = @resolver.executor_for(command)
    executor.execute(command)
  end
end

En pocas palabras: el CommandBus obtiene una instancia del ejecutor o handler de un comando y se lo pasa invocando execute. Eso est todo.

Se podría decir que la chicha está en el Resolver, un objeto al que le pasamos el objeto comando y nos dice quien debería ejecutarlo.

class Resolver
  def executor_for(command)
    case command.class.name
    when 'GoodMorningCommand'
      GoodMorningHandler.new(Display.new, SpanishLanguage.new(Spanish.new))
    when 'GoodNightCommand'
      GoodNightHandler.new(Display.new, SpanishLanguage.new(Spanish.new))
    when 'PlayAlarmCommand'
      PlayAlarmHandler.new(Sound.new)
    when 'PlayBeepCommand'
      PlayBeepHandler.new(Sound.new)
    when 'ShowTimeCommand'
      ShowTimeHandler.new(Display.new)
    else
      raise ArgumentError.new, "unknown command #{command.class.name}"
    end
  end
end

Es también muy simple: dependiendo del nombre del comando, crea una instancia del ejecutor. Cada ejecutor se construye con todas las dependencias que necesite para su trabajo.

Por supuesto, me diréis que no es necesario crear una instancia nueva de cada dependencia cada vez:


class Resolver
  def executor_for(command)
    display = Display.new
    sound = Sound.new
    
    spanish = Spanish.new
    spanish_language = SpanishLanguage.new(spanish)

    case command.class.name
    when 'GoodMorningCommand'
      GoodMorningHandler.new(display, spanish_language)
    when 'GoodNightCommand'
      GoodNightHandler.new(display, spanish_language)
    when 'PlayAlarmCommand'
      PlayAlarmHandler.new(sound)
    when 'PlayBeepCommand'
      PlayBeepHandler.new(sound)
    when 'ShowTimeCommand'
      ShowTimeHandler.new(display)
    else
      raise ArgumentError.new, "unknown command #{command.class.name}"
    end
  end
end

Es más, me diréis que en realidad, deberíamos poder instanciar cada ejecutor fuera de Resolver, de tal modo que no tenga la responsabilidad de construir esos objetos.

Y, aún más, como mencionamos más arriba, lo suyo sería que esta asociación se pudiese hacer desde fuera, tal vez mediante algún tipo de registro que nos permita asociar un comando con la instancia del handler que lo va a manejar. ¿Qué tal algo así?

class Resolver
  def initialize
    @executors = {}
  end

  def register(command, executor)
    @executors[command] = executor
  end

  def executor_for(command)
    @executors[command.class.name]
  end
end

¿Dónde se han ido todos? Pues a la configuración:

display = Display.new
sound = Sound.new

spanish = Spanish.new
spanish_language = SpanishLanguage.new(spanish)

resolver = Resolver.new
resolver.register('GoodMorningCommand', GoodMorningHandler.new(display, spanish_language))
resolver.register('GoodNightCommand', GoodNightHandler.new(display, spanish_language))
resolver.register('PlayAlarmCommand', PlayAlarmHandler.new(sound))
resolver.register('PlayBeepCommand', PlayBeepHandler.new(sound))
resolver.register('ShowTimeCommand', ShowTimeHandler.new(display))

command_bus = CommandBus.new(resolver)

clock = AlarmClock.new(command_bus, 7, 22)
clock.run

¿Ha molado? Pues mola bastante, porque ahora vamos a gozar de una flexibilidad tremenda para construir nuestro reloj. Todo el código anterior constituye la base de la arquitectura de nuestro reloj.

Lo único que aún no os he mostrado son los comandos y sus ejecutores. Los pongo aquí y luego volvemos con las explicaciones.

GoodMorningCommand = Struct.new(:time)

class GoodMorningHandler
  def initialize(display, language)
    @display = display
    @language = language
  end

  def execute(command)
    raise ArgumentError, 'invalid command' unless command.is_a? GoodMorningCommand

    print "#{command.time}:00 -> "
    @display.show(@language.good_morning)
  end
end

GoodNightCommand = Struct.new(:time)

class GoodNightHandler
  def initialize(display, language)
    @display = display
    @language = language
  end

  def execute(command)
    raise ArgumentError, 'invalid command' unless command.is_a? GoodNightCommand

    print "#{command.time}:00 -> "
    @display.show(@language.good_night)
  end
end

ShowTimeCommand = Struct.new(:time)

class ShowTimeHandler
  def initialize(display)
    @display = display
  end

  def execute(command)
    raise ArgumentError, 'invalid command' unless command.is_a? ShowTimeCommand

    @display.show("#{command.time}:00")
  end
end

PlayAlarmCommand = Struct.new(:time)

class PlayAlarmHandler
  def initialize(sound)
    @sound = sound
  end

  def execute(command)
    raise ArgumentError, 'invalid command' unless command.is_a? PlayAlarmCommand

    @sound.alarm
  end
end

PlayBeepCommand = Struct.new(:time)

class PlayBeepHandler
  def initialize(sound)
    @sound = sound
  end

  def execute(command)
    raise ArgumentError, 'invalid command' unless command.is_a? PlayBeepCommand

    @sound.beep
  end
end

Aquí están los servicios que nos abstraen del hardware.

class Display
  def show(message)
    puts message
  end
end
class Sound
  def alarm
    puts '      Playing... Sounding Alarm!!!'
  end

  def beep
    puts '      Playing... Beep! Beep!'
  end
end

Y aquí tenemos una librería que actúa como proveedora de los textos necesarios. Hemos introducido el concepto Language para tener una interfaz propia que define cómo queremos interactuar con estos proveedores. Y para nuestro ejemplo, implementamos un adaptador para tener un SpanishLanguage.

class Spanish
  def morning
    'Buenos días'
  end

  def night
    'Buenas noches'
  end
end

class Language
  def good_morning
    raise NotImplementedError.new, 'implement good_morning'
  end

  def good_night
    raise NotImplementedError.new, 'implement good_night'
  end
end

class SpanishLanguage < Language
  def initialize(spanish)
    @spanish = spanish
  end

  def good_morning
    return @spanish.morning
  end

  def good_night
    @spanish.night
  end
end

Este es el output generado:

0:00
      Playing... Beep! Beep!
1:00
      Playing... Beep! Beep!
2:00
      Playing... Beep! Beep!
3:00
      Playing... Beep! Beep!
4:00
      Playing... Beep! Beep!
5:00
      Playing... Beep! Beep!
6:00
      Playing... Beep! Beep!
7:00 -> Buenos días
      Playing... Sounding Alarm!!!
8:00
      Playing... Beep! Beep!
9:00
      Playing... Beep! Beep!
10:00
      Playing... Beep! Beep!
11:00
      Playing... Beep! Beep!
12:00
      Playing... Beep! Beep!
13:00
      Playing... Beep! Beep!
14:00
      Playing... Beep! Beep!
15:00
      Playing... Beep! Beep!
16:00
      Playing... Beep! Beep!
17:00
      Playing... Beep! Beep!
18:00
      Playing... Beep! Beep!
19:00
      Playing... Beep! Beep!
20:00
      Playing... Beep! Beep!
21:00
      Playing... Beep! Beep!
22:00 -> Buenas noches
23:00
      Playing... Beep! Beep!

Reflexiones

Como se puede ver, la mayor parte de la lógica de la aplicación está en los pares comando/ejecutor.

AlarmClock básicamente recorre las horas del día y según sea una de las configuradas o no, envía el comando correspondiente a través del CommandBus. Si queremos que pasen varias cosas a la misma hora, no tenemos más que enviar los nuevos comandos.

AlarmClock no sabe nada acerca de qué componentes del sistema se encargan de qué comandos. Esto nos permitirá cambiar su comportamiento sin tocar su código, simplemente cambiando los ejecutores por otros configurados de manera distinta. Gracias al Command Bus podemos hacer estos cambios respetando el principio Open/Close… o casi.

Nota al margen: Aquí anticipo que hay otras formas de resolver esto, que veremos en otro artículo, porque una cosa lleva a la otra. Centrémonos en el CommandBus.

El CommandBus, por su parte, tampoco sabe nada de nadie. No tiene ni idea de quién le envía comandos ni quién los ejecuta. Se limita a recibirlos y preguntar al Resolver si conoce algún ejecutor que lo pueda manejar, para pasárselo.

Resolver, finalmente, registra una asociación arbitraria de nombres de comandos con una instancia de un ejecutor que lo va a poder manejar. El código del resolver tampoco sabe nada de esta asociación, puesto que se la indicamos nosotras. Se limita a registrarla y usarla cuando se le pide.

Por tanto, CommandBus y Resolver son completamente genéricos, nos valen para montar cualquier aplicación.

En cuanto a los comandos, estos expresan intenciones de las usuarias del sistema: cosas que queremos que pasen. Cada una de estas intenciones se parametriza con los datos necesarios.

Los ejecutores son como aplicaciones pequeñísimas que usan elementos del sistema para cumplimentar la intención. Para ello, se configuran con las dependencias necesarias. En una aplicación grande, esos ejecutores coordinarán objetos del dominio del sistema. En aplicaciones más pequeñas puede que simplemente contengan el código necesario para realizar esa acción.

El hecho de que se trate de acciones muy específicas debería servir para que sean bastante fáciles de poner bajo test.

A donde vamos a continuación

Para no alargar más esta primera entrega y darnos tiempo a asentar los conceptos básicos, vamos a dejarlo aquí.

Como puedes ver, la idea de usar un Command Bus puede ser una gran solución para estructurar una aplicación de forma sencilla. Pero podemos llegar más lejos.

En las próximas entregas vamos a ver varios temas. Principalmente, puedo anticipar los siguientes:

  • Cómo podemos cambiar el comportamiento de la aplicación simplemente configurando otros ejecutores.
  • Cómo podemos aprovecharnos del Command Bus para añadir toda una serie de servicios comunes y modificar su comportamiento usando Middlewares, que son añadidos que podemos configurar en el Bus y pueden acceder a los comandos y sus resultados.

November 12, 2023

Etiquetas: design-patterns  

Temas

good-practices

refactoring

php

testing

tdd

design-patterns

python

blogtober19

design-principles

tb-list

misc

bdd

legacy

golang

dungeon

ruby

tools

hexagonal

tips

ddd

books

bbdd

software-design

soft-skills

pulpoCon

oop

javascript

api

sql

ethics

agile

typescript

swift

java