“Clean Architecture” para Android

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 as diversas mudanças durante seu crescimento. Para alcançar esse resultado os desenvolvedores recorrem a diversas práticas como Clean Architecture, Domain-Driven Design, Model-View-Controller, Model-View-Presenter, Design Patterns, entre diversas outras boas práticas.

Não, não tem motivos para essa imagem estar aqui. Ela está aqui porque eu gosto de gatos.

A ideia por trás de todas elas é simples. Um conjunto de práticas que tem como proposito 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 nesta postagem não é explicar a teoria sobre a Clean Architecture, mas mostrar como eu costumo implementar a arquitetura dos softwares (quando há tempo, claro) baseados em conceitos como o Clean Architecture, Domain-Driven Design e Model-View-Controller.

OverEngineering (excesso de engenharia) é utilizar mais do que o necessário no desenvolvimento de um software, tornando ele 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 arquitetura.

O que é Clean Architecture?

Proposto por Robert Cecil Martin (muito conhecido como Uncle Bob) em 13 de Agosto de 2012, 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 principio da responsabilidade única, separando o interesse de cada módulo e mantendo 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), onde 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.
  • Infraestruture (módulo Android): responsável pelo banco de dados, acesso a 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 estarão disponíveis em sua devida camada, onde será definido o contrato para que esta possa ser acessada.

Presentation (módulo Android)

Esta é 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. É este o local onde 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ífica de sua aplicação. Ela incorpora e implementa todos os casos de uso do sistema e muitos preferem chamar 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 proposito 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. Estes 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 desta 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 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).

É utilizado 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: AndroidAnnotations, Dagger 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 esta estrutura você fica livre para utilizar diversas soluções ao qual se sentir mais confortável. 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.
  • Infraestruture: 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 esta certa. O motivo de eu não ter explicado isso no tópico principal é que isso exige um 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 dependencia 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 estrategia geralmente é utilizar Callbacks (geralmente 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 onde 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 eu tiver um tempo extra, vou desenvolver um protótipo aberto e disponibilizar um novo artigo explicando ele 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!