Você não é o seu framework: como projetar software saudável

Daniel Oliveira
6 min readAug 9, 2017

--

Como desenvolvedores, nós não conseguimos evitar o uso de bibliotecas e frameworks em nossos sistemas. As mãos da comunidade constroem ferramentas maravilhosas, e nada é mais natural que usá-las. No entanto, tudo tem um lado negativo.

Times e indivíduos mais descuidados podem entrar numa situação perigosa ao estruturar suas aplicações ao redor das ferramentas que usam. Regras de negócio se misturam a detalhes de implementação, e o resultado é um sistema quebradiço, difícil de estender e manter. O que deveria ser uma rápida modificação na interface gráfica acaba se tornando uma caça ao bug que se estende por horas. Mas não tem que ser assim.

A Arquitetura de Software propõe modelos e regras para determinar as estruturas de um sistema (i.e. classes, interfaces e structs) e como elas se relacionam. Essas regras promovem a reusabilidade e a separação de responsabilidades destes elementos, tornando fácil a mudança de detalhes de implementação como o SGBD ou a biblioteca de front-end. Refatorações e correções de bugs afetam o mínimo possível de partes do sistema, e adicionar novas features fica fácil.

Nesse artigo, farei uma demonstração de um modelo de arquitetura proposto em 2012 por Robert C. Martin, o Tio Bob. Ele é o autor de clássicos como Código Limpo e O Codificador Limpo; em Outubro desse ano, lançará outro livro (que já reservei para mim), Arquitetura Limpa. O modelo tem o nome do livro, e é conceitualmente simples:

A proposta é dividir a composição do sistema em camadas com papéis distintos e bem definidos. Também há uma regra que restringe os relacionamentos entre entidades que se encontram em diferentes camadas. Não há nada novo em dividir sua aplicação em camadas, mas escolhi essa abordagem porque ela foi a que achei mais simples de entender e executar, e ela torna o teste de casos de uso muito simples. Nós só temos que assegurar que os Interactors funcionam direito, e tá tudo certo. E não se preocupe se a palavra Interactors pareceu estranha para você, nós iremos aprender sobre eles logo mais.

De dentro pra fora, vamos explorar mais a fundo cada uma das camadas, usando como exemplo uma aplicação muito batida, mas simples o bastante para não nos distrair do tema deste artigo: contadores.

Uma demonstração da aplicação pode ser encontrada aqui, e os exemplos serão dados em TypeScript. Alguns dos trechos de código abaixo utilizam React e Redux, então o conhecimento sobre essas soluções pode ajudar no entendimento. No entanto, os conceitos da Arquitetura Limpa são muito mais universais, e podem ser entendidos mesmo sem conhecimento prévio das ferramentas.

Entidades

Entidades (também apontadas na ilustração como Regras de Negócio da Organização) englobam regras de negócio que são universais a uma companhia, representando entidades básicas à sua área de atuação. Entidades são os componentes com o maior nível de abstração.

No nosso exemplo de contadores, uma Entidade é bem óbvia: o próprio Contador.

Eu encapsulei o numeral que representa o contador no tipo Counter. Isso foi feito para nos permitir que modifiquemos esse tipo no futuro com o mínimo possível de impacto no resto do sistema.

Casos de Uso

Casos de Uso (apontados como Regras de Negócio da Aplicação) representam cada um dos casos de uso de uma determinada aplicação. Cada elemento dessa camada proveem uma interface à camada mais externa e agem como uma central que se comunica com outras partes do sistema. Eles são responsáveis pela completa execução dos casos de uso, e são comumente chamados de Interactors.

No nosso exemplo, temos um Caso de Uso para incrementar e decrementar nosso contador:

Perceba que a função que cria instâncias de ChangeCounterInteractor recebe um parâmetro do tipo CounterGateway. A existência desse tipo será discutida mais adiante, mas podemos dizer que gateways são o "portão de acesso" entre os Casos de Uso e a próxima camada:

Adaptadores de Interface

Essa camada é a fronteira entre as Entidades e Casos de Uso e as ferramentas que permitem seu contato com o mundo externo, como bancos de dados e interfaces gráficas. Elementos nessa camada atuam como mediadores: recebem dados de uma camada e os repassam a outra, adaptando os dados da forma mais conveniente.

No nosso exemplo, temos vários Adaptadores de Interface. Um deles é o componente React que apresenta o contador e os controles para incrementar e decrementar:

Note que o componente não faz uso de uma instância de Counter para apresentar o valor do contador, e sim de CounterViewData. Essa troca foi feita para desacoplar a lógica de apresentação dos dados de negócio. Um exemplo disso é a lógica de exibição do contador baseado no modo de visualização (numerais romanos ou indo-arábicos). A implementação de CounterViewData segue abaixo:

Note como CounterViewData retém a lógica de exibição do contador em si.

Outro exemplo de Adaptador de Interface seria a implementação do Redux em nossa aplicação. Módulos responsáveis por requisições a um servidor e uso de local storage também residem nessa camada.

Frameworks e Drivers

As ferramentas utilizadas pelo seu sistema de modo a se comunicar com o mundo externo compoem a camada mais externa. Normalmente, não se escreve código pertencente a essa camada, que inclui bibliotecas como React e Redux, APIs de browser, etc.

A regra das dependências

Essa divisão em camadas tem dois objetivos principais. Um deles é esclarecer as responsabilidades de cada parte do sistema. A outra é assegurar que cada uma delas cumpre seu papel da forma mais independente possível das outras. Para que isso acontece, existe uma regra que determina como os elementos devem depender dos outros:

Um elemento não deve depender de nenhum elemento que pertença a um círculo mais externo que o seu.

Por exemplo, um elemento na camada de Casos de Uso não pode ter conhecimento sobre nenhuma classe ou módulo relacionados à interface gráfica ou à persistência de dados. De forma análoga, uma Entidade não deve saber de quais casos de uso a envolvem.

Essa regra pode levantar dúvidas. Tome como exemplo um Caso de Uso. Quando ele é iniciado como resultado da interação com a UI, sua execução envolve a atualização persistente de dados (por exemplo, num banco de dados). Como o Interactor pode fazer as chamadas relevantes à atualização sem depender de um Adaptador de Interface responsável pela persistência de dados?

A resposta está num elemento mencionado anteriormente: Gateways. Eles são responsáveis por estabelecer a interface de que os Casos de Uso precisam para realizar suas tarefas. Uma vez que essa interface é estabelecida, cabe aos Adaptadores de Interface atender a esse contrato, como no diagrama acima. Abaixo temos a definição da interfaceCounterGateway e uma implementação concreta usando Redux:

Você pode não precisar

É claro, esse exemplo foi um tanto complicada demais para uma aplicação de incrementar e decrementar um contador. E eu gostaria de deixar claro que você não precisa disso tudo para um pequeno projeto ou protótipo. Mas confie em mim, à medida que sua aplicação crescer você vai querer maximizar a capacidade de reuso e manutenção de seu código. Uma boa arquitetura de software torna projetos resistentes à passagem do tempo.

Tá… mas e daí?

Com esse artigo, conhecemos uma abordagem para desacoplar as entidades de nossos sistemas, de modo a torná-los mais facilmente extensíveis. Por exemplo, para implementar a mesma aplicação usando Vue.js, teríamos que re-escrever apenas os elementos CounterPage e CounterWidget, mantendo até mesmo CounterViewData intacta. O código-fonte da aplicação de exemplo se encontra no link abaixo:

Que vantagens e/ou desvantagens você vê nessa abordagem? Já usou ela em algum projeto em produção? Compartilhe suas experiências através de comentários. Se gostou do artigo, bata palmas sem medo de ser feliz!

--

--

Daniel Oliveira

Programmer, wannabe-philosopher, music lover, dreamer. Get to know me better @ www.dvalbrand.com