Arquitetura Multidimensional 

Mar/21
8

Em post anteriores vimos como se caracteriza uma arquitetura plana a partir das arquiteturas Clean, Onion e Hexagonal. O objetivo de qualquer arquitetura plana é isolar o domínio do resto da aplicação e esta, por sua vez, de qualquer dependências tecnológica de bibliotecas e frameworks.

Chegamos num diagrama como este:

Arquitetura Plana
Diagrama base de todas as arquiteturas planas

As camadas verde amarela n√£o dependem de tecnologias e frameworks.

Vimos depois os detalhes das classes dentro de cada uma dessas camadas. Várias famílias de objetos têm lugares específicos dentro das camadas e ajudam a organizar as responsabilidades do código.

Agora vamos falar um pouco de como isto é só a ponta do iceberg e temos que pensar que ela se enquadra num modelo de andares como este:

Diagrama de andares em que a arquitetura plana resolve o primeiro andar

Por enquanto, não sabemos o que existe nos andares de baixo, mas sabemos que o primeiro andar isola a lógica de domínio e a lógica de aplicação de qualquer interferência tecnológica.

Descobrindo o que est√° por baixo

Na prática existem certas classes auxiliares que gostaríamos de compartilhar tanto entre diferentes sistemas, como entre diferentes camadas. Vimos o exemplo disso na classe Converter. Tanto a camada de entrega como a de aplicação precisavam de conversores para manter as camadas isoladas. Estes conversores implementam uma lógica recursiva em que os campos de um objeto são convertidos para os campos de outro objeto. Seria vantajoso se pudéssemos isolar essa lógica recursiva do propósito do conversor. Ou seja, não importa o quê estejamos convertendo, o algoritmo do processo de conversão seria sempre o mesmo.

Al√©m de conversores podemos precisar de classes utilit√°rias que operam sobre tipos fundamentais como String ou BigDecimal e que s√£o necess√°rios √† constru√ß√£o dos objetos de dom√≠nio ou de aplica√ß√£o. Bibliotecas utilit√°rias como uma API de Datas e Horas ou uma que trabalhe com quantidades monet√°rias pode ser bem √ļtil, dependendo da complexidade do dom√≠nio. Estas API t√™m uma reaproveitabilidade maior que as classes de dom√≠nio e de aplica√ß√£o.

Por fim, se estamos usando Domain Driven Design (DDD) na nossa camada de dom√≠nio, √© bem poss√≠vel que esse dom√≠nio esteja dividido em contextos. Os contextos precisam – muitas vezes – comunicar entre si. Claro que n√£o podemos usar classes de dom√≠nio para isso, ent√£o teremos que usar algum tipo de objeto desacoplado para transportar os dados entre os contextos do dom√≠nio. Por exemplo, como payload de um objeto de evento.O pr√≥prio objeto de evento √© comum a v√°rios contextos. Este conjunto de objetos de suporte ao dom√≠nio formam, segundo o DDD, o Shared Kernel (o n√ļcleo compartilhado).

Na face da necessidade de todas estas outras classes, podemos então criar uma camada a mais onde colocar estas classes e partilhá-las com as camadas que precisam usá-las. O problema, é que, se criarmos essa camada junto das camadas que já temos vamos acabar violando as regras de dependência que construimos.

A solução, é então, pensar que estas novas camadas vivem em um outro, novo, andar:

O que nos leva ao seguinte diagrama:

Todas as camadas do andar de cima podem depender de camadas no andar de baixo. O inverso n√£o √© permitido. J√° vimos que o uso de interfaces pode facilitar bastante a manter esta regra de depend√™ncia. Isto √© especialmente √ļtil para frameworks, como o de convers√£o. Interfaces e classes abstratas definidas no andar de baixo podem ser facilmente implementadas ou estendidas no andar de cima.

Dependências são de cima para baixo

As camadas azuis dos andar de cima s√£o as √ļnicas que podem depender de frameworks e bibliotecas e conter anota√ß√Ķes, por exemplo. Agora podemos ver onde poderiam estar estes frameworks, bibliotecas e anota√ß√Ķes: em um andar inferior.

