Implementación de CQRS en Arquitecturas Limpias con C#

Clase 17 de 24Curso de Arquitecturas Limpias para Desarrollo de Software

Resumen

¿Qué es CQRS y cómo puede mejorar tu arquitectura de software?

En el mundo del desarrollo de software, siempre buscamos herramientas y técnicas que nos ayuden a manejar de manera más eficiente nuestros sistemas y aplicaciones. Aquí es donde entra CQRS (Command Query Responsibility Segregation), una técnica que puede cambiar por completo la manera en que manejamos nuestras operaciones de lectura y escritura dentro de una aplicación.

¿Cómo funciona CQRS?

CQRS, o Segregación de Responsabilidades de Comandos y Consultas, se basa en la idea de separar las operaciones que alteran el estado del sistema de aquellas que simplemente consultan datos. Esto se traduce en dos componentes principales:

  1. Comandos: Operaciones de escritura que cambian el estado de algún objeto o sistema. Por ejemplo, guardar un usuario o modificar un atributo de una base de datos.

  2. Consultas: Operaciones de lectura que se encargan de recuperar información, como "dame todos los usuarios registrados en los últimos 30 días" o "obtén la fecha de la última venta".

Al separar estos componentes, logramos una estructura más eficiente y sencilla de mantener, especialmente en aplicaciones que necesitan escalar.

¿Por qué elegir CQRS?

Hay diversas razones para adoptar CQRS:

  • Escalabilidad: Algunas aplicaciones pueden requerir una administración más intensiva de las operaciones de lectura o escritura. Separando estas operaciones, cada componente puede escalar independientemente según las necesidades del sistema.

  • Mantenimiento: La separación de responsabilidades generalmente lleva a un código más limpio y comprensible, facilitando el mantenimiento de la aplicación a largo plazo.

  • Desempeño: Permite optimizar componentes específicos de acuerdo a su función principal (leer o escribir), mejorando el rendimiento general.

Ejemplo de implementación en C#

CQRS se podría implementar en C# utilizando una estructura de proyecto predefinida que enfatice las arquitecturas limpias. Por ejemplo:

public class CreateTodoListCommand : IRequest
{
    public string Title { get; set; }
}

// Handler para el comando CreateTodoList
public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, Result>
{
    private readonly TodoDbContext _context;

    public CreateTodoListCommandHandler(TodoDbContext context) 
    {
        _context = context;
    }

    public async Task<Result> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
    {
        var todoList = new TodoList { Title = request.Title };
        _context.TodoLists.Add(todoList);
        await _context.SaveChangesAsync(cancellationToken);

        return Result.Success(todoList.Id);
    }
}

En este ejemplo, el comando CreateTodoListCommand está diseñado específicamente para manejar la operación de creación de una lista de tareas, utilizando Entity Framework para interactuar con la base de datos.

¿Qué sucede con las consultas?

Las consultas se manejan de manera similar, pero se enfocan únicamente en recuperar datos:

public class GetTodosQuery : IRequest<IList<TodoList>>
{
}

public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, IList<TodoList>>
{
    private readonly TodoDbContext _context;

    public GetTodosQueryHandler(TodoDbContext context) 
    {
        _context = context;
    }

    public async Task<IList<TodoList>> Handle(GetTodosQuery request, CancellationToken cancellationToken)
    {
        return await _context.TodoLists.ToListAsync(cancellationToken);
    }
}

Este patrón asegura que el código esté limpio y que cada clase tenga una sola responsabilidad, mejorando la mantenibilidad y legibilidad del código.

¿Cómo consumir los comandos y consultas?

En una arquitectura que adopta CQRS, los controladores (en una API, por ejemplo) no interactúan directamente con las consultas o comandos. En lugar de eso, utilizan un mediador que maneja los eventos y consultas:

[HttpGet]
public async Task<ActionResult<IList<TodoList>>> GetTodos()
{
    var query = new GetTodosQuery();
    var result = await _mediator.Send(query);
    return Ok(result);
}

El uso de un framework como Mediator permite lanzar eventos para manejar consultas y comandos, promoviendo un desacoplamiento significativo entre las capas de la aplicación.

¿Dónde más se puede aplicar CQRS?

Además de operaciones dentro del código, CQRS se puede expandir a:

  • Métodos: Evitando mezclar operaciones de lectura/escritura en un único método.
  • Clases: Separando las clases en función de si gestionan comandos o consultas.
  • Servicios y backends: Implementando microservicios o backend dedicados para operaciones de lectura y otro para operaciones de escritura.

En escenarios más avanzados, incluso se puede aplicar CQRS a nivel de bases de datos, separando bases de datos dedicadas a operaciones de lectura y escritura, lo cual sería beneficioso en entornos de alta concurrencia.

Implementar CQRS es un paso importante hacia una arquitectura de software más robusta y escalable. Con un poco de práctica, verás cómo esta técnica puede transformar tu forma de desarrollar aplicaciones. ¡Anímate a investigar más sobre este tema y sigue aprendiendo!