O Guia oficial de arquitetura de aplicativos Android recomenda o uso das classes Repositório para "fornecer uma API limpa para que o restante do aplicativo possa recuperar dados facilmente". No entanto, em minha opinião, se você usar esse padrão em seu projeto, certamente ficará atolado em um código espaguete confuso.
Neste artigo, falarei sobre o "padrão de repositório" e explicarei por que ele é, na verdade, um antipadrão para aplicativos Android.
Repositório
O Guia de Arquitetura de Aplicativos mencionado anteriormente recomenda a seguinte estrutura para organizar a lógica da camada de apresentação:
A função do objeto de repositório nesta estrutura é a seguinte:
Os módulos de repositório tratam das operações de dados. Eles fornecem uma API limpa para que o restante do aplicativo possa recuperar esses dados facilmente. Eles sabem onde obter os dados e quais chamadas de API devem ser feitas quando forem atualizados. Você pode pensar nos repositórios como intermediários entre diferentes fontes de dados, como modelos persistentes, serviços da web e caches.
Basicamente, o guia recomenda o uso de repositórios para abstrair a fonte de dados em seu aplicativo. Parece muito razoável e até útil, não é?
No entanto, não vamos esquecer que conversar não é jogar sacos (neste caso, escrever código), mas revelar tópicos de arquitetura usando diagramas UML - mais ainda. O verdadeiro teste de qualquer padrão de arquitetura é a implementação no código e a identificação de suas vantagens e desvantagens. Então, vamos encontrar algo menos abstrato para revisar.
Repositório no Android Architecture Blueprints v2
Há cerca de dois anos, analisei a "primeira versão" do Android Architecture Blueprints. Em teoria, eles deveriam implementar um exemplo de MVP limpo, mas, na prática, esses projetos resultaram em uma base de código bastante suja. Eles continham interfaces denominadas View e Presenter, mas não definiam nenhum limite de arquitetura, portanto, não era essencialmente um MVP. Você pode ver a revisão do código fornecido aqui .
Desde então, o Google atualizou projetos arquitetônicos usando Kotlin, ViewModel e outras práticas "modernas", incluindo repositórios. Esses blueprints atualizados foram prefixados com v2.
Vamos dar uma olhada na interface TasksRepository dos blueprints da v2:
interface TasksRepository {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
Antes mesmo de ler o código, você pode prestar atenção no tamanho dessa interface - isso já é um alerta. Muitos métodos em uma interface levantariam questões até mesmo em grandes projetos Android, mas estamos falando de um aplicativo ToDo com apenas 2.000 linhas de código. Por que esse aplicativo bastante trivial precisa de uma classe com uma superfície de API tão grande?
Repositório como um objeto divino
A resposta à pergunta da seção anterior é abordada nos nomes dos métodos TasksRepository. Posso dividir aproximadamente os métodos dessa interface em três grupos não sobrepostos.
Grupo 1:
fun observeTasks(): LiveData<Result<List<Task>>>
fun observeTask(taskId: String): LiveData<Result<Task>>
Grupo 2:
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
Grupo 3:
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
Agora vamos definir as áreas de responsabilidade de cada um dos grupos acima.
O Grupo 1 é basicamente uma implementação do padrão Observer usando o recurso LiveData. O Grupo 2 é o gateway para o armazenamento de dados mais dois métodos
refreshque são necessários porque o armazenamento de dados remoto está oculto atrás do repositório. O Grupo 3 contém métodos funcionais que basicamente implementam duas partes da lógica do domínio do aplicativo (conclusão e ativação da tarefa).
Portanto, essa interface tem três responsabilidades diferentes. Não admira que seja tão grande. E embora possa ser argumentado que a presença do primeiro e do segundo grupos como parte de uma única interface é aceitável, adicionar o terceiro não se justifica. Se este projeto precisar ser mais desenvolvido e se tornar um aplicativo Android real, o terceiro grupo crescerá em proporção direta ao número de fluxos de domínio no projeto. Hmm.
Temos um termo especial para classes que compartilham tantas responsabilidades: Objetos Divinos. Este é um antipadrão comum em aplicativos Android. Activitie e Fragment são suspeitos padrão neste contexto, mas outras classes podem degenerar em objetos Divinos também. Principalmente se seus nomes terminarem em “Gerente”, certo?
Espere ... Acho que encontrei um nome melhor para TasksRepository:
interface TasksManager {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
Agora o nome desta interface reflete suas responsabilidades muito melhor!
Repositórios anêmicos
Aqui você pode perguntar: "Se eu retirar a lógica do domínio do repositório, isso resolverá o problema?" Bem, de volta ao "diagrama arquitetônico" do manual do Google.
Se você
completeTaskquisesse extrair, digamos, métodos do TasksRepository, onde os colocaria? De acordo com a "arquitetura" recomendada pelo Google, você precisará mover essa lógica para um de seus ViewModels. Não parece uma decisão tão ruim, mas realmente é.
Por exemplo, imagine que você está colocando essa lógica em um ViewModel. Então, depois de um mês, seu gerente de conta deseja permitir que os usuários concluam tarefas em várias telas (isso é relevante para todos os gerentes de tarefas que já usei). A lógica dentro do ViewModel não pode ser reutilizada, então você precisa duplicá-la ou devolvê-la ao TasksRepository. Obviamente, ambas as abordagens são ruins.
Uma abordagem melhor seria extrair esse fluxo de domínio em um objeto personalizado e, em seguida, colocá-lo entre o ViewModel e o repositório. Então, diferentes ViewModels serão capazes de reutilizar aquele objeto para executar aquela thread em particular. Esses objetos são conhecidos como "casos de uso" ou "interações"... No entanto, se você adicionar casos de uso à sua base de código, os repositórios se tornarão essencialmente um modelo inútil. O que quer que eles façam, será mais adequado aos casos de uso. Gabor Varadi já cobriu esse tópico neste artigo , então não vou entrar em detalhes. Eu subscrevo quase tudo o que ele disse sobre "repositórios anêmicos".
Mas por que os casos de uso são muito melhores do que os repositórios? A resposta é simples: os casos de uso encapsulam fluxos separados. Portanto, em vez de um repositório (para cada conceito de domínio) que gradualmente se transforma em um objeto Divino, você terá várias classes de caso de uso altamente focadas. Se o fluxo depende da rede e dos dados que estão sendo armazenados, você pode passar as abstrações apropriadas para a classe de caso de uso e ela irá "arbitrar" entre essas fontes.
Em geral, parece que a única maneira de evitar a degradação dos repositórios para classes Divine, evitando abstrações desnecessárias, é livrar-se dos repositórios.
Repositórios fora do Android.
Agora você deve estar se perguntando se os repositórios são uma invenção do Google. Não, eles não são. O padrão de repositório foi descrito muito antes de o Google decidir usá-lo em seu guia de arquitetura.
Por exemplo, Martin Fowler descreveu repositórios em seu livro, Patterns of Enterprise Application Architecture. Seu blog também tem um artigo convidado que descreve o mesmo conceito. De acordo com Fowler, um repositório é apenas um invólucro em torno da camada de armazenamento que fornece uma interface de consulta de nível superior e, possivelmente, armazenamento em cache na memória. Eu diria que, do ponto de vista de Fowler, os repositórios se comportam como ORMs.
Eric Evans também descreveu repositórios em seu livro Domain Driven Design. Ele escreveu:
, , , — . , . , , .
Observe que você pode substituir o "repositório" na citação acima por "ORM da sala" e ainda faz sentido. Portanto, no contexto do Domain Driven Design, um repositório é um ORM (implementado manualmente ou usando uma estrutura de terceiros).
Como você pode ver, o repositório não foi inventado no mundo Android. Este é um padrão de design muito lógico no qual todas as estruturas ORM são construídas. Observe, entretanto, o que os repositórios não são: nenhum dos "clássicos" jamais argumentou que os repositórios deveriam tentar abstrair a distinção entre acesso à rede e acesso ao banco de dados.
Na verdade, tenho certeza de que eles acharão essa ideia ingênua e contraproducente. Para entender por que, você pode ler outro artigo, desta vez de Joel Spolsky (fundador do StackOverflow), intitulado"A lei das abstrações vazadas . " Simplificando: a rede é muito diferente do acesso ao banco de dados para abstrair sem vazamentos significativos.
Como o repositório se tornou anti-padrão no Android
Então, o Google interpretou mal o padrão de repositório e introduziu a ideia ingênua de abstrair o acesso à rede nele? Eu duvido.
Encontrei o link mais antigo para este antipadrão neste repositório GitHub , que infelizmente é um recurso muito popular. Não sei se esse antipadrão foi inventado por esse autor em particular, mas parece que foi esse repo que popularizou a ideia geral dentro do ecossistema Android. Os desenvolvedores do Google provavelmente conseguiram isso de lá ou de uma das fontes secundárias.
Conclusão
Portanto, o repositório no Android se tornou um antipadrão. Parece bom no papel, mas se torna problemático mesmo em aplicativos triviais e pode levar a problemas reais em projetos maiores.
Por exemplo, em outro projeto do Google, desta vez para componentes arquitetônicos, o uso de repositórios acabou gerando joias como NetworkBoundResource . Lembre-se de que o navegador de amostra GitHub ainda é um aplicativo KLOC minúsculo de ~ 2.
Pelo que eu posso dizer, o "padrão de repositório" conforme definido nos documentos oficiais é incompatível com código limpo e sustentável.
Obrigado pela leitura e como de costume você pode deixar seus comentários e perguntas abaixo.
