DEV Community

10 Dicas Basicas de Arquitetura de Código : 6 - Strategy Pattern

Fala galera,

tudo beleza?

Dando continuidade a serie de artigos sobre arquitetura (se não viu a parte 5 clique aqui), quando nossa aplicação cresce é comum encontrarmos métodos cheios de if, else e switch para decidir qual comportamento deve ser executado.

Lembre-se, ou você morre herói ou vive o bastante para ver você mesmo virar vilão : 🦇 🦇

Strategy Pattern sem perceber: usando Func no .NET

Uma curiosidade de quem a quase 20 anos pega a porquisse alheia, digo, código legado.

Eu estava refatorando um Worker que integra com um enorme ERP (um abraço pra Anitta) e faz muitas funções e queria reutilizar toda a lógica de processamento de um lote, mas mudando apenas o que seria feito com aqueles registros.

Minha primeira ideia foi criar uma interface, algumas implementações e seguir o Strategy Pattern clássico.

Depois eu parei e pensei:

"Cara... eu só preciso receber uma função."

No fim, a solução ficou assim:

private async Task ProcessarLoteAsync(
    IEnumerable<int> ids,
    Func<IEnumerable<int>, Task> executar)
{
    if (!ids.Any())
        return;

    await executar(ids);
}
Enter fullscreen mode Exit fullscreen mode

Entendeu? Praticamente eu estava usando Strategy Pattern, só que sem criar nenhuma interface.


Ok Zueiras a parte, vamos a pratica, O que esse método faz?

Repare que ele não sabe:

  • Se vai enviar e-mail;
  • Se vai integrar com um ERP;
  • Se vai gerar arquivos;
  • Se vai publicar mensagens.

Ele sabe apenas uma coisa:

Existe alguém que sabe processar essa lista.

E isso é exatamente a ideia do Strategy Pattern.


Mudando o comportamento em tempo de execução

Agora imagine essas chamadas.

Enviar e-mails:

await ProcessarLoteAsync(ids, EnviarEmailsAsync);
Enter fullscreen mode Exit fullscreen mode

Gerar arquivos:

await ProcessarLoteAsync(ids, GerarArquivosAsync);
Enter fullscreen mode Exit fullscreen mode

Enviar para o ERP:

await ProcessarLoteAsync(ids, IntegrarComErpAsync);
Enter fullscreen mode Exit fullscreen mode

O método nunca mudou.

Quem muda é somente a estratégia.


Sem Strategy Pattern

Normalmente vemos algo parecido com isso:

switch (tipo)
{
    case TipoProcessamento.Email:
        await EnviarEmailsAsync(ids);
        break;

    case TipoProcessamento.Arquivo:
        await GerarArquivosAsync(ids);
        break;

    case TipoProcessamento.Erp:
        await IntegrarComErpAsync(ids);
        break;
}
Enter fullscreen mode Exit fullscreen mode

Funciona.

Mas toda vez que surgir um novo processamento você precisa alterar esse método.

Com Strategy, basta criar uma nova função.


Mas o que é Func ?

Pode parecer estranho a forma que é feito:

Func<IEnumerable<int>, Task>
Enter fullscreen mode Exit fullscreen mode

Mas é muito mais simples do que parece.

Vamos ler de trás para frente.

Task
Enter fullscreen mode Exit fullscreen mode

Retorna uma operação assíncrona.

Depois:

IEnumerable<int>
Enter fullscreen mode Exit fullscreen mode

Recebe uma coleção de inteiros.

Juntando tudo:

Func<IEnumerable<int>, Task>
Enter fullscreen mode Exit fullscreen mode

É simplesmente:

Uma função que recebe uma lista de inteiros e executa alguma operação assíncrona.

Só isso.


Qualquer método compatível funciona

Se a assinatura for igual, você pode passar o método diretamente.

private Task EnviarEmailsAsync(IEnumerable<int> ids)
{
    ...
}

private Task GerarArquivosAsync(IEnumerable<int> ids)
{
    ...
}

private Task IntegrarComErpAsync(IEnumerable<int> ids)
{
    ...
}
Enter fullscreen mode Exit fullscreen mode

Depois:

await ProcessarLoteAsync(ids, EnviarEmailsAsync);

await ProcessarLoteAsync(ids, GerarArquivosAsync);

await ProcessarLoteAsync(ids, IntegrarComErpAsync);
Enter fullscreen mode Exit fullscreen mode

Sem if.

Sem switch.

Sem dezenas de classes.


Interface ou Func?

Se você chegou aqui e entendeu o que eu expliquei acima, deve ta com o dedo coçando pra perguntar:

"Então nunca mais devo criar interfaces?"

Claro que não.

Uma interface continua sendo excelente quando sua estratégia possui:

  • dependências;
  • regras complexas;
  • vários métodos;
  • estado interno;
  • identidade dentro do domínio.

Exemplo:

public interface IProcessador
{
    Task ProcessarAsync(IEnumerable<int> ids);
}
Enter fullscreen mode Exit fullscreen mode

Já para comportamentos pequenos, um Func costuma deixar o código muito mais limpo.


Você já usa Strategy Pattern todos os dias

Olha esse código:

var ativos = usuarios.Where(u => u.Ativo);
Enter fullscreen mode Exit fullscreen mode

O método Where sabe qual filtro você quer?

Não.

Quem fornece o filtro é você.

Na prática ele recebe algo parecido com:

Func<Usuario, bool>
Enter fullscreen mode Exit fullscreen mode

Ou seja...

Você entrega a estratégia de filtragem.

O LINQ inteiro faz isso o tempo todo.


Outra vantagem

Como você está recebendo uma função, fica muito fácil criar métodos reutilizáveis.

await ProcessarLoteAsync(ids, AtualizarBancoAsync);

await ProcessarLoteAsync(ids, PublicarFilaAsync);

await ProcessarLoteAsync(ids, InvalidarCacheAsync);
Enter fullscreen mode Exit fullscreen mode

Toda validação continua centralizada.

Só muda o comportamento.


Quando eu usaria Func ?

Eu gosto bastante dessa abordagem quando:

  • existe apenas uma operação;
  • a estratégia é pequena;
  • quero evitar criar várias classes;
  • estou montando utilitários;
  • quero reutilizar fluxo de processamento.

Se a lógica começar a crescer, aí sim faz sentido migrar para interfaces.


Nem tudo é vantagem

Apesar de eu gostar bastante dessa abordagem, existe um cuidado importante.

Receber uma função como parâmetro é extremamente poderoso.

E justamente por isso também é muito fácil exagerar.

Um exemplo clássico acontece com repositórios utilizando Entity Framework.

Imagine uma interface como esta:

public interface IUsuarioRepository
{
    Task<IEnumerable<Usuario>> BuscarAsync(
        Expression<Func<Usuario, bool>> filtro);
}
Enter fullscreen mode Exit fullscreen mode

À primeira vista parece uma ótima ideia.

Afinal, qualquer filtro pode ser enviado para o repositório.

Mas aí começa o problema.

Em vez da camada de acesso a dados decidir como consultar o banco, quem passa a definir esse comportamento é a camada de negócio.

await repository.BuscaAsync(usuario =>
    usuario.Ativo &&
    usuario.Pedidos.Any(p => p.Total > 1000));
Enter fullscreen mode Exit fullscreen mode

Repare que a regra de acesso aos dados deixou de estar encapsulada no repositório.

Agora qualquer camada pode construir consultas arbitrárias.

Na prática, a responsabilidade começou a "vazar" entre as camadas.

O repositório virou praticamente um executor de consultas.

O problema não é o Func

É importante deixar claro que o problema não é utilizar Func, Action ou Expression.

O problema é utilizá-los para expor comportamentos que deveriam permanecer encapsulados.

Quando fazemos isso, acabamos quebrando um dos principais objetivos da arquitetura em camadas: cada camada deve ser responsável apenas pelas decisões que lhe pertencem.

Em vez de o repositório decidir como acessar os dados, ele passa a executar qualquer regra enviada por outra camada.

Esse tipo de acoplamento costuma aparecer aos poucos e, quando percebemos, regras de acesso a dados, integração ou infraestrutura já estão espalhadas pela aplicação.

Antes de expor uma função como parâmetro, costumo fazer uma pergunta simples:

Essa decisão realmente pertence a quem está chamando?

Se a resposta for não, provavelmente essa lógica deveria continuar encapsulada na própria camada.

O Strategy Pattern continua sendo uma ótima solução

O objetivo do Strategy Pattern é separar comportamentos para reduzir acoplamento e facilitar a evolução do código.

Mas isso não significa transformar qualquer método em um callback genérico.

Flexibilidade é excelente.

Flexibilidade sem limites pode acabar criando exatamente o problema que estamos tentando evitar: responsabilidades espalhadas pela aplicação.

Então...

Muita gente associa Strategy Pattern a dezenas de interfaces e implementações.

Hoje, principalmente com os recursos modernos do C#, muitas vezes um simples Func resolve o problema de forma muito mais elegante.

O importante não é usar interface ou delegate.

O importante é separar aquilo que muda daquilo que permanece igual.

Naquele Worker que eu estava refatorando, a única coisa que mudava era o processamento do lote.

Então, em vez de perguntar "qual processamento devo executar?", o método passou simplesmente a receber a resposta como parâmetro.

Na prática, isso também é Strategy Pattern.

E, na minha opinião, é uma das formas mais elegantes de aplicar esse padrão no .NET.

Quer saber mais de Strategy Pattern ? Clica aqui

Espero ter ajudado!

Aquele abraço!

Top comments (0)