Criando um código verdadeiramente modular sem dependências
Publicados: 2022-03-11Desenvolver software é ótimo, mas… acho que todos podemos concordar que pode ser uma montanha-russa emocional. No início, tudo é ótimo. Você adiciona novos recursos um após o outro em questão de dias, se não horas. Você está em um rolo!
Avance alguns meses e sua velocidade de desenvolvimento diminui. É porque você não está trabalhando tão duro quanto antes? Na verdade. Vamos avançar mais alguns meses e sua velocidade de desenvolvimento cai ainda mais. Trabalhar neste projeto não é mais divertido e se tornou uma chatice.
Fica pior. Você começa a descobrir vários bugs em seu aplicativo. Muitas vezes, resolver um bug cria dois novos. Neste ponto, você pode começar a cantar:
99 pequenos bugs no código. 99 pequenos insetos. Pegue um para baixo, remenda-o ao redor,
…127 pequenos bugs no código.
Como você se sente trabalhando neste projeto agora? Se você é como eu, provavelmente começa a perder a motivação. É muito trabalhoso desenvolver esse aplicativo, pois cada alteração no código existente pode ter consequências imprevisíveis.
Essa experiência é comum no mundo do software e pode explicar por que tantos programadores querem jogar fora seu código-fonte e reescrever tudo.
Razões pelas quais o desenvolvimento de software diminui com o tempo
Então, qual é a razão para este problema?
A principal causa é o aumento da complexidade. Pela minha experiência, o maior contribuinte para a complexidade geral é o fato de que, na grande maioria dos projetos de software, tudo está conectado. Por causa das dependências que cada classe possui, se você alterar algum código na classe que envia emails, seus usuários de repente não poderão se registrar. Por que é que? Porque seu código de registro depende do código que envia e-mails. Agora você não pode mudar nada sem introduzir bugs. Simplesmente não é possível rastrear todas as dependências.
Então aí está; a verdadeira causa de nossos problemas é aumentar a complexidade proveniente de todas as dependências que nosso código possui.
Grande bola de lama e como reduzi-la
O engraçado é que esse problema é conhecido há anos. É um antipadrão comum chamado de “grande bola de lama”. Eu vi esse tipo de arquitetura em quase todos os projetos em que trabalhei ao longo dos anos em várias empresas diferentes.
Então, o que é exatamente esse antipadrão? Simplesmente falando, você obtém uma grande bola de lama quando cada elemento tem uma dependência com outros elementos. Abaixo, você pode ver um gráfico das dependências do conhecido projeto de código aberto Apache Hadoop. Para visualizar a grande bola de lama (ou melhor, a grande bola de lã), você desenha um círculo e coloca as classes do projeto uniformemente nele. Basta desenhar uma linha entre cada par de classes que dependem uma da outra. Agora você pode ver a fonte de seus problemas.
Uma solução com código modular
Então me fiz uma pergunta: seria possível diminuir a complexidade e ainda se divertir como no início do projeto? Verdade seja dita, você não pode eliminar toda a complexidade. Se você quiser adicionar novos recursos, sempre terá que aumentar a complexidade do código. No entanto, a complexidade pode ser movida e separada.
Como outras indústrias estão resolvendo esse problema
Pense na indústria mecânica. Quando uma pequena oficina mecânica está criando máquinas, eles compram um conjunto de elementos padrão, criam alguns personalizados e os juntam. Eles podem fazer esses componentes completamente separadamente e montar tudo no final, fazendo apenas alguns ajustes. Como isso é possível? Eles sabem como cada elemento se encaixará de acordo com os padrões estabelecidos do setor, como tamanhos de parafusos, e decisões antecipadas, como o tamanho dos furos de montagem e a distância entre eles.
Cada elemento da montagem acima pode ser fornecido por uma empresa separada que não tem conhecimento algum sobre o produto final ou suas demais peças. Desde que cada elemento modular seja fabricado de acordo com as especificações, você poderá criar o dispositivo final conforme planejado.
Podemos replicar isso na indústria de software?
Claro que podemos! Usando interfaces e inversão do princípio de controle; a melhor parte é o fato de que essa abordagem pode ser usada em qualquer linguagem orientada a objetos: Java, C#, Swift, TypeScript, JavaScript, PHP — a lista continua. Você não precisa de nenhuma estrutura sofisticada para aplicar esse método. Você só precisa seguir algumas regras simples e manter a disciplina.
A inversão de controle é sua amiga
Quando ouvi pela primeira vez sobre a inversão de controle, percebi imediatamente que havia encontrado uma solução. É um conceito de pegar dependências existentes e invertê-las usando interfaces. Interfaces são simples declarações de métodos. Eles não fornecem nenhuma implementação concreta. Como resultado, eles podem ser usados como um acordo entre dois elementos sobre como conectá-los. Eles podem ser usados como conectores modulares, se você quiser. Desde que um elemento forneça a interface e outro elemento forneça a implementação para ela, eles podem trabalhar juntos sem saber nada um do outro. É brilhante.
Vamos ver em um exemplo simples como podemos desacoplar nosso sistema para criar código modular. Os diagramas abaixo foram implementados como aplicativos Java simples. Você pode encontrá-los neste repositório do GitHub.
Problema
Vamos supor que temos uma aplicação muito simples que consiste apenas em uma classe Main , três serviços e uma única classe Util . Esses elementos dependem uns dos outros de várias maneiras. Abaixo, você pode ver uma implementação usando a abordagem “grande bola de lama”. As classes simplesmente chamam umas às outras. Eles estão fortemente acoplados e você não pode simplesmente remover um elemento sem tocar nos outros. Os aplicativos criados usando esse estilo permitem que você cresça rapidamente. Acredito que esse estilo seja apropriado para projetos de prova de conceito, pois você pode brincar com as coisas facilmente. No entanto, não é apropriado para soluções prontas para produção porque até mesmo a manutenção pode ser perigosa e qualquer alteração única pode criar bugs imprevisíveis. O diagrama abaixo mostra essa grande bola de arquitetura de lama.
Por que a injeção de dependência deu tudo errado
Em busca de uma abordagem melhor, podemos usar uma técnica chamada injeção de dependência. Esse método pressupõe que todos os componentes devem ser usados por meio de interfaces. Eu li alegações de que ele dissocia elementos, mas isso realmente acontece? Não. Dê uma olhada no diagrama abaixo.
A única diferença entre a situação atual e uma grande bola de lama é o fato de que agora, em vez de chamar as classes diretamente, as chamamos por meio de suas interfaces. Melhora ligeiramente a separação de elementos uns dos outros. Se, por exemplo, você quiser reutilizar o Service A em um projeto diferente, poderá fazer isso retirando o próprio Service A , junto com a Interface A , bem como a Interface B e a Interface Util . Como você pode ver, o Service A ainda depende de outros elementos. Como resultado, ainda temos problemas ao alterar o código em um lugar e atrapalhar o comportamento em outro. Ainda cria o problema de que, se você modificar o Service B e a Interface B , precisará alterar todos os elementos que dependem dele. Essa abordagem não resolve nada; na minha opinião, apenas adiciona uma camada de interface em cima dos elementos. Você nunca deve injetar dependências, mas sim se livrar delas de uma vez por todas. Viva a independência!
A solução para código modular
A abordagem que acredito resolve todas as principais dores de cabeça das dependências não usando dependências. Você cria um componente e seu ouvinte. Um ouvinte é uma interface simples. Sempre que você precisar chamar um método de fora do elemento atual, basta adicionar um método ao ouvinte e chamá-lo. O elemento só tem permissão para usar arquivos, chamar métodos dentro de seu pacote e usar classes fornecidas pelo framework principal ou outras bibliotecas usadas. Abaixo, você pode ver um diagrama do aplicativo modificado para usar a arquitetura de elementos.