Neste andar inferior podemos ter qualquer tipo de biblioteca e framework, como Spring, Hibernate, Joda Time, Joda Money, etc.. √Č prudente aplicar aqui os mesmos princ√≠pios que aplicamos no andar de cima e isolar ao m√°ximo estas bibliotecas e frameworks. Imagine um sistema que usava Joda Time em todas as classes de modelo e dom√≠nio. Obviamente isto simplificava muito a manipula√ß√£o de datas e c√°lculos de tempo. Mas com o advento da biblioteca padr√£o de Date and Time API no Java 8, n√£o h√° mais porque depender do Joda Time. √Č aqui que o Shared Kernel pode ajudar bastante. Modelando objetos pr√≥prios do Shared Kernel para as finalidades de representar datas e tempos e as opera√ß√Ķes necess√°rias ao dom√≠nio encapsulando o uso da verdadeira biblioteca, tornaria f√°cil remover a depend√™ncia do Joda Time e usar apenas a Date and Time API do Java 8. Isto reduz nossas depend√™ncias sem termos que modificar ou recompilar o andar de cima. Nem sempre isto √© poss√≠vel. Por exemplo, √© muito dif√≠cil – para n√£o dizer quase imposs√≠vel – remover as depend√™ncias do Spring ou de qualquer outro framework que usemos para dar vida aos nossos controladores REST. Mas esses casos s√£o exce√ß√£o. A maior parte das API, e especialmente as que fazem parte do Shared Kernel e classes utilit√°rias √© sempre poss√≠vel esconder a verdadeira API. Isto nos leva a um refinamento no nosso segundo andar:

Repare que a Infraestrutura fica no centro, mantendo assim as regras de dependência que descobrimos e usamos no primeiro andar. As camadas azuis do primeiro andar podem depender desta camada de infraestrutura, assim como as outras camadas do segundo andar. Quanto mais esta infraestrutura isolar a verdadeira tecnologia como API de terceiros ou até as próprias API do SDK da linguagem, melhor. Isto torna o primeiro andar mais resiliente a mudanças tecnológicas ao longo do tempo.

O quanto mais a Infraestrutura isola as verdadeiras bibliotecas e frameworks mais longeva é a solução.

Sustentação

Todas as camadas e andares que vimos at√© agora assentam em uma base comum. Esta base sustenta todo o conjunto da edifica√ß√£o que andares que estamos construindo. Esta base √© formada pela linguagem de programa√ß√£o e sua respectiva biblioteca padr√£o. A biblioteca padr√£o oferece certo tipos j√° implementados – como String, Integer ou Double – que formam os blocos padr√£o para construir os outros blocos. Muitas linguagens oferecem um pouco mais, como bibliotecas de cole√ß√Ķes, bibliotecas para criptografia ou bibliotecas para acessar bancos de dados.

A linguagem em conjunto com a biblioteca padr√£o s√£o a depend√™ncia √ļltima dos andares superiores. Contudo, muitas vezes precisamos de certos m√©todos que usamos recorrentemente, mas que n√£o existem na biblioteca padr√£o. Nestas circunst√Ęncias criamos nossas pr√≥prias bibliotecas ou recorremos a bibliotecas de terceiros como as bibliotecas da Apache, como o Apache Commons. Em linguagens que suportam extens√Ķes, como C#, e muito comum criarmos v√°rias extens√Ķes para facilitar o trabalho e dar mais flu√™ncia ao c√≥digo. Em linguagens que n√£o suportam extens√Ķes √© comum vermos o uso de m√©todos est√°ticos para resolver certo casos de uso mais frequentes.

Seja que tipo de extensão você precisa ou se a implementa você mesmo ou usa uma de terceiros, o fato é que todas elas são escritas com base em uma certa linguagem. Isto nos leva a um diagrama como este:

Andar de linguagem

Mantemos as mesmas regras de dependência de fora para dentro como nos outros andares. Atualizando o nosso modelo temos algo como isto :

Diagrama de m√ļltiplos andares e camadas

As setas lembram a direção das dependências: de dentro para fora e de cima para baixo.

Olhando para o todo podemos ver como existem v√°rias diferentes dimens√Ķes que nos ajudam a manter regras de depend√™ncia saud√°veis, que por sua vez nos ajudam a manter o c√≥digo organizado e a aumentar a vida √ļtil do software. A linguagem nos d√° uma base onde come√ßar a construir o software. Podemos ter que estender a linguagem com mais bibliotecas dependendo do que precisamos e do que j√° vem inclu√≠do na biblioteca padr√£o. No andar mais de cima temos todas as l√≥gicas de aplica√ß√£o e neg√≥cio que s√£o o c√©rebro e objetivo final do software: o software faz algo. No andar do meio temos um conjunto de classes que s√£o mais direcionadas ao software que as classes b√°sicas da linguagem, e que s√£o compartilhadas entre diversas camadas dos andar superior. Este andar pode at√© ser compartilhado com outros primeiros andares.

