Antipattern du référentiel sous Android

La traduction de l'article a été préparée en prévision du début du cours «Développeur Android. Professionnel " .








Le guide officiel de l'architecture des applications Android recommande d'utiliser les classes Repository pour «fournir une API propre afin que le reste de l'application puisse facilement récupérer des données». Cependant, à mon avis, si vous utilisez ce modèle dans votre projet, vous êtes assuré de vous enliser dans un code spaghetti en désordre.



Dans cet article, je vais vous parler du "modèle de référentiel" et expliquer pourquoi il s'agit en fait d'un anti-modèle pour les applications Android.



Dépôt



Le Guide d'architecture d'application susmentionné recommande la structure suivante pour organiser la logique du niveau de présentation:







Le rôle de l'objet de référentiel dans cette structure est le suivant:



Les modules de référentiel gèrent les opérations de données. Ils fournissent une API propre afin que le reste de l'application puisse récupérer ces données facilement. Ils savent où obtenir les données et quels appels d'API effectuer lors de leur mise à jour. Vous pouvez considérer les référentiels comme des intermédiaires entre différentes sources de données telles que les modèles persistants, les services Web et les caches.



Fondamentalement, le guide recommande d'utiliser des référentiels pour extraire la source de données dans votre application. Cela semble très raisonnable et même utile, n'est-ce pas?



Cependant, n'oublions pas que bavarder n'est pas jeter des sacs (dans ce cas, écrire du code), mais révéler des sujets d'architecture à l'aide de diagrammes UML - encore plus. Le vrai test de tout modèle architectural est l'implémentation dans le code, puis l'identification de ses avantages et inconvénients. Alors trouvons quelque chose de moins abstrait à revoir.



Référentiel dans les plans d'architecture Android v2



Il y a environ deux ans, j'ai examiné la "première version" des plans d'architecture Android. En théorie, ils étaient censés implémenter un exemple de MVP propre, mais en pratique, ces plans ont abouti à une base de code plutôt sale. Ils contenaient des interfaces nommées View et Presenter, mais ne définissaient aucune limite architecturale, ce n'était donc pas essentiellement un MVP. Vous pouvez voir la révision du code donnée ici .



Depuis lors, Google a mis à jour les plans architecturaux à l'aide de Kotlin, ViewModel et d'autres pratiques «modernes», y compris les référentiels. Ces plans mis à jour ont été préfixés par v2.



Jetons un coup d'œil à l'interface TasksRepository à partir des plans 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)
}


Même avant de lire le code, vous pouvez faire attention à la taille de cette interface - il s'agit déjà d'un appel au réveil. Un tel nombre de méthodes dans une interface soulèverait des questions même dans les grands projets Android, mais nous parlons d'une application ToDo avec seulement 2000 lignes de code. Pourquoi cette application plutôt triviale a-t-elle besoin d'une classe avec une telle surface d'API?



Le référentiel en tant qu'objet de Dieu



La réponse à la question de la section précédente est couverte dans les noms des méthodes TasksRepository. Je peux diviser grossièrement les méthodes de cette interface en trois groupes qui ne se chevauchent pas.



Groupe 1:



fun observeTasks(): LiveData<Result<List<Task>>>
   fun observeTask(taskId: String): LiveData<Result<Task>>


Groupe 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)


Groupe 3:



  suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)


Définissons maintenant les domaines de responsabilité de chacun des groupes ci-dessus.



Le groupe 1 est essentiellement une implémentation du modèle Observer utilisant la fonction LiveData. Le groupe 2 est la passerelle vers la banque de données plus deux méthodes refreshrequises car la banque de données distante est cachée derrière le référentiel. Le groupe 3 contient des méthodes fonctionnelles qui implémentent essentiellement deux parties de la logique du domaine d'application (achèvement et activation des tâches).



Cette interface unique a donc trois responsabilités différentes. Pas étonnant que ce soit si gros. Et bien que l'on puisse affirmer que la présence des premier et deuxième groupes dans le cadre d'une interface unique est acceptable, l'ajout du troisième est injustifié. Si ce projet doit être développé davantage et qu'il devient une véritable application Android, le troisième groupe va croître en proportion directe du nombre de flux de domaine dans le projet. Hmm.



Nous avons un terme spécial pour les classes qui partagent tant de responsabilités: les objets divins. Il s'agit d'un anti-motif courant dans les applications Android. Activitie et Fragment sont des suspects standards dans ce contexte, mais d'autres classes peuvent aussi dégénérer en objets Divins. Surtout si leurs noms se terminent par «Manager», non?



Attendez ... je pense avoir trouvé un meilleur nom pour 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)
}


Désormais, le nom de cette interface reflète bien mieux ses responsabilités!



Dépôts anémiques



Ici, vous pouvez demander: "Si je retire la logique de domaine du référentiel, est-ce que cela résoudra le problème?" Eh bien, revenons au "schéma architectural" du manuel Google.



Si vous completeTaskvouliez extraire, par exemple, des méthodes du TasksRepository, où les placeriez-vous? Selon l '«architecture» recommandée par Google, vous devrez déplacer cette logique dans l'un de vos ViewModels. Cela ne semble pas être une si mauvaise décision, mais c'est vraiment le cas.