Observe que, nesta arquitetura, apenas a classe Main possui várias dependências. Ele conecta todos os elementos e encapsula a lógica de negócios do aplicativo.
Os serviços, por outro lado, são elementos completamente independentes. Agora, você pode retirar cada serviço deste aplicativo e reutilizá-los em outro lugar. Eles não dependem de mais nada. Mas espere, fica melhor: você não precisa modificar esses serviços novamente, desde que não altere o comportamento deles. Contanto que esses serviços façam o que deveriam fazer, eles podem ser deixados intocados até o fim dos tempos. Eles podem ser criados por um engenheiro de software profissional, ou um programador de primeira viagem comprometido com o pior código de espaguete que alguém já cozinhou com instruções goto misturadas. Não importa, porque sua lógica é encapsulada. Por mais horrível que seja, nunca se espalhará para outras classes. Isso também lhe dá o poder de dividir o trabalho em um projeto entre vários desenvolvedores, onde cada desenvolvedor pode trabalhar em seu próprio componente de forma independente, sem a necessidade de interromper outro ou mesmo saber da existência de outros desenvolvedores.
Finalmente, você pode começar a escrever código independente mais uma vez, assim como no início do seu último projeto.
Padrão de elemento
Vamos definir o padrão do elemento estrutural para que possamos criá-lo de maneira repetível.
A versão mais simples do elemento consiste em duas coisas: uma classe de elemento principal e um ouvinte. Se você quiser usar um elemento, precisará implementar o ouvinte e fazer chamadas para a classe principal. Aqui está um diagrama da configuração mais simples:
Obviamente, você precisará adicionar mais complexidade ao elemento eventualmente, mas pode fazê-lo facilmente. Apenas certifique-se de que nenhuma de suas classes lógicas dependa de outros arquivos no projeto. Eles só podem usar a estrutura principal, bibliotecas importadas e outros arquivos neste elemento. Quando se trata de arquivos de ativos como imagens, visualizações, sons, etc., eles também devem ser encapsulados em elementos para que no futuro sejam fáceis de reutilizar. Você pode simplesmente copiar a pasta inteira para outro projeto e pronto!
Abaixo, você pode ver um gráfico de exemplo mostrando um elemento mais avançado. Observe que ele consiste em uma visão que está usando e não depende de nenhum outro arquivo de aplicativo. Se você quiser conhecer um método simples de verificar dependências, basta olhar na seção de importação. Existem arquivos de fora do elemento atual? Nesse caso, você precisa remover essas dependências movendo-as para o elemento ou adicionando uma chamada apropriada ao ouvinte.
Vamos também dar uma olhada em um exemplo simples “Hello World” criado em Java.
public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } } Inicialmente, definimos ElementListener para especificar o método que imprime a saída. O elemento em si é definido abaixo. Ao chamar sayHello no elemento, ele simplesmente imprime uma mensagem usando ElementListener . Observe que o elemento é completamente independente da implementação do método printOutput . Ele pode ser impresso no console, em uma impressora física ou em uma interface de usuário sofisticada. O elemento não depende dessa implementação. Devido a essa abstração, esse elemento pode ser facilmente reutilizado em diferentes aplicações.
Agora dê uma olhada na classe App principal. Ele implementa o ouvinte e monta o elemento junto com a implementação concreta. Agora podemos começar a usá-lo.
Você também pode executar este exemplo em JavaScript aqui
Arquitetura de elementos
Vamos dar uma olhada no uso do padrão de elemento em aplicativos de grande escala. Uma coisa é mostrá-lo em um projeto pequeno, outra é aplicá-lo ao mundo real.
A estrutura de um aplicativo da Web full-stack que gosto de usar é a seguinte:
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elementsEm uma pasta de código-fonte, inicialmente dividimos os arquivos do cliente e do servidor. É uma coisa razoável a se fazer, pois eles são executados em dois ambientes diferentes: o navegador e o servidor back-end.
Em seguida, dividimos o código em cada camada em pastas chamadas app e elements. Elementos consiste em pastas com componentes independentes, enquanto a pasta do aplicativo conecta todos os elementos e armazena toda a lógica de negócios.
Dessa forma, os elementos podem ser reutilizados entre diferentes projetos, enquanto toda a complexidade específica do aplicativo é encapsulada em uma única pasta e muitas vezes reduzida a simples chamadas para elementos.
Exemplo prático
Acreditando que a prática sempre supera a teoria, vamos dar uma olhada em um exemplo da vida real criado em Node.js e TypeScript.
Exemplo da vida real
É uma aplicação web muito simples que pode ser usada como ponto de partida para soluções mais avançadas. Ele segue a arquitetura do elemento, bem como usa um padrão de elemento estrutural extensivamente.
Dos destaques, você pode ver que a página principal foi distinguida como um elemento. Esta página inclui sua própria visualização. Então, quando, por exemplo, você quiser reutilizá-lo, basta copiar a pasta inteira e soltá-la em um projeto diferente. Basta conectar tudo e pronto.
É um exemplo básico que demonstra que você pode começar a introduzir elementos em seu próprio aplicativo hoje. Você pode começar a distinguir componentes independentes e separar sua lógica. Não importa quão confuso seja o código em que você está trabalhando atualmente.
Desenvolva mais rápido, reutilize com mais frequência!
Espero que, com este novo conjunto de ferramentas, você possa desenvolver mais facilmente um código mais sustentável. Antes de começar a usar o padrão de elemento na prática, vamos recapitular rapidamente todos os pontos principais:
Muitos problemas em software acontecem por causa de dependências entre vários componentes.
Ao fazer uma mudança em um lugar, você pode introduzir um comportamento imprevisível em outro lugar.
Três abordagens arquitetônicas comuns são:
A grande bola de lama. É ótimo para desenvolvimento rápido, mas não tão bom para fins de produção estável.
Injeção de dependência. É uma solução incompleta que você deve evitar.
Arquitetura do elemento. Esta solução permite criar componentes independentes e reutilizá-los em outros projetos. É fácil de manter e brilhante para lançamentos de produção estáveis.
O padrão de elemento básico consiste em uma classe principal que possui todos os métodos principais, bem como um ouvinte que é uma interface simples que permite a comunicação com o mundo externo.
Para alcançar a arquitetura de elementos de pilha completa, primeiro você separa seu código de front-end do código de back-end. Em seguida, você cria uma pasta em cada um para um aplicativo e elementos. A pasta de elementos consiste em todos os elementos independentes, enquanto a pasta do aplicativo conecta tudo.
Agora você pode começar a criar e compartilhar seus próprios elementos. A longo prazo, isso o ajudará a criar produtos de fácil manutenção. Boa sorte e me conte o que você criou!
Além disso, se você estiver otimizando prematuramente seu código, leia Como evitar a maldição da otimização prematura , do colega Toptaler Kevin Bloch.