Plataforma

Finalmente, a linguagem assenta em uma plataforma. Esta plataforma pode ser virtual como no caso de linguagens como Java, C# e Python em que as instru√ß√Ķes s√£o executadas em uma m√°quina virtual, ou pode ser uma plataforma f√≠sica como linguagens como C++ e Rust cujas as instru√ß√Ķes s√£o executadas diretamente por CPU reais.

Edifico arquitetural completo em que a plataforma é a base

A escolha da plataforma √© a primeira escolha que temos que fazer para poder montar nosso edif√≠cio de andares de arquitetura. N√£o poderemos mudar essa plataforma depois. N√£o sem ter muito trabalho e custos. Escolher a plataforma √© onde assenta o maior risco e desafio. Se o seu alvo √© uma plataforma especifica como seja um celular Android ou um celular iOS, n√£o √© preciso pensar muito. Ambos t√™m plataformas especificas. Mas se voc√™ visa os dois em simult√Ęneo as coisas se complicam bastante. O mesmo para sistemas como Windows. Mac e Linux.

Além das diferentes plataformas, hoje em dias temos opção de diferentes linguagens em cada plataforma e até linguagens multiplataforma como Kotlin, Scala, Dart ou Ceylon. Estas linguagens normalmente se comunicam bem dentro da mesma plataforma com outras linguagens e com a linguagem nativa da plataforma. Isto nos dá mais margem de manobra caso queiramos construir nosso edifício arquitetural em mais de uma linguagem. Contudo, isso não é recomendado.

Podem existir casos onde misturar mais do que uma linguagem no seu edif√≠cio arquitetural seja uma boa aposta, mas isso tem que ser feito dentro das regras que abordamos – limitando o uso da linguagem a um conjunto restrito de camadas. Por exemplo, √© muito comum termos que usar SQL para comunicar com o banco de dados, mas o uso essa linguagem √© restrito √† camada de Store. Da mesma forma, quando uma linguagem especial √© usada para construir front ends o seu uso tem que ser limitado √† camada de Delivery. Usar uma linguagem diferente para cada camada, embora poss√≠vel torna a manuten√ß√£o cada vez mais dif√≠cil porque que quem vai fazer a manuten√ß√£o seja poliglota e fluente em todas essas linguagens. H√° que usar com parcim√īnia e para os casos espec√≠ficos onde a linguagem natural da plataforma n√£o atende. Contudo com a evolu√ß√£o de v√°rios padr√Ķes como Fluent Builder e a pr√≥pria evolu√ß√£o das linguagens √© cada vez menos necess√°rio misturar linguagens.

Algumas pessoas defendem que a vantagem de usar micro serviços é poder construir diferentes nodos para formar o software. Cada nodo com seu edifício arquitetural e cada um com uma plataforma diferente. Isto pode funcionar em casos muito específicos onde a tecnologia de uma plataforma é mais eficiente de a que de outra. Mas não funciona no caso geral. Dito de outra forma; funciona quando os micro serviços representam serviços de aplicação, mas não quando representam serviços de domínio. Manter os contextos de domínio estritamente separados é equivalente a não existir um Shared Kernel e isso é equivalente a perde controle sobre as partes comuns dos contextos de domínio o que causa ruído na comunicação entre eles.

Arquitetura Multidimensional

Como vimos ao longo desta serie de artigos, a construção de um software passa pela definição de dependências entre as classes de uma forma que a manutenção de uma classe não implique em mudanças em outras. Ganhamos este isolamento estabelecendo camadas com responsabilidades especificas e regras rígidas de direção de dependência.

Considerando o que vimos at√© agora temos 3 dire√ß√Ķes de depend√™ncia a considerar :

De fora para dentro Рas nossas camadas são concêntricas com as camadas mais exteriores dependendo das camadas mais interiores até uma camada central que é independente das demais.

Da entrada para a sa√≠da – as nossas camadas se dividem em duas regi√Ķes. Uma cuja responsabilidade √© receber comandos externos e uma cuja responsabilidade √© comandar outros software. Vimos como o conceito de portos e adaptadores nos ajuda a entender este fluxo.

De cima para baixo Рa arquitetura plana não é suficiente na prática sendo necessário ter mais andares para incluir classes com responsabilidades importantes, mas que têm que ser compartilhadas pelas camadas originais. A dependência vertical vai até à plataforma escolhida para ser base do edifício arquitetural e está ligada ao conceito de que todas as camadas são construidas em cima de uma base comum.

Além da terceira dimensão

