Android

17 ago, 2017

“Clean Architecture” para Android

Publicidade

Nós sabemos que desenvolver um software de qualidade é difícil e complexo. Não é apenas questão de satisfazer os requisitos; ele precisa ser robusto, de fácil manutenção, testável e flexível para se adaptar a diversas mudanças durante seu crescimento. Para alcançar esse resultado, os desenvolvedores recorrem a diversas práticas como Clean Architecture, Domain-Driven DesignModel-View-ControllerModel-View-PresenterDesign Patterns e diversas outras.

A ideia por trás de todas elas é simples. Um conjunto de práticas que tem como propósito entregar um sistema que seja:

  • Independente de frameworks: a arquitetura não depende da existência de alguma biblioteca. Isso permite que você use tais estruturas como ferramentas, em vez de ter que enfiar o sistema em suas restrições limitadas;
  • Testável: as regras de negócio devem poder ser testadas sem depender de uma interface do usuário, banco de dados, servidor web ou qualquer agente externo;
  • Independente da UI: a UI pode mudar facilmente, sem alterar o resto do sistema. A interface do usuário da web pode ser substituída por um UI console, por exemplo, sem alterar as regras de negócios;
  • Independente do banco de dados: você precisa ser capaz de trocar seu Banco, do SQLite para PaperDB, RealmDB, Cupboard ou qualquer outro sem que suas regras de negócios sejam afetadas durante o processo;
  • Independente de qualquer agente externo: na verdade, suas regras de negócios simplesmente não sabem nada sobre o mundo exterior.

Nada disso é diferente quando se está desenvolvendo um aplicativo para Android. Durante algumas pesquisas, encontrei muitas postagens que explicavam os tópicos descritos acima. O problema é que a grande maioria complicava o assunto e/ou acabava fazendo overengineering. Meu objetivo não é explicar a teoria sobre a Clean Architecture, mas mostrar como costumo implementar a arquitetura dos softwares (quando há tempo, claro) baseados em conceitos como o Clean ArchitectureDomain-Driven Design e Model-View-Controller.

OverEngineering (excesso de engenharia) é utilizar mais do que o necessário no desenvolvimento de um software, tornando-o mais complicado do que deveria ser. Para mais detalhes sobre overengineering, veja (em inglês) aqui.

Obviamente, esta não é uma solução perfeita e talvez seja errado eu chamá-la de Clean Architecture, mas se mostrou satisfatória para o desenvolvimento de alguns aplicativos que desenvolvi. Qualquer feedback a respeito será muito bem recebido nos comentários, então, leia tudo antes de criticar toda a arquitetura.

O que é Clean Architecture?

Proposto por Robert Cecil Martin (muito conhecido como Uncle Bob), ela tem a proposta de focar no domínio da aplicação, sendo os drivers, frameworks e libraries apenas detalhes de implementação.

Para mais detalhes, veja (em inglês) aqui.

Clean Architecture no Android

O objetivo é o princípio da responsabilidade única, que separa o interesse de cada módulo e mantém as regras de negócio sem conhecer qualquer detalhe sobre o mundo exterior; assim, eles podem ser testados sem dependência de qualquer elemento externo.

Para alcançar esse resultado, a minha proposta é separar o projeto em três módulos diferentes (camadas), nos quais cada um teria o seu propósito e funcionaria separadamente dos outros.

Agora que você entendeu o conceito, veja a arquitetura:

Módulos no projeto Android; verde significa que é um “Módulo Android” e vermelho significa que é um “Módulo Java”
  • Presentation (módulo Android): responsável pela interface do aplicativo e a exibição dos dados recebidos do domínio;
  • Domain (módulo Java): responsável pelas entidades e as regras de domínio específicas do seu projeto. Esse módulo deve ser totalmente independente da plataforma Android;
  • Infrastruture (módulo Android): responsável pelo banco de dados, acesso à internet e outros “detalhes” da aplicação.
As fronteiras dos módulos são definidos pela cor azul

Para melhor definir a separação dos módulos, temos um conceito de boundary (fronteira) que apresenta um conjunto de regras definidas por interfaces para que uma camada possa se comunicar com a camada seguinte. Cada uma dessas essas especificações estão disponíveis em sua devida camada, na qual será definido o contrato para que esta possa ser acessada.

Presentation (módulo Android)

 

Este é o módulo responsável pela apresentação dos dados, animações, listas e execução das Classes e Objetos Android. Essa camada não é mais do que um Model-View-Controller (MVC, a partir de agora), mas você pode usar qualquer outro padrão que se sentir confortável: como MVP ou MVVM.

Para mais detalhes sobre MVC, veja (em inglês) aqui.

Não vou entrar em muitos detalhes sobre o MVC, mas aqui as Activities e Fragments são apenas pontos de vista: não há lógica dentro deles que não seja a lógica de interface do usuário. É aqui que todas as coisas são renderizadas e as animações são executadas.

Os Controllers nesta camada são responsáveis por acessar os Boundaries (neste caso, as interfaces dos Interactors) que executam suas tarefas em uma thread fora da interface do usuário e retornam um Callback com os dados que serão exibidos.

Os Interactors contêm as regras de negócio específicas de sua aplicação. Ela incorpora e implementa todos os casos de uso do sistema e muitos preferem usar o termo Use Case (caso de uso) para se referir a um Interactor. Mais detalhes sobre eles serão vistos no Módulo de Domínio.

Os Helpers são funcionalidades compartilhadas em múltiplas activities ou fragments, tendo o propósito de ajudar na apresentação de dados.

Domain (módulo Java)

