Talvez você não concorde com essa visão, já vi isso acontecer antes. Mas algo que você nunca poderá negar é a experiência alheia. E nesse caso a minha experiência década-após-década com esse tipo de abordagem vem mostrando resultados fantásticos. SOLID não é só um conjunto de regras aleatórias de qualidade, elas proporcionam real reaproveitamento, e faz parte da modelagem isolar as partes do software em elementos de forma a parecerem pequenas peças de lego, que juntas compõem coisas incríveis, mas facilmente podem ser aplicadas a outros contextos AS-IS, sem mudança alguma. Estou falando de abstrações, configurações e estratégias de modelagem que favorecem o reaproveitamento.
Contexto
Talvez você tenha ouvido falar do Oragon, do Spring.NET, do NHibernate e possivelmente mas não provavelmente do Fluent NHibernate também. Se você ouviu falar nisso tudo junto, com certeza você sabe mais ou menos ou tem alguma ideia do que possa vir a ser o Oragon.
Bom, o Oragon consiste em uma visão de arquitetura, que prevê um alto desacoplamento entre arquitetura e negócio. Em linhas gerais, se alguém precisa gerenciar complexidade, essa é a sua camada de arquitetura, ou infraestrutura, chame como preferir. Sua camada de negócio precisa ser simples, a ponto de parecer que todos os requisitos não funcionais foram ignorados. Claro que se realmente forem negligenciados você terá um problema, mas quero com essa afirmativa lembrar que:
- tratamento de exceção
- controle de transação
- controle de abertura de conexão com banco
- logging
- segurança
- e muitos outros…
Todos esses aspectos não funcionais podem ser “ignorados” (você verá no posts futuros que nenhum desses aspectos está sendo ignorado, eles só foram movidos para um lugar centralizado, em uma outra camada transversal à sua arquitetura, servindo à sua aplicação e a outras). Toda a complexidade ligada aos requisitos não funcionais precisa estar fora do código de negócio. Além disso, o design precisa favorecer o reaproveitamento. O Oragon, junto com essa visão, entrega de código que comprova que tudo isso é perfeitamente possível, viável e fica lindo!
De fato eu quero usar a analogia do Lego há muito, mas nunca consegui um exemplo concreto realmente pequeno o suficiente para não criar um textão monumental, talvez até confuso. Agora tenho um exemplo real, de produção, que pode ajudar a passar um pouco dessa filosofia. Trata-se do simples ato de obter uma connectionstring. Trivial, mas você verá que pode ficar bem interessante.
O fato
Então é hora de contar uma história. A história que dá origem a esse post:
Primeiro, uma classe de configuração do Fluent NHibernate que criei possui uma propriedade peculiar: A propriedade ConnectionStringDiscoverer do tipo IConfigurationResolver. Aqui está uma implementação dela, a NHConfigFileConnectionStringDiscoverer, que obter uma connectionstring a partir do XML de configuração do NHibernate. Embora você possa olhar a data do commit, algo que você não sabe é que essas interfaces e classes devem estar fazendo seus 5 ou até 7 anos.
Os commits nesse repositório são recentes pois estou consolidando os projetos.
Mas algo novo, que surgiu bem depois foi o .NET Core, e seu novo modelo de configuração. Enfim, appsettings.json, appsettings.Development.json e afins…
Com a demanda de colocar um projeto com .NET Core no ar, me veio a necessidade de trabalhar com novos modelos de configuração, então para suportar .NET Core eu implementei a versão abaixo:
public class JsonSettingsConfigurationResolver : IConfigurationResolver
{
public string Key { get; set; }
private IConfigurationRoot GetConfigurationRoot()
{
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true);
var config = configBuilder.Build();
return config;
}
public string GetConfiguration()
{
string returnValue = this.GetConfigurationRoot().GetValue<string>(this.Key);
return returnValue;
}
}
E se você se questionar sobre caching dessa configuração para evitar que a configuração seja obtida do file system a todo momento, eu lhe respondo: Não é papel dessa classe, caching disso é responsabilidade de uma outra implementação, que poderia ser assim:
public class ConfigurationResolverCache : IConfigurationResolver
{
public IConfigurationResolver Target { get; set; }
private string cached = null;
public string GetConfiguration()
{
if(this.cached == null)
{
this.cached = this.Target.GetConfiguration();
}
return this.cached;
}
}
E via injeção de dependência, eu configuro que quem depende dessa configuração, na verdade, depende do cache, que por sua vez depende da implementação real.
Mas talvez fosse legal ter uma estratégia de Fallback, onde caso uma configuração não exista, utilize outra estratégia. Isso também pode ser feito, porém também em outra classe, com 2 targets. Algo parecido com isso:
public class FallbackConfigurationResolver : IConfigurationResolver
{
public IConfigurationResolver FirstTarget { get; set; }
public IConfigurationResolver SecondTarget { get; set; }
public string GetConfiguration()
{
string configData = this.FirstTarget.GetConfiguration();
if(configData == null)
configData = this.SecondTarget.GetConfiguration();
return this.cached;
}
}
Essa sugestão de implementação que possui 2 targets poderia dar lugar a outra que possui uma lista de targets. E se você olhar com cuidado, verá que seja uma implementação de fallback, caching ou qualquer outra, todas estão sob a mesma interface. Isso significa que todas são plugáveis nas mesmas dependências. Vira uma questão de pura escolha e estratégia usar uma ou outra implementação. Criar novas e dar vida a novas possibilidades também é algo extremamente fácil.
Voltando aos exemplos, essa implementação de fallback poderia lidar com exceptions ao invés de tratar valor nulo, ou ao invés de validar apenas que é nulo, levar em conta se é vazio. Independente da escolha, o que está em jogo é o design, e como o desacoplamento oferece ganhos. Da mesma forma que essa connectionstring está no JSON, poderia estar em um secret do docker, que efetivamente se traduz em um arquivo texto em um lugar específico do container. Abstrair a configuração para carregar essa informação de lá, também é trivial, e compatível com o stack.
Conclusão
Esse tipo de abstração é o que proponho e desenhar 5, 10 possíveis implementações para uma mesma interface é algo que realmente me faz pensar que esta interface tem um sentido real, ela é robusta o suficiente para durar em um projeto. Esse é um modelo mais flexível e fácil de manter, permanecendo aderente a cenários que ainda sequer foram imaginados, ou você já colocou strings de conexão em arquivos plain text em um servidor qualquer?
Esse é o mindset que só pode ser obtido quando se usa injeção de dependência declarativa, abstraindo cenários onde você tem 100% das injeções automáticas. Embora possa parecer mais trabalhoso, é esse modelo quem lhe dá altos saltos em sua modelagem. Quando você para de criar managers, orquestradores desnecessários.
Quando você para de dar voltas pois só consegue desenhar 1 implementação por interface, é quanto você realmente começa a modelar de forma clean. E os ganhos com esse tipo de modelagem são absurdos, trazendo benefícios para sua aplicação, sua arquitetura base, e quaisquer outros elementos que puderem partilhar implementações para as mesmíssimas interfaces.
Embora esse texto tenha ficado maior do que eu imaginava, acredito que as abstrações apresentadas aqui são realmente simples.
É sobre essa simplicidade que eu debato, brigo, esperneio e faço birra! Pode parecer abstrato, mas lhe dá poderes inimagináveis.
0 comentários