Já faz muito tempo que gosto de modelar os elementos de uma arquitetura como componentes reaproveitáveis, em geral abstrações que me proporcionem a qualquer consumidor simplesmente usá-la, e para isso há algum esforço na modelagem para abstrair aspectos que são peculiares às suas demandas de negócio.
Em conjunto com um bom Container de IoC/DI, é possível fazer coisas incríveis. E muito do que reclamo da abordagem com Register/Resolver se dá pela impossibilidade de modelar direito e de realmente promover reaproveitamento, quando sua única informação para obter uma dependência é seu tipo. Para a configuração do container, é preciso dar alguma pista de que você quer uma instância configurada do jeito A ou do jeito B, para que possa existir 2 jeitos diferentes de configurar uma dependência. É a inversão de controle quem determinará onde usar cada uma das versões da mesma classe. Reside aí o real poder de reaproveitamento e a simplicidade de uso quando falamos de Inversão de Controle e Injeção de Dependência.
Exemplos:
Indo direto ao ponto, e tentando ser o menos abstrato possível na explicação vejamos 2 exemplos, Email e Parser. Nestes exemplos vou dar alguns detalhes para tornar palpável a compreensão.
Se eu preciso enviar emails, meu primeiro instinto é modelar um serviço com um único método e suas sobrecargas para enviar um email para um destinatário. Quem (a classe) precisa enviar um email (geralmente um requisitante de negócio) sequer precisa saber que há um servidor SMTP, que há configurações dessa natureza, é da infraestrutura de envio de email a responsabilidade por saber isso. Assim eu reduzo complexidade e facilito o reaproveitamento.
public interface IEmailService { void SendMessage(string subject, MailAddress[] To, MailAddress[] Cc, MailAddress[] Bcc, string message); }
Já a implementação teria todas as informações, como SMTP Server, Configurações de Autenticação, enfim, tudo o que for necessário para conectar nos principais serviços do mercado. Sim, eu bedufo.
Parser
Não é incomum que o envio de email se desdobre na necessidade de criar templates de email, então para isso você precisa realizar o parse de uma estrutura de dados, usando uma template engine e posteriormente resultando em um HTML, o que é mais provável. Assim transpor um objeto em um template produzindo o body da mensagem a ser enviada. Bom, quem faz o parser é um outro elemento, destinado a parser, também configurável e com uma interface também irritantemente simples. Seria algo como:
public interface IParser { string Parse(string templateName, objet rootMessage); }
Para que um templateName? Estou cogitando haver um repositório de templates, ok?!
Ambos são simples, um não depende do outro, mas se fizer sentido juntar ambos, aí nasce um terceiro elemento que orquestra as chamadas a ambos. Poderia ser uma composição ou mesmo uma herança sobre a infraestrutura de email. Sendo uma herança, cada classe da pilha de hereditariedade precisaria ter seu sentido bem definido. Trocando em miúdos, ambas as classes deveriam fazer sentido sozinhas, dependendo do contexto e necessidade.
Infraestrutura Global vs Infraestrutura Local
Uma das práticas que me ajudou muito a modelar melhor é separar o que é infraestrutura do que é domínio. Essa decisão te dá o poder de modelar de forma agnóstica. O envio de email só envia email, não faz update em tabela alguma, não chama serviço de notificação, no máximo possui um evento que precisa ser assinado. Quem quer que tenha de fazer tais tarefas adicionais, trata o evento e fará seu trabalho.
Ainda assim, eu gosto de tratar 2 tipos de Infraestrutura, nos meus projetos eu chamo de projetos de arquitetura (****.Architecture).
Infraestrutura global
Infraestrutura global é aquele arcabouço que você sabe que poderá usar em todos ou quase todos os projetos. No meu caso eu trago comigo toda a parte de AOP para dar suporte a transações, exception handling, contextos de acesso a dados (uma forma excelente de resolver leaks e interferências na modelagem). Tudo que sei que precisarei em 100% dos projetos (ou quase isso 😉 ). Esses são elementos menos mutáveis, já que diversos projetos dependem deles, e possuem um nível de maturidade alto.
Infraestrutura local
Também sobre o sufixo Architecture, uso em geral um projeto class library dentro da solution do sistema em questão para criar as abstrações que são específicas para esse projeto. E um ponto é extremamente interessante. Esse é o playground que me permite criar novas soluções que no futuro podem ser promovidas a implementações globais. É assim que mesmo com um modelo parcialmente rígido, o time não perde flexibilidade para inovar e criar abstrações na arquitetura. Aliás…
Lugar de Complexidade é na Arquitetura
Sua arquitetura deve abstrair a complexidade tecnológica afim de prover uma infraestrutura que suporte sua aplicação de forma simples. Se seu código de domínio está complexo, então você não fez o dever de casa na arquitetura. Levando em conta que estamos falando de uma aplicação de linha de negócio, LOB Apps, seu domínio não pode realizar tarefas tecnicamente complexas de forma direta. Seu domínio precisa ser o mais simples possível e se não está simples, você pecou em não criar boas abstrações para essa complexidade.
Domínios orquestram e delegam, e assim, a aparência de um método de domínio que siga essa linha de raciocínio, é invariavelmente simples, não obstante, com uma boa arquitetura, ele pode parecer ao olhar desatento, até omisso (por não esboçar tratamento de exceção, e outros desvios de fluxo desnecessários ali pois alguém o faz de forma global e consistente).
Modelagem
Gosto de encarar cada elemento como autônomo e agnóstico. E aqui ainda estou falando da infraestrutura/arquitetura. Gosto de olhar para uma implementação na infraestrutura e ver que ela não diz respeito ao negócio, mas a uma tarefa técnica, pertinente ao negócio, mas completamente desconectada enquanto solução. Como disse ela é autônoma.
Já no domínio acabo por não usar tantas abstrações e em geral modelo um domínio muito mais flat. Mais flat, significa mais simplista, minimalista ao máximo. São poucas heranças pouco polimorfismo, enfim, simples. Não é uma regra, não crio limites, apenas não tenho feito nada diferente (há exceções, mas não convém citar para não confundir ainda mais). Essa é uma escolha controversa e polêmica, e ainda não tive resultados negativos com os mais variados cenários de negócio de forma a esbarrar em necessidades que enderecem outro perfil de modelagem, como DDD, por exemplo.
A propósito, a infraestrutura de automação com AOP funciona perfeitamente com domínios ricos.
Acesso a Dados
Eu sou um fã de full featured ORM’s, como NHibernate, por exemplo. Trago NHibernate comigo há pelo menos 12 anos, como meu ORM default. A primeira recomendação do NHibernate fiz em 2005 em um estudo direcionado à escolha do ORM que iria compor, mais tarde, o framework corporativo .NET da Petrobras, o FCORP. (NH não foi aceito na época, resolveram implementar um, anos mais tarde jogaram fora e adotaram o NHibernate, enfim isso é assunto para mesa de bar).
De qualquer forma, quando modelo minhas abstrações, o acesso ao NHiberrnate é abstraído, mas não por completo. Isso significa que tenho facilitadores que se você quiser, consegue trabalhar sem saber que existe NHibernate na arquitetura, no entanto, se você estiver sob um repositório/dataprocess, então você consegue sem esforço algum ter acesso ao contexto, à session e objetos internos de gestão do NHibernate. A propósito, independente de CQRS, mesmo com ORM em operações procedimentais, eu separo escrita de leitura. Há somente 1 persistidor para todas as entidades que operam em um determinado banco. Eu também suporto múltiplos bancos/tecnologias side-by-side. É relativamente comum achar cenários em que você precisa de 2 tecnologias diferentes, como MySQL e SQL Server, já houve cenários com até 4 bancos.
Em relação à separação da escrita e leitura a questão é muito simples. Eu não preciso criar formas diferentes de escrita, mas com certeza preciso lidar com regras específicas de consulta, e em geral há regras de acesso a dados que não quero proliferar, por isso preciso especializar meus repositórios de consulta, diferente do repositório de escrita, este eu simplesmente consumo.
Outro ponto interessante é que para esses repositórios, a arquitetura global provê essa funcionalidade, mas extensões são criadas na arquitetura local, via herança, para dar flexibilidade e tomar a decisão, no projeto, do que quero expor ou não de forma pública, ou com nomes em inglês, português, nomes de métodos que possam fazer mais sentido de acordo com a cultura da empresa ou do projeto em que estou refatorando. Na prática, algumas decisões são irrelevantes, ou precisam ser irrelevantes para a arquitetura global e sequer gosto de entrar nessas brigas. Sigo aplicando Open-Closed principle não só para não engessar o projeto, mas para dar flexibilidade e evitar algumas discussões que ao meu ver são pouco produtivas.
Conectando os pontos
Por fim, como unir isso tudo: IoC + DI. Uso Spring .NET já há mais de 10 anos, e recentemente criei um fork do projeto para portá-lo para .NET Standard. E sinceramente, sinto muita falta de soluções que façam o que ele faz. A flexibilidade de adicionar comportamentos, proxies AOP e outros recursos o fazem, na minha opinião, o melhor IoC/DI container do mercado. E tenho muito a falar sobre o projeto, são muitos recursos úteis que facilitam o dia-a-dia. Com algumas automações que criei, fica ainda mais fácil torná-lo compatível com ASP.NET Core. É incrível ver o que conseguimos fazer com ele.
Conclusão
Todos temos predileções quanto a modelagem e arquitetura, essas são as dicas que levo comigo e por enquanto trago para o meu dia-a-dia de projeto. Gosto desse tipo de abordagem e gosto dessa forma de modelar. Embora possa parecer abstrata demais ao olhar de quem usa esse tipo de arquitetura, ela oferece imensa flexibilidade, extensibilidade e permite criar soluções incríveis e altamente reaproveitáveis.
0 comentários