Par exemple, imaginez que vous mettez cette logique dans un ViewModel. Ensuite, après un mois, votre responsable de compte souhaite permettre aux utilisateurs d'effectuer des tâches à partir de plusieurs écrans (cela concerne tous les gestionnaires de tâches que j'ai jamais utilisés). La logique à l'intérieur du ViewModel ne peut pas être réutilisée, vous devez donc la dupliquer ou la renvoyer au TasksRepository. De toute évidence, les deux approches sont mauvaises.



Une meilleure approche serait d'extraire ce flux de domaine dans un objet personnalisé, puis de le placer entre le ViewModel et le référentiel. Ensuite, différents ViewModels pourront réutiliser cet objet pour exécuter ce thread particulier. Ces objets sont appelés "cas d'utilisation" ou "interactions"... Cependant, si vous ajoutez des cas d'utilisation à votre base de code, les référentiels deviennent essentiellement un modèle inutile. Quoi qu'ils fassent, cela correspondra mieux aux cas d'utilisation. Gabor Varadi a déjà couvert ce sujet dans cet article , je n'entrerai donc pas dans les détails. Je souscris à presque tout ce qu'il a dit sur les «dépôts anémiques».



Mais pourquoi les cas d'utilisation sont-ils tellement meilleurs que les référentiels? La réponse est simple: les cas d'utilisation encapsulent des flux séparés. Par conséquent, au lieu d'un référentiel (pour chaque concept de domaine) qui se transforme progressivement en un objet Divine, vous aurez plusieurs classes de cas d'utilisation très ciblées. Si le flux dépend du réseau et des données stockées, vous pouvez transmettre les abstractions appropriées à la classe de cas d'utilisation et elle «arbitrera» entre ces sources.



En général, il semble que le seul moyen d'empêcher la dégradation des référentiels en classes Divine tout en évitant les abstractions inutiles est de se débarrasser des référentiels.



Référentiels en dehors d'Android.



Maintenant, vous vous demandez peut-être si les référentiels sont une invention de Google. Non ils ne sont pas. Le modèle de référentiel a été décrit bien avant que Google ne décide de l'utiliser dans son guide d'architecture.



Par exemple, Martin Fowler a décrit les référentiels dans son livre, Patterns of Enterprise Application Architecture. Son blog a également un article invité décrivant le même concept. Selon Fowler, un référentiel n'est qu'un wrapper autour du niveau de stockage qui fournit une interface de requête de niveau supérieur et éventuellement une mise en cache en mémoire. Je dirais que du point de vue de Fowler, les référentiels se comportent comme des ORM.



Eric Evans a également décrit les référentiels dans son livre Domain Driven Design. Il a écrit:



, , , — . , . , , .


Notez que vous pouvez remplacer le "référentiel" dans la citation ci-dessus par "Room ORM" et cela a toujours du sens. Ainsi, dans le contexte de Domain Driven Design, un référentiel est un ORM (implémenté à la main ou à l'aide d'un framework tiers).



Comme vous pouvez le voir, le référentiel n'a pas été inventé dans le monde Android. Il s'agit d'un modèle de conception très sensé sur lequel tous les frameworks ORM sont construits. Notez, cependant, ce que les référentiels ne sont pas: aucun des "classiques" n'a jamais soutenu que les référentiels devraient essayer d'abstraire la distinction entre l'accès au réseau et l'accès à la base de données.



En fait, je suis presque sûr qu'ils trouveront cette idée naïve et autodestructrice. Pour comprendre pourquoi, vous pouvez lire un autre article, cette fois de Joel Spolsky (fondateur de StackOverflow), intitulé"La loi des abstractions qui fuient . " Pour faire simple: le réseautage est trop différent de l'accès à la base de données pour être abstrait sans fuites significatives.



Comment le référentiel est devenu anti-pattern dans Android



Google a-t-il donc mal interprété le modèle de référentiel et introduit l'idée naïve de l'abstraction de l'accès au réseau? J'en doute.



J'ai trouvé le lien le plus ancien vers cet anti-modèle dans ce référentiel GitHub , qui est malheureusement une ressource très populaire. Je ne sais pas si cet auteur en particulier a inventé cet anti-modèle, mais il semble que ce soit ce repo qui ait popularisé l'idée générale au sein de l'écosystème Android. Les développeurs de Google l'ont probablement obtenu à partir de là ou de l'une des sources secondaires.



Conclusion



Ainsi, le référentiel sous Android est devenu un anti-pattern. Cela a l'air bien sur le papier, mais cela devient problématique même dans des applications triviales et peut conduire à de réels problèmes dans des projets plus importants.



Par exemple, dans un autre plan de Google, cette fois pour les composants architecturaux, l'utilisation de référentiels a finalement conduit à des gemmes telles que NetworkBoundResource . Gardez à l'esprit que l'exemple de navigateur GitHub est toujours une petite application ~ 2 KLOC.



Pour autant que je sache, le "modèle de référentiel" tel que défini dans la documentation officielle est incompatible avec un code propre et maintenable.



Merci d'avoir lu et comme d'habitude vous pouvez laisser vos commentaires et questions ci-dessous.






All Articles