As 3 dimens√Ķes referidas s√£o a base de uma boa arquitetura, mas n√£o s√£o as √ļnicas. Na realidade existem outras dimens√Ķes mais complicadas de mostrar pictoricamente, mas que tamb√©m desempenham um papel importante.

Aspectos

Aspecto √© um conceito que conhecemos da Programa√ß√£o Orientada a Aspectos. A ideia √© que conseguimos interceptar a chamada que um objeto faz e dessa forma expandir ou modificar o resultado da chamada. Os exemplos cl√°ssicos de raz√£o para fazer isto s√£o a√ß√Ķes como log e auditoria, monitoramento como medi√ß√£o de performance e controle de transa√ß√£o.

V√°rios interceptores podem ser adicionados numa mesma chamada e realizar diferentes a√ß√Ķes. Por outro lado, v√°rios interceptores podem ser espalhados por diferentes chamadas em diferentes camadas.

O uso de aspectos se tornou f√°cil e bastante famoso mas se n√£o for usado corretamente pode causar problemas. √Č necess√°rio entender como ele pode respeitar as dire√ß√Ķes de depend√™ncia.

O uso de aspectos é principalmente relacionado com a questão de quando algo acontece. A ideia é intervir quando uma certa ação acontece e para isso colocamos uma forma de interceptar a chamada do método que representa que esse algo está acontecendo. Ora, à primeira vista isto pode parece simples e óbvio. Se interceptamos o método save de um repositório ou serviço pode saber quando algo foi salvo. Mas na prática não é bem assim. O método save de um serviço, por exemplo, pode realizar alguma validação que interrompe o processo e os dados não são realmente gravados.

Como regra os aspectos n√£o podem conter l√≥gica de neg√≥cios nem l√≥gica da aplica√ß√£o j√° que eles n√£o pertencem nem √† camada de aplica√ß√£o nem √† de dom√≠nio. Os aspectos vivem nas entr√Ęncias entre as camadas:

Demarcação da região onde os Aspectos vivem

Podemos ver em vermelho a região onde os aspectos vivem. Esta capacidade do aspectos de se intrometerem entre as camadas é o seu principal beneficio, mas é uma outra dimensão de dependência que temos que levar em consideração. Porque os aspectos vivem entre as camadas eles não podem depender de nenhuma outra camada do mesmo andar. Aspectos não podem depender da aplicação e muito menos do domínio.

O uso de aspectos √© principalmente importante no tratamento de exce√ß√Ķes. Cada camada tem suas exce√ß√Ķes. Normalmente o tratamento final das exce√ß√Ķes fica em algum interceptor na camada azul. Este interceptor √© respons√°vel por analisar a exce√ß√£o e realizar algum tratamento. Por exemplo, mapear a exce√ß√£o para um dos c√≥digos de status do HTTP. Mas se a exce√ß√£o √© lan√ßada da camada de dom√≠nio no centro, como a camada azul a pode tratar se n√£o pode depender dessa camada ?

A solu√ß√£o √© transformar as exce√ß√Ķes entre as camada usando aspectos. Um aspecto pode ser inclu√≠do na chamada servi√ßo ou reposit√≥rio da camada de dom√≠nio. Conceitualmente este aspecto pertence e √© construido na camada de aplica√ß√£o, ent√£o ele tem acesso √†s exce√ß√Ķes de dom√≠nio e de aplica√ß√£o e mapear uma para a outra. Isto significa que o c√≥digo de aplica√ß√£o s√≥ recebe exce√ß√Ķes de aplica√ß√£o, mesmo quando chama objetos do dom√≠nio.

Este é um exemplo clássico do uso de aspectos numa arquitetura plana, mas existem outros. O importante é não colocar regras de negócio nestas classes.

Evolução temporal

Quando se constr√≥i um sistema √© muito comum os seus colegas, e sobre tudo seus chefes, lhe dizerem algo como “n√£o se preocupa com isso agora, depois mudamos se for necess√°rio” . Esta √© a pior frase que pode ser dita em desenvolvimento de software e √© a mais ouvida. Voc√™ n√£o aceitaria isso de um empreiteiro se voc√™ perguntar onde vai ficar a cozinha e ele responder “n√£o se preocupa com isso agora, depois mudamos se for necess√°rio”. Certo?

