Determinar quando separar a interface da implementação é um dos grandes desafios da arquitetura de software. Não há resposta óbvia, porque separar de menos causa problemas e separar demais causa problemas também.
Na minha prática, em função do tipo de problema que eu costumo resolver, adotei como regra geral desenhar soluções procedendo bottom up. É uma regra geral obtida a partir da experiência. Isso significa na prática que os desenhos que o trabalho de arquitetura começa desenhando partes primitivos primeiro, e desenhando partes complexas depois.
De modo que nós estamos frequentemente desenhando e produzindo programas e bibliotecas. A tendência natural, como observado no ecossistema de software livre, é produzir em cada caso um único artefato ou componente: o projeto de um programa gera um foo.exe, o projeto de uma biblioteca gera um foo.jar.
O valor de produzir múltiplos artefatos em projetos como esse não apenas é difícil de entender mas inclusive, possivelmente, ofende a sensibilidade do leitor. Afinal, keep it simple. De fato, nossa regra básica de trabalhar bottom up sempre nos orientou a desenhar nossos projetos partindo deste início absolutamente simples.
A experiência, porém, mudou esse comportamento. Demandas recorrentes, reincidentes, eventualmente levaram todos os nossos projetos a uma condição de múltiplos artefatos. Nenhum dos nossos projetos que sobreviveu por tempo suficiente na trincheira permence gerando apenas um único artefato. Abaixo, vou apresentar alguns casos.
O primeiro deles não pertence estrito senso a uma diferença entre “api” e “impl”, mas, eu considero que pertence lato sensu. Nós frequentemente desenhamos programas para servir ambientes que processam arquivos em algum tipo de batch à moda antiga. Estes programas são modelados como unidades de transformação apropriadas para compor algum tipo de pipeline. Uma coisa muito simples. Exceto que em nenhum dos casos a demanda permaneceu assim: todos eventualmente foram solicitados para integrar sistemas por ligação direta, por chamada de subrotina. De modo que em cada caso houve eventualmente a demanda de uma biblioteca irmã. O caminho natural foi extrair um componente de dentro do outro, de modo que o núcleo da coisa foi extraído para uma biblioteca, reusada pelo programa. Após fazer isso um tanto de vezes, percebemos que neste cenário o programa faz o papel de uma “api” (cuja superfície se oferece a sistemas batch e shells textuais) enquanto a biblioteca faz o papel de “impl” (do ponto de vista do programa). [0]
Dois outros casos não são tão recorrentes quanto o primeiro, separadamente, mas, somados, quase. O primeiro deles acontece no mesmo ambiente. Algumas dessas ferramentas realizam trabalho em um ciclo típico de alocar muitos recursos, operar e liberar os recursos alocados. Certos ambientes ao longo do tempo tiveram sua carga de trabalho aumentada de tal maneira que o custo de alocar e liberar todos os recursos por operação começou a ofender. Análises de perfil de execução tenderam a um veredito: é vantagem introduzir um processo intermédiario, de longa vida, capaz de manter esses recursos ativos, em troca de um pouco de vai-e-vém entre processos. Neste cenário, o programa tem sua função esvaziada para entregar a entrada ao daemon e receber dele saída. Isso é tudo muito razoável, exceto pelo seguinte: não são todas as arquiteturas de usuário que justificam a introdução de um daemon. Somente os casos de alta carga, ou que vieram a ganhar um requisito de latência estrito, se beneficiam o suficiente pra pagar esse custo. Agora, manter dois programas com implementação diferente e mesma “api” pareceu e ainda parece ofensivo.
A solução que nós aplicamos então é manter um programa (uma “api”) com diferentes implementações. Como fazer isso? Replicando a mesma ideia para dentro. A biblioteca que foi extraída do programa, e forma seu núcleo, também foi separada em duas partes: uma “api” e um “core”. Introduzindo algum mecanismo de descoberta de serviço, ou injeção de dependência, o programa se torna completamente independente do “core”. Obtendo esse resultado, construímos uma implementação alternativa para o “core”: um “spool” [1] que realiza seu trabalho delegando para o spool daemon. Acrescentar um novo parâmetro opcional à “api” do programa, que permite informar a implementação desejada, finaliza a solução para o problema. O resultado é um único programa, oferecendo uma única “api” para seus usuários, capaz de operar em dois modos, cada um deles com suas ofertas e seus custos. A definição do programa é tão simples quanto consumir uma “api” de programação e fazer variar, em poucas linhas, qual dos provedores de serviço será escolhido pelo mecanismo de descoberta de serviço ou injeção de dependências. [2]
O terceiro caso é uma variante do segundo. Como discutido no primeiro caso, todos os nossos componentes foram eventualmente demandados integrar por ligação direta, por compilador. Então o mundo se tornou a Internet, e a Internet se tornou a WWW, e aí todo serviço se tornou um web service. Hoje, existe uma grande quantidade de textos onde a palavra «API» denota um endpoint HTTP através do qual se troca documentos JSON com um processo remoto. Aconteceu então a demanda de que nossas bibliotecas se transformasse em coisas desse tipo. Este é foi um caso muito diferente: aparte reusar o “core” para construir a coisa, não parece haver muito mais ligação com todo o resto.
Essa aparência durou somente até os primeiros dias de teste. Claro que foi construído um plano de testes para o web service. Claro que haviam testes de unidade e testes de integração. Mas na hora da agonia, na hora da incerteza, as pessoas responsáveis pela qualidade sentem um grande conforto de poder “rodar um comando” e ver que as coisas estão acontecendo. E o que você faz nesses casos? Uma linha de comando usando curl, né? Eu também. E depois de fazer isso, eu escrevo um programinha pra pessoa poder fazer um teste rapidinho… E de repente, mais uma vez, apareceram dois programas para fazer a mesma coisa, sendo que um deles a equipe está acostumadíssima, com uma “api” já desenhada e validade em produção, enquanto o outro foi feito nas coxas, tem seus próprios bugs, e uma linha de comando não documentada. A antecipação do custo que este caminho inexoravelmente nos cobraria interrompeu rapidamente este processo, e mais uma vez aquilo que veio a se tornar uma regra básica de metodologia foi aplicado: construímos uma implementação alternativa do “core”, que realiza seu trabalho delegando para o “rest”. Tal implementação se pode gerar quase que automaticamente com ferramentas como OpenAPI. Em troca desse esforço, a equipe reutiliza toda a inteligência e o hábito já adquirido com o programa original. E no futuro ainda resta uma opção maluca para alguma arquitetura maluca do futuro que é usar o programa com a implementação “rest” em produção. [3]
Após muitas aventuras que, de algum modo, caem em um desses três casos, nós introduzimos uma regra metodológica básica para o desenho de um “serviço” qualquer: haverá sempre no mínimo três componentes: api, core e tool. Para nós, o custo de fazer isso nunca falhou em se pagar.
- OK, não, nem todos os nossos projetos começaram programas e deles se extraiu uma biblioteca. Houveram projetos que começaram uma biblioteca e dela nasceu um programa. O leitor entenderá facilmente que a conclusão metodológica é a mesma.
- depois de muito procurar um bom nome e fracassar, nós optamos por “spool” como o menos pior. Aceito sugestões.
- poucos mecanismo de injeção de dependência são capazes de lidar com essa situação! O mais notável na minha opinião é OSGi, que é capaz de injetar múltiplos provedores do mesmo serviço sem nenhuma dificuldade.
- eu acho esse cenário inacreditável, mas, é impressionante como a arquiteturas de software do mundo real são esquisitas.

Deixe um comentário