Experimentos com Command Pattern em Rust

Eu tenho feito alguns experimentos implementando o command pattern em Rust e encontrei pelo menos duas formas de implementar.

Mas Primeiro... Por que?

Existe uma coisa que eu estou tentando fazer em que o command pattern se encaixa perfeitamente: Eu quero ter uma biblioteca com todas as ações do sistema e implementar uma interface em cima disso, sendo que pode ser uma CLI ou uma interface web ou uma interface qualquer. Para isso, a lógica por trás da ação deve estar de alguma forma isolada da origem da chamada.

O Que É

O command pattern é descrito como ter um objeto para cada ação (porque, basicamente, os patterns são mais focados em projetos orientados a objetos) e cada um destes tem um método chamado execute que... bem... executa o comando.

A Solução Enum

Como o que você têm é uma lista de ações, uma das ideias foi usar Enum, mesmo que isso não seja exatamente o que pattern descreve.

Digamos que nós temos duas ações que podem ser chamadas: Depositar dinheiro e sacar dinheiro. Simples.

Assim, podemos ter o seguinte Enum1:

enum Command {
    Depositar(Decimal),
    Sacar(Decimal),
}

Como Rust permite que as variantes de um Enum carreguem um valor com elas, o valor a ser depositado ou sacado fica anexado junto com a variante.

E então você tem a função execute(). E, de novo, porque Rust permite que sejam adicionadas funções em basicamente tudo, o que eu fiz foi adicionar um método diretamente no Enum:

impl Command {
    fn execute(&self) -> Result<...> {
        match self {
            Depositar(valor) => faz_o_deposito(valor),
            Sacar(valor) => sacar_dinheiro(valor),
        }
    }
}

E assim por diante.

Para usar, eu coloquei algo parecido com isso na minha camada de interface:

let valor = requisicao_externa.valor();
let comando = match requisicao_externa.comando() {
    "depositar" => Command::Depositar(valor),
    "sacar" => Command::Sacar(valor),
}
comando.execute();

Tudo fica simples e tal, mas existe uma tendência a deixar uma bagunça com a quantidade de conteúdo que fica dentro ou ao redor do impl, na minha opinião. Mas, ao mesmo tempo, a camada de dispatch (que fica entre a camada de serviço/enum e a camada de interface) é bem básica.

Uma solução para para a quantidade de "conteúdo dentro ou ao redor do impl" seria o uso de múltiplos impl: Ter um módulo deposito.rs que faz o impl de faz_o_deposito e outro módulo saque.rs que também faz o impl dentro do enum com o conteúdo de sacar_dinheiro. Mas eu ainda precisaria centrar todas as operações no execute para ter um dispatch correto.

A Solução com Traits

A solução com trait é bem parecida com o que o pattern diz: Você cria uma trait (interface) e "impl" em todos os comandos, que são structs. Por exemplo:

trait Command {
    fn execute(&self) -> Result<...>;
}
struct Depositar(Decimal);
impl Command for Depositar {
    fn execute(&self) -> Result <...> {
        // o que era o `faz_o_deposito` vai aqui.
    }
}

struct Sacar(Decimal);
impl Command for Sacar {
    fn execute(&self) -> Result <...> {
        // o que era o `sacar_dinheiro` vai aqui.
    }
}

... o que parece um pouco mais limpo, já que todas as coisas relacionadas com Deposito ou Saque estão juntas agora.

Entretanto, isso causa um pequeno problema com a camada de interface: Agora ela não pode mais retorna algo com tamanho fixo: É necessário usar um conteúdo com dispatch dinâmico, como Box<dyn Command>, o que não é tão direto quando um Enum/Struct/conteúdo com tamanho.

Por outro lado, como Box implementa Deref, uma vez que a interface retorne algo-que-implementa-Command, basta chamada execute() diretamente nele.

let comando = interface_que_retorna_um_comando_num_box_dyn();
comando.execute();

Onde Eu Vejo Esses Dois

Eu consigo ver o uso do Enum em arquiteturas simples, com apenas um domínio. Como toas as coisas são relacionadas, elas podem viver tranquilamente dentro do Enum.

Mas quando estamos lidando com múltiplos domínios, a solução de trait/dispatch dinâmico parece fazer mais sentido: Coisas relacionadas nos seus próprios módulos, nos seus próprios espaços e a ideia de misturar os mesmos (por exemplo, se você tiver um domínio de tags e um domínio de dinheiro, e quer colocar tags nas operações de dinheiro) ficaria na camada acima deles.


1

Decimal não faz parte da biblioteca padrão do Rust, mas pode ser usada a partir da crate rust_decimal.