fbpx
Publicado em: domingo, 30 de jun de 2019
Oragon – Princípios de Design – Complexidade Reside na Arquitetura

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:

  1. Se há uma abstração para o assunto, não criamos uma nova, evoluímos a anterior.
  2. Se temos novas necessidades, adaptamos a abstração existente.
  3. 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

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.