O desenvolvimento de software é ótimo, mas ... acho que todos podemos concordar que pode ser uma montanha-russa emocional. No começo está tudo ótimo. Adicione novos recursos um após o outro em questão de dias, se não horas. Você está em uma maré de sorte!
Avance alguns meses e sua velocidade de desenvolvimento ficará mais lenta. É porque você não está trabalhando tanto quanto antes? Realmente não. Avance mais alguns meses e sua velocidade de desenvolvimento diminuirá ainda mais. Trabalhar neste projeto não é mais divertido e se tornou uma chatice.
Mas fica pior. Você começa a descobrir vários erros em seu aplicativo. Freqüentemente, resolver um bug cria dois novos. Neste ponto, você pode começar a cantar:
99 pequenos bugs no código. 99 pequenos erros. Pegue um, coloque um patch nele,
… 127 pequenos erros no código.
Como você se sente trabalhando neste projeto agora? Se você for como eu, provavelmente começará a perder a motivação. O desenvolvimento deste aplicativo é complicado, 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 abandonar seu código-fonte e reescrever tudo.
Então, qual é a razão desse problema?
A principal causa é o aumento da complexidade. Pela minha experiência, o maior contribuidor para a complexidade geral é o fato de que, na grande maioria dos projetos de software, tudo está conectado. Devido às dependências de cada turma, se você alterar algum código da turma que envia e-mails, seus usuários de repente não podem se registrar. Porquê é isso? Porque o seu código de registro depende do código que envia os e-mails. Agora você não pode mudar nada sem introduzir erros. Simplesmente não é possível rastrear todas as dependências.
Então, você tem isso; a verdadeira causa de nossos problemas é o aumento da complexidade proveniente de todas as dependências que nosso código possui.
O engraçado é que esse problema é conhecido há anos. É um antipadrão comum denominado 'grande bola de argila'. Já 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ê ganha uma grande bola de argila quando cada item é dependente de outros itens. Abaixo você pode ver um gráfico das dependências do popular projeto Apache Hadoop de software livre. Para visualizar o grande novelo de argila (ou melhor, o grande novelo de lã), desenhe um círculo e coloque as classes do projeto uniformemente nele. Basta traçar uma linha entre cada par de classes que dependem uma da outra. Agora você pode ver a origem de seus problemas.
Então me perguntei: seria possível reduzir a complexidade e ainda me divertir como no início do projeto? Verdade seja dita, você não pode deletar todo o mundo a complexidade. Se você quiser adicionar novos recursos, sempre terá que aumentar a complexidade do seu código. No entanto, a complexidade pode se mover e se espalhar.
Pense na indústria mecânica. Quando uma pequena oficina cria máquinas, ela compra um conjunto de itens padrão, cria alguns itens personalizados e os combina. Eles podem fazer esses componentes completamente separadamente e montar tudo por último, fazendo apenas alguns retoques. Como isso é possível? Eles sabem como cada item se ajusta com base nos padrões da indústria, como o tamanho do parafuso, e nas decisões iniciais, como o tamanho dos orifícios de montagem e a distância entre eles.
Cada item do conjunto acima pode ser fornecido por uma empresa independente que não possui nenhum conhecimento sobre o produto final ou suas outras partes. Contanto que cada item modular seja fabricado de acordo com as especificações, você pode criar o dispositivo final conforme planejado.
Podemos replicar isso na indústria de software?
Com certeza podemos! Usando interfaces e invertendo o 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 é infinita. Você não precisa de nenhuma estrutura sofisticada para aplicar este método. Você apenas precisa seguir algumas regras simples e manter a disciplina.
Quando ouvi pela primeira vez sobre reversão de controle, percebi imediatamente que havia encontrado uma solução. É um conceito de pegar dependências existentes e invertê-las por meio do uso de interfaces. Interfaces são declarações de método simples. 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 desejado. Contanto que um elemento forneça a interface e outro elemento forneça a implementação, eles podem trabalhar juntos sem saber nada um sobre o outro. É brilhante.
Vamos ver em um exemplo simples como podemos desacoplar nosso sistema para criar código modular. Os diagramas a seguir foram implementados como aplicativos Java simples. Você pode encontrá-los neste Repositório GitHub .
Suponha que tenhamos um aplicativo muito simples que consiste em apenas 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 apenas se ligam. Eles estão intimamente ligados e você não pode simplesmente puxar um item sem tocar nos outros. Os aplicativos criados neste estilo permitem que você cresça rapidamente. Acho que esse estilo é apropriado para projetos de prova de conceito, já que você pode tocar com facilidade. No entanto, não é adequado para soluções prontas para produção porque até a manutenção pode ser perigosa e qualquer alteração pode criar erros imprevisíveis. O diagrama abaixo mostra esta grande bola de arquitetura de argila.
Em busca de uma abordagem melhor, podemos usar uma técnica chamada injeção de dependência. Este método assume que todos os componentes devem ser usados nas interfaces. Eu li afirmações de que desencaixa itens, mas realmente o faz? 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, nós as chamamos por meio de suas interfaces. Melhora ligeiramente os elementos de separação uns dos outros. Se, por exemplo, você deseja reutilizar Servicio A
Em um projeto diferente, você pode fazer isso retirando Servicio A
, junto com Interfaz A
, bem como Interfaz B
e Interface Útil
. Como você pode ver, o Servicio A
ainda depende de outros elementos. Como resultado, ainda temos problemas para alterar o código em um lugar e bagunçar o comportamento em outro. Ele ainda cria o problema de que, se você modificar Servicio B
e Interfaz B
, você precisará alterar todos os elementos que dependem dele. Essa abordagem não resolve nada; na minha opinião, ele apenas adiciona uma camada de interface no topo dos elementos. Você nunca deve injetar dependências; em vez disso, você deve se livrar delas de uma vez por todas. Viva a independência!
A abordagem que eu acho que resolve todas as principais dores de cabeça de dependência faz isso por não usar 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ó pode usar arquivos, chamar métodos dentro de seu pacote e usar classes fornecidas pela estrutura 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
ele tem várias dependências. Conecte todos os elementos e encapsule a lógica de negócios do aplicativo.
Os serviços, por outro lado, são elementos totalmente independentes. Agora, você pode retirar cada serviço deste aplicativo e reutilizá-lo em outro lugar. Eles não dependem de mais nada. Mas espere, fica melhor: você não precisa mais modificar esses serviços, contanto que não mude seu comportamento. Contanto que esses serviços façam o que devem fazer, eles podem ser deixados intactos até o fim dos tempos. Eles podem ser criados por um engenheiro de software profissional , ou um programador iniciante comprometido com o pior código espaguete que alguém já inventou goto
misturado. Não importa, porque sua lógica está 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 autônomo mais uma vez, assim como no início de seu último projeto.
Vamos definir o padrão do elemento estrutural para que possamos criá-lo repetidamente.
A versão mais simples do elemento consiste em duas coisas: uma classe de elemento principal e um ouvinte. Se quiser usar um elemento, você 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 você pode fazer isso facilmente. Apenas certifique-se de que nenhuma de suas classes lógicas dependa de outros arquivos no projeto. Eles só podem usar o quadro principal, bibliotecas importadas e outros arquivos neste elemento. Quando se trata de arquivos de ativos, como imagens, imagens, sons, etc., eles também devem ser encapsulados nos elementos para que sejam fáceis de reutilizar no futuro. Você pode simplesmente copiar a pasta inteira para outro projeto e pronto!
Abaixo você pode ver um gráfico de amostra mostrando um item mais avançado. Observe que ele consiste em uma visualização que você está usando e não depende de nenhum outro arquivo de aplicativo. Se você quiser conhecer um método simples para verificar dependências, basta olhar a seção de importação. Existe algum arquivo de fora do elemento atual? Nesse caso, você deve remover essas dependências movendo-as para o elemento ou adicionando uma chamada apropriada ao ouvinte.
Vamos dar uma olhada em um exemplo simples de “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 próprio elemento é definido abaixo. Ligando sayHello
no elemento, basta imprimir 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 esta abstração, este elemento pode ser facilmente reutilizado em diferentes aplicações.
Agora dê uma olhada na classe principal de App
. Implemente o ouvinte e monte 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
Vamos dar uma olhada no uso do padrão de elemento em aplicativos de grande escala. Uma coisa é exibi-lo em um pequeno projeto; outra é aplicá-lo ao mundo real.
A estrutura de um aplicativo da web full stack que gosto de usar é assim:
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements
Em 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 apps e itens. Os itens consistem em pastas com componentes separados, enquanto a pasta do aplicativo conecta todos os itens 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 frequentemente reduzida a chamadas de elementos simples.
Se acreditamos que a prática sempre supera a teoria, vamos dar uma olhada em um exemplo da vida real criado em Node.js e TypeScript.
É 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 e usa um padrão de elemento amplamente estrutural.
Nos destaques, você pode ver que a página principal foi distinguida como um item. Esta página inclui sua própria visualização. Quando, por exemplo, você quiser reutilizá-lo, basta copiar a pasta inteira e colocá-la em um projeto diferente. Basta conectar tudo e você está pronto para ir.
É um exemplo básico que demonstra que você pode começar a introduzir itens em seu próprio aplicativo hoje. Você pode começar a distinguir componentes independentes e separar sua lógica. Não importa o quão confuso seja o código no qual você está trabalhando atualmente.
Espero que, com este novo conjunto de ferramentas, você possa desenvolver código com mais facilidade e manutenção. Antes de começar a usar o padrão de elemento na prática, vamos examinar rapidamente todos os pontos principais:
Muitos problemas ocorrem no software devido a dependências entre vários componentes.
Ao fazer uma mudança em um lugar, você pode introduzir um comportamento imprevisível em outro.
As três abordagens arquitetônicas comuns são:
A grande bola de barro. É ótimo para desenvolvimento rápido, mas não tão bom para fins de produção estável.
Injeção de dependência. É uma meia solução que você deve evitar.
Arquitetura do elemento. Esta solução permite criar componentes independentes e reutilizá-los em outros projetos. É sustentável 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 listener que é uma interface simples que permite a comunicação com o mundo externo.
Para obter uma arquitetura de elemento de pilha completa, o front-end é primeiro separado do código de back-end. Em seguida, crie uma pasta em cada um para um aplicativo e itens. A pasta de itens consiste em todos os itens independentes, enquanto a pasta de aplicativos conecta tudo junto.
Agora você pode começar a criar e compartilhar seus próprios itens. No longo prazo, ele o ajudará a criar produtos de fácil manutenção. Boa sorte e deixe-me saber 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 meu parceiro do ApeeScape, Kevin Bloch.