Certas decis√Ķes t√™m que ser tomadas com anteced√™ncias e n√£o podem ser mudadas depois. Se elas v√£o ser mudadas depois, h√° que isolar essa parte do sistema de forma que quando mudar n√£o impacte o resto. Esse isolamento tamb√©m tem que acontecer propositalmente. Ent√£o mesmo que n√£o saibamos agora como vai ser no futuro temos que desenhar considerando que a forma como implementarmos agora ser√° mudada depois. Temos que desenhar considerando o isolamento da parte que vai mudar. O isolamento entre o qu√™ pode mudar e as suas op√ß√Ķes futuras √© necess√°rio. N√£o podemos simplesmente ignorar esses fatos, fazer o software de qualquer forma e deixar para quem vier depois. Mas normalmente – e contra todas as regras do bom senso – √© o que se faz.

Algumas vezes este isolamento advém do uso de certas ferramentas. Por exemplo, usando Hibernate é bem provável que não seja necessário mudar nenhum código se você migrar de usar um certo gerenciador de banco de dados para outro. Isolamento de banco de dados é meio que implícito se usar Hibernate. Mas nem sempre todas as funcionalidades são tão simples de compreender que será alvo de mudanças no futuro.

Não sei se você já foi alguma vez a pessoa que tem que dar manutenção nesses sistemas que têm muitos anos , ou, pior, ter que criar alguma nova funcionalidade. Eu já. E é até mesmo de sistemas que eu mesmo construir. Sistemas que existem há mais de 5 Р10 anos. Depois de tanto tempo você nem lembra direito qual era o modelo ou as regras. Você tem que ler o código. E o código tem que lhe dizer a regra. Quando as regras não estão isoladas e uma parte é feita na UI e outra no servidor e sabe-se lá onde mais, fica muito complicado de você voltar a entender o sistema. O que dirá, uma pessoas que nem nunca viu o sistema antes ?

A arquitetura multidimensional se preocupa também com essa dependência entre as camadas ao longo do tempo. As camadas têm de ser uma forma que seja fácil inclui mais camadas no meio , ou à volta. Tem que ser fácil trocar uma tecnologia por outra sem ter que mudar as regras ou mexer com os objetos que contém as regras. Esta foi a maior critica ao EJB 2 e que despertou ferramentas como o Spring e conceitos como DDD. Há que ter independência tecnológica das camadas de aplicação e de negócios. Enquanto a tecnologia evolui, essas camadas permanecem.

Não é economicamente viável você escrever uma aplicação nova a cada 3 ou 5 anos. Mas a tecnologia avança de uma forma que você será forçado a isso por fatores externos. A arquitetura multidimensional ajuda você a proteger o investimento na regras de domínio e de aplicação e permite que novos adaptadores sejam criados para ligar esse cerne do software a uma nova camada de delivery ou de storage.

Certas decis√Ķes s√£o tomadas num certo ponto do tempo mas t√™m que ser afetadas tanto pelo hist√≥rico passado como pelo futuro. Normalmente o argumento agora seria algo como “n√£o podemos prover o futuro” e de facto n√£o podemos e n√£o √© isso que estamos pedindo. O pr√≥prio conceito de Orienta√ß√£o a Objetos mais b√°sico √© baseado em isolamento. N√£o √© preciso muito para isolar algo, basta colocar todas as regras em uma √ļnica classe ou por detr√°s de um interface. Se as regras n√£o estiverem espalhadas ser√° muito mais f√°cil mud√°-las depois. E n√£o apenas isso, mas ter certeza que quando mudarmos a regra num certo ponto do c√≥digo isso se reflete em todos os outros.

Conclus√£o

A Arquitetura Multidimensional n√£o √© nova. Arquitetura de Software sempre foi formada por diferentes facetas e dimens√Ķes. Mostrei em detalhe as 3 dimens√Ķes mais importantes e 2 dimens√Ķes extra que vale a pena ter em mente. Se conseguir focar sua aten√ß√£o e dominar as 3 mais importantes j√° ser√° um grande ganho e lhe dar√° insights para entender as outras.

Os detalhes de como o edif√≠cio arquitetural √© dividido variam de autor para autor, o importante √© entender que existem, pelo menos, 3 dire√ß√Ķes de depend√™ncia e uma plataforma que sustenta todo o edif√≠cio. Aqui tem outro exemplo.

As camadas em cada andar dependem do tipo de software sendo construido e talvez nem todas sejam necessárias para alguns projetos. O importante não é decorar as camadas mas entender o seu papel para depois entender se faz falta ou não no software que estamos construido.

Finalmente, é importante sublinhar que estes conceitos não se aplicam apenas a software de backend, mas a qualquer software em qualquer plataforma. Aqui tem um exemplo com Angular no frontend.

Comente

Artigos