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);
}
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);
Gerar arquivos:
await ProcessarLoteAsync(ids, GerarArquivosAsync);
Enviar para o ERP:
await ProcessarLoteAsync(ids, IntegrarComErpAsync);
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;
}
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>
Mas é muito mais simples do que parece.
Vamos ler de trás para frente.
Task
Retorna uma operação assíncrona.
Depois:
IEnumerable<int>
Recebe uma coleção de inteiros.
Juntando tudo:
Func<IEnumerable<int>, Task>
É 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)
{
...
}
Depois:
await ProcessarLoteAsync(ids, EnviarEmailsAsync);
await ProcessarLoteAsync(ids, GerarArquivosAsync);
await ProcessarLoteAsync(ids, IntegrarComErpAsync);
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);
}
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);
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>
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);
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);
}
À 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));
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)