Alguns poucos lembram, pois alguns poucos estavam lá, mas quando comecei minha carreira profissional. Uma das coisas que me projetou rápido na Petrobras foi a capacidade de identificar padrões e automatizar via abstrações e componentes a escrita de código repetitivo e burocrático.
Ainda no ASP Clássico, em 2002, já havia criado alguns componentes, seja com VBScript para o ASP, ou com JavaScript para UI.
Nunca me dei bem com trabalho repetitivo, principalmente, pois esse tipo de trabalho me faz perder a atenção fácil. E o resultado é sempre o mesmo, faltou um detalhinho aqui, outro detalhinho acolá.
Antagonicamente, minha capacidade de concentração é meu forte desde aquela época, difícil mesmo era me concentrar fazendo CRUD’s!
Quando comecei a construir o Oragon, anos depois, eu percebi que precisava endereçar assuntos de forma mais pragmática e rígida. Mas como rigidez e flexibilidade são tão antagônicos quanto necessários, levei um tempo até achar um balanceamento que trouxesse o melhor dos 2 mundos.
Vale lembrar que estamos falando de LoB apps.
A propósito o que eu chamo de arquitetura é endereçado em diversas literaturas como infraestrutura, use esse termo se preferir, faz sentido atualmente.
E assim surgiu um dogma no Oragon:
Complexidade deve residir na arquitetura.
É papel da Arquitetura absorver a complexidade técnica do projeto, permitindo que o código de negócio seja o mais simples possível.
Para obter esse objetivo, eu segui algumas regras:
Criar ou adotar abstrações
Uma abstração abstrai a complexidade de uma ou mais tecnologias.
Por exemplo, abstraindo a complexidade de configuração de ORM
builder.Services.AddNHibernate(cfg => cfg .Schema(CatalogConstants.Schema) .ConnectionStringKey("catalog") .AddMappingsFromAssemblyOf<CategoryMapping>() .RegisterSession() );
Ou como evitar que se precise criar manualmente uma classe para ser um consumidor de filas do RabbitMQ?
Como em 99.9% das vezes queremos resiliência, e todas as garantias possíveis, criar um padrão é fácil. E aqui a gente só expressa:
- Quem é o tipo que será obtido do Service Provider (DiscordSyncService)
- Qual é o tipo de mensagem para tentarmos desserializar (SyncProfileCommand)
- Como chamar o método de negócio de DiscordSyncService ( svc.SyncAsync(msg) )
Se as garantias técnicas já não justificassem, o método que recebe a mensagem, não faz a menor ideia de que foi chamado por um consumidor do RabbitMQ.
builder.Services.MapQueue<DiscordSyncService, SyncProfileCommand>(builder => builder .WithAdapter(async (svc, msg) => svc.SyncAsync(msg)) .WithQueueName("cmd-discord-sync-user") .WithPrefetchCount(1) );
Ou nesse caso abaixo, em que temos 2 abstrações:
A primeira é a abstração simplifica a publicação de mensagens com o RabbitMQ.
public static void PublishExternalAccountLinkEvent(this IBus publisher, Profile profile) => publisher.PublishEvent<ProfileChangedEvent>(it => it.WithMessage(new ProfileChangedEvent() { ProfileId = profile.ProfileId, Area = ProfileChangeArea.ExternalAccountLink }) .WithExchange("events") .WithRoutingKey("profile.changed") );
Ou abstrações que simplifiquem e abstraiam a complexidade nas configurações:
RedisConfiguration redisConfig = builder.Configuration.CreateAndConfigureWith<RedisConfiguration>(nameof(RedisConfiguration));
Nesse caso, delegamos o preenchimento das propriedades de um objeto de configuração, para a infraestrutura do asp.net. Portanto, por mais que existam variações, nas formas de preencher, todas as possibilidades estão cobertas sem necessidade de rebuild ou adaptação, tudo que o objeto permitir ser configurado, pode ser injetado via arquivo, variável de ambiente, ou qualquer um dos providers de configurações do asp.net.
Entre rigidez e flexibilidade
Você tem total liberdade para criar, estender, incrementar uma abstração, ou uma solução “mirabolante”, mas precisará fazer seguindo algumas regras:
Encapsulamento da complexidade
Encapsular a complexidade de acessar, configurar e usar frameworks, libraries e recursos que são objetivamente tecnológicos.
Uma boa abstração simplifica reduz as necessidades de configurações complexas e reduz o acoplamento.
Exponha a API Original
Se a abstração for muito específica, expor a API original ajuda em casos de uso mais específicos.
Expor a API original deve ser uma opção a ser considerada e encorajada. Sua abstração deve oferecer comportamentos adicionais, mas não pode, sob hipótese alguma ser uma barreira para o uso da API original. A API original tende a oferecer recursos novos em um ciclo de vida diferente de sua abstração, portanto possibilitar o acesso a ela é importante.
Simplifique, não dificulte
Toda abstração tem o papel de simplificar o desenvolvimento. Nisso implica automatizar passos repetitivos e complexos, e também limitar o uso de uma tecnologia ao que a arquitetura propoem-se a usar dela. É inevitável que em algum momento algum caso de uso precise tocar no componente nativo, não limite isso.
KISS – DRY – Privilegie concorrência local e monopólio global
Da mesma forma que a ideia é deixar estupidamente simples, também temos de ficar atentos para não nos repetir.
Em uma arquitetura de referência, não devemos ter mais de 1 abstração para o mesmo assunto. Não queremos abstrações concorrendo entre si. Permitir abstrações concorrentes tiram o foco da arquitetura como solução de problemas, e gradativamente se torna um espaço de ego para cada um criar sua abstração para o mesmo assunto.
É natural que o interesse na construção de abstrações aumente, visto que se bem projetadas, o código de negócio se torna ridiculamente simples.
Algumas regras precisam ser seguidas para isso:
- Se há uma abstração para o assunto, não criamos uma nova, evoluímos a anterior.
- Se temos novas necessidades, adaptamos a abstração existente.
- Se a proposta é de reconstrução, por considerar a versão atual falha, há de se considerar algumas coisas:
- A nova abstração deve substituir 100% a abstração anterior.
- Seu escopo ganha 100% dos requisitos da versão anterior.
- Faz parte do escopo dessa nova abstração refatorar todas as dependências.
- Ao final dessa implementação a versão anterior não pode ser mais usada, e a nova versão asume seu lugar.
Nenhum desses passos é passível de ser ignorado.
Na manutenção de abstrações temos 3 tipos de tarefas:
- Nova Abstração / Reescrita do zero de uma abstração
- Adição de cenários e funcionalidades a uma abstração antiga
Assim temos o seguinte conjunto de regras:
- Se estamos diante de um problema novo, não resolvido pelas abstrações e mecanismos existentes, essa nova abstração ou mecanismo precisa:
- Ser genérico.
- Ser desacoplado das regras de seu negócio.
- Precisa estar em um projeto que NÃO POSSUA, dependência (nem direta, nem indireta) com o código de negócio.
- Precisa poder ser referenciada por teus projetos de negócio (e não importa se você chama de serviço, domínio, não importa qual arquitetura ou estratégia de modelagem seja a utilizada para o core do teu projeto).
- É criado embarcada na solution do projeto, como uma abstração local.
- Embora só atenda seu projeto, ela objetivamente está nascendo com o propósito de virar library, independente. Só não tem maturidade, na primeira implementação para ter seu uso disseminado indiscriminadamente.
- Se estamos falando da substituição (reescrita) de um mecanismo, ou abstração, então temos outro conjunto de regras:
- Seu desenvolvimento deve seguir as mesmas regras para uma abstração nova.
- A nova abstração deve no mínimo atender a todos os requisitos da versão anterior (exceto quando requisitos/premissas estão errados).
- É responsabilidade de quem está desenvolvendo a nova abstração, alterar 100% do código/configuração que usava a abstração anterior.
- Não é possível conviver com 2 versões distintas que façam a mesma tarefa.
Assim desafios tecnológicos devem ser gerenciados por abstrações reaproveitáveis.
O que isso quer dizer?
Complexidade precisa ser gerenciada, ter um método de negócio que nele faça referência para diversas tecnologias diretamente aumenta a complexidade e dificulta a manutenção. O reaproveitamento e testes passam a ser dificultados e se tem ao longo do tempo uma colcha de retalhos, difícil de manter, difícil de gerenciar e com potencial para ser duplicada a qualquer momento. E isso nada tem a ver com monolitos ou microsserviços, estamos falando puramente de um código que faça uma tarefa.
Essas regras são desenhadas para inibir aventureiros e incentivar quem tem consciência de que pode fazer algo melhor. Por outro lado, empurra quem quer reinventar a roda na direção de refatorar um mecanismo, pois o esforço tende a ser menor.
Liberdade com responsabilidade
Você é livre para usar as tecnologias que bem entender e que forem necessárias. No entanto para fazer isso, você não faz de qualquer jeito, nem em qualquer lugar, nem deixa lixo pelo caminho. Seu código de negócio precisa continuar clean e sua arquitetura precisa continuar sendo coesa.
Esse conjunto de regras força na direção da abstração e do desacoplamento. Mas também te empurra para desenhar soluções de fato reaproveitáveis.
Seu projeto não é um playground
Embora toda abstração seja uma simplificação, toda abstração ganha um propósito adicional de ser reaproveitável. É preciso inibir síndromes como NIH (Not invented here). Esse modelo desencoraja o uso irracional de uma tecnologia da moda. E fica evidente por regras e gestão de histórico que algumas coisas podem ou não pode ser realizadas.
Genérico vs Específico, BDUF vs EDUF vs NDUF
Precisamos pontuar o que são esses acrônimos:
NDUF – No Design Up Front
EDUF – Enough Design Up Front
BDUF – Big Design Up Front
Em nome do EDUF, evitando BDUF, acaba-se realizando o NDUF.
Esse é um ponto polêmico: O que é Big? O que é Enough? Qual é o limiar entre eles? Muita gente confunde especificidade com acoplamento na hora de usar esses conceitos para construir seja lá o que for.
Sob a armadilha de achar que qualquer mínimo design é BDUF, se pratica NDUF indiscriminadamente. Da mesma forma que se confunde especificidade com acoplamento:
Especificidade tem a ver com a resolução objetiva de uma questão, já acoplamento tem a ver com o nível de interdependência.
Devemos nos atentar para a possibilidade de atender outras possibilidades, mas apenas para o design. Isso quer dizer que está tudo bem se você só implementou um cenário de uso, desde que tenha vislumbrado novos cenários e deixados pontos para abstrações futuras. Aplicar SOLID faz com que você, automaticamente, cumpra esse papel.
Minha dica é que o mínimo de design deva ser o uso de SOLID. Acredito que o design ideal de mecanismos arquiteturais, deva levar em conta a possibilidade desse mecanismo ser usado por outros. Pra isso é necessário pensar em cada componente ou classe como um elemento singular, fora do contexto de negócio, com um propósito próprio, com uma responsabilidade própria e assim entender como esse mecanismo pode atender outros consumidores futuramente. Quais os pontos de abstração são necessários? Como servir de base para novas implementações? Onde ser opinativo e onde não ser?
Aqui acredito que caiba um exemplo: Tenho para apreciação a interface IConfigurationResolver. A simplicidade dessa interface é o ponto central para novas implementações futuras. Hoje só temos a implementação de StaticConfigurationResolver, no entanto abre portas para implementações usando Secrets do Docker ou Vault’s dos mais variados vendors ou implementações como Consul ou Zookeeper ou Etcd. O poder de soluções simples mas genéricas é a possibilidade de compor implementações, reaproveitar para os mais variados fins. No github é possível ver onde essa interface é usada inclusive uma implementação feita com base em arquivos de configuração do NHibernate.
Não devemos inventar rodas quadradas
Ao criar um mecanismo é necessário endereçar problemas de forma a garantir que não esteja reinventando a roda, principalmente se essa roda for quadrada, ou seja: pior ou incompleta em relação ao que já existe. Novamente Not Invented Here é um problema que queremos endereçar aqui.
Aguçando o senso de propósito e suprimindo o ímpeto revolucionário
Esse sem sombra de dúvidas é o elemento mais controverso e polêmico, mas o que melhor expressa e separa crianças de adultos.
Muitas das soluções que encontramos em clientes não passam de PoC’s que foram para produção, ou testes que viraram implementações definitivas. A falta de gestão de código tem imenso potencial destrutivo. Há casos em que tecnologias são utilizadas sem o menor cabimento ou necessidade, ou uso equivocado de tecnologias empenhadas no papel errado, há casos em que o roadmap pessoal de estudos do desenvolvedor lidera a implementação de mecanismos e abstrações duplicadas ou desnecessárias, ou até a adoção de tecnologias que não fazem sentido naquele contexto.
Essa abordagem que proponho, principalmente no que diz respeito à reimplementação, prevê que o “legado” seja revisitado e refatorado para usar a nova solução, força 2 coisas relevantes:
- Compreensão dos requisitos atendidos pela versão anterior do mecanismo/componente/abstração.
- Compreensão das features da versão anterior do mecanismo/componente/abstração.
Em casos de reconstrução (reescrever uma abstração/mecanismo), espera-se que a nova versão substitua completamente a versão anterior, eliminando inclusive o código da versão anterior. Para isso todos os consumidores precisam ser migrados deixando de consumir a versão anterior para consumir a nova versão.
Caso a versão anterior continue sendo necessária, é sinal de que a nova implementação ainda não está acabada e a tarefa não está concluída.
Pois substituir a implementação anterior é um dos requisitos da tarefa.
Na medida que compreendemos melhor e conhecemos o esforço de criar algo novo, ponderamos sobre esse esforço de criação de algo novo, em comparação à customização da versão anterior. Afinal, é possível e talvez até provável que com pouco redesign se alcance o objetivo com a implementação anterior. E é esse um tipo de refactoring que precisa ser encorajado.
Quem está convicto de que tem condições de fazer melhor o faz por puro e simples senso de propósito. Pratique o desapego, não importa quem tenha feito a melhor versão, o que importa é que temos uma versão melhor. Esse tipo de refactoring precisa ser encorajado.
A construção de um ativo de arquitetura
Essa relação com seus mecanismos é uma relação sempre positiva: Ou você está criando algo novo ou está melhorando/adaptando o que estava pronto para atender a novos cenários. Se estiver reimplementando algo, sua substituição tende a ser no mínimo melhor. O resultado dessas decisões é a produção de um ativo de arquitetura, algo com potencial para crescer e possivelmente se tornar uma solução compartilhada entre projetos.
Pensando grande, começando pequeno
Mesmo vislumbrando que essa solução, para atender a um problema comum, poderá ser promovida a library compartilhada entre projetos, seu desenvolvimento acontece no âmbito local, começa dentro da solution e repositório git do projeto que primeiro demandou aquilo, resolvendo um problema por vez, começando pequeno. Com o tempo e maturidade nesse design ou após diversas intervenções, e enfim alcançando a maturidade e estabilidade, é possível cogitar sua promoção para uso geral.
A promoção de uma implementação local para uma implementação global segue outro conjunto de regras que precisam ser discutidos em algum momento.
Benefícios
Afinal, quais são os benefícios dessa abordagem?
- Redução monumental do acoplamento.
- Centralização lógica das abstrações (Abstrações estão na mesma camada).
- Descentralização física das abstrações (Abstrações estão em projetos independentes e reaproveitáveis).
- Começam no mesmo projeto
- Quando promovidas, ganham status de projeto
- Independência e capacidade de reaproveitamento de abstrações
- Maior coesão
- Gestão de código mais eficiente, consequentemente alterações são mais efetivas, embora mais complexas.
- Redução de complexidade acidental causada pela adição não estruturada e planejada de tecnologias.
- Facilidade de gestão de
Conclusão
Esse tipo de abordagem evita que se crie abstrações específicas demais para o negócio, com baixo reaproveitamento, e nenhuma coesão.
Algo que vou falar mais detalhadamente quando abordar Abstrações, é que elas são promovidas de abstrações do projeto para abstrações globais, e viram projetos autônomos na medida que ganham maturidade.
0 comentários