Toda a lógica acontece neste módulo. Esta camada sempre será um módulo Java puro sem qualquer dependência Android. Todos os componentes externos usam as interfaces (Interactor Interface; Boundary) durante a execução dos objetos de negócio.

  • Interactor (interface): estes são os boundaries desta camada com o módulo Presentation. Para acessar os Interactors (implementation) será necessário utilizar os contratos definidos por estas interfaces;
  • Interactor (implementation): são as implementações dos contratos definidos como fronteiras deste módulo. Esses são os objetos que possuem as regras de negócio e fornecem as Entities para o Presentation a partir do módulo Infraestruture;
  • Behavior: são os comportamentos compartilhados em múltiplas Entities e Interactors, servindo como ponte para funcionalidades iguais;
  • Repository (interface): estes são os boundaries dessa camada com o módulo Infraestruture. Para acessar os Repositories (implementation) será necessário utilizar os contratos definidos por estas interfaces.
Infraestruture (módulo Android)

Esta é a camada responsável por obter os dados necessários para a aplicação. Todo os dados (seja por uma API Rest nas nuvens ou persistidas em um banco SQLite no dispositivo móvel) são acessados a partir da camada de Repository (a interface é definida na camada de domínio).

É utilizada, então, uma Factory (ou Factory Method) que gera fontes de dados diferentes dependendo de certas condições, como, por exemplo, memória (cache), disco (SQLite) ou nuvem.

A ideia é que a origem dos dados será transparente para o Repository, que não se importa se os dados são provenientes de memória, disco ou nuvem. A única verdade é que os dados vão estar lá.

  • Repository (Implementation): responsável por implementar o contrato definido pelo módulo Domain na Repository (Interface). Esta é a camada responsável por obter os dados de forma transparente, seja eles da memória, disco ou nuvem;
  • Provider Factory: responsável por criar os provedores de dados que serão consumidos pelo Repository (Implementation);
  • Provider (Interface): este é o contrato que será fornecido pelo Factory ao solicitar uma das fontes de dados: memory, cloud ou database;
  • Provider (Memory): responsável pela implementação do Cache em memória, acessado a partir do Repository (Implementation);
  • Provider (Cloud): responsável pela implementação do consumo das APIs, acessado a partir do Repository (Implementation);
  • Provider (Database): responsável pela implementação do consumo do banco de dados (geralmente SQLite ou RealmDB), acessado a partir do Repository (Implementation).

Unindo os módulos

Para unir todos os módulos e fazer a arquitetura funcionar, geralmente utilizo o conceito de Inversão de Controle e Injeção de Dependência. No Android, você tem várias boas alternativas como AndroidAnnotationsDagger e RoboGuice.

Independente do Framework que você escolher, ele será responsável principalmente por injetar os boundaries nos outros módulos e disponibilizar acesso entre as camadas.

Testando

Em relação aos testes, com essa estrutura você fica livre para utilizar diversas soluções. Uma possível solução seria:

  • Presentation: testes de Instrumentation do Android e o Framework Espresso para integração e testes funcionais;
  • Domain: JUnit com Mockito para testes de unidade;
  • Infrastructure: Robolectric (uma vez que esta camada tem dependências Android) com JUnit e Mockito para testes de integração e unidade.

Dúvidas frequentes

  • Cada camada deveria usar seu próprio modelo de dados para alcançar a total independência? Sim, sua linha de pensamento está certa. O motivo de eu não ter explicado isso no tópico principal é que exige alto custo: duplicação de todas as suas entidades ao longo da aplicação (os três módulos). Eu, particularmente, costumo acessar os modelos do domínio nas três camadas e mantenho apenas o Domain livre desta dependência. Isso mantem meu código mais simples e essa dependência na Infraestruture e Presentation nunca causou problemas em nenhum projeto que trabalhei. Minha sugestão é: evite o overengineering. Se for necessário, faça a separação e implemente um Data Mapper para garantir a transformação dos dados.
  • Tratamento de Erros entre os Módulos: este é um tópico que sempre gera ótimas discussões. Minha estratégia geralmente é utilizar Callbacks (com um onSuccess e um onError). O último encapsula exceções que podem ter sido criadas em uma classe Wrapper denominada “Error Bundle”. Esta abordagem traz algumas dificuldades, porque existe uma cadeia de chamadas de retorno um após o outro até que o erro seja exibido na camada de apresentação para ser processado. Por outro lado, eu poderia implementar um sistema de EventBus (por exemplo, o EventBus da GreenRobot) para registrar subscribes e lançar events quando algo de errado acontece. Mas este tipo de solução é como usar GOTO e, na minha opinião, às vezes pode se tornar complexo em projetos maiores nos quais temos vários subcribes e events acontecendo simultaneamente.
  • Shut up and show me the code: infelizmente, eu não tive tempo de desenvolver um código exemplo para colocar com licença aberta neste artigo. Assim que tiver um tempo extra, vou desenvolver um protótipo aberto e disponibilizar um novo artigo explicando detalhadamente. Update: código seguindo o conceito de Clean Architecture disponibilizado no Github, clicando aqui.

Conclusão

Como diria o Uncle Bob: “Architecture is About Intent, not Frameworks”. Claro que existem muitas formas diferentes para fazer as coisas e eu tenho certeza que você enfrenta vários desafios, mas seguindo os tópicos abordados nesta postagem, você garante um código:

  • Fácil de manter;
  • Fácil de testar;
  • Desacoplado.

Se você decidir experimentar a arquitetura proposta aqui, não deixe de compartilhar seus resultados e experiências ou qualquer outra abordagem que encontrou para funcionar melhor. Espero que você tenha achado este artigo útil e qualquer feedback será muito bem-vindo nos comentários.

Até a próxima!

***

Artigo originalmente publicado em: https://www.concrete.com.br/2017/08/02/clean-architecture-para-android/