Conception Côté Client (Partie 1) : MVC, MVP, MVVM et Cohésion/Couplage

La conception de l'architecture côté client repose sur des principes fondamentaux pour organiser le code et garantir la maintenabilité. Cet article explore les patterns d'architecture MVC, MVP et MVVM, ainsi que les concepts de haute cohésion et de faible couplage.

8.1 MVC / MVP / MVVM

Les trois architectures partagent des couches Model (sources de données comme les requêtes réseau, le cache, les objets Bean) et View (composants UI tels que Activity/Fragment, View). La différence principale réside dans la couche Controller / Presenter / ViewModel.

La distinction fondamentale entre ces architectures concerne : qui gère la logique, si la View et le Model peuvent communiquer directement, et le degré de couplage entre la View et la couche métier. Le choix d'une architecture détermine le "système de division du travail" du code, et pas seulement la manière dont l'UI est rafraîchie.

8.1.1 Différences


MVC:  View ──→ Controller ──→ Model
            View ←── Model (observation directe)

MVP:  View ←──→ Presenter ←──→ Model
     (View et Model sont totalement isolés ; le Presenter fait office d'intermédiaire)

MVVM: View ←──→ ViewModel ←──→ Model
     (La View observe le ViewModel via la liaison de données ; le ViewModel n'a pas connaissance de la View)

Critère MVC MVP MVVM
Interaction View et Model Possible Totalement isolés Totalement isolés
Contrôle de la logique Controller Presenter ViewModel
Implémentation Souvent dans la View, parfois avec le pattern Observer ; la View met à jour l'UI directement sur callback du Model. Le Presenter détient la View ; appelle view.xxx() sur callback du Model ; la View met à jour l'UI dans la méthode de rappel. Souvent avec LiveData ou Pattern Observer ; le ViewModel ne détient pas la View, seulement les données ; la View s'abonne aux changements de données pour se rafraîchir.
Logique métier Dans M ou V, ou Controller Dans P Dans VM, capable de logique métier complexe
Responsabilité de la View Déclencher les opérations métier, recevoir les changements de données, déclencher les mises à jour UI, potentiellement gérer la logique métier. Implémenter les interfaces du Presenter, y mettre à jour l'UI. Transmettre les opérations métier au VM, écouter les changements de données du VM pour rafraîchir l'UI.
Couplage View / Couche métier Élevé Moyen (la View implémente une interface, le Presenter dépend de cette interface) Faible (la View n'implémente aucune interface spécifique, elle s'abonne simplement à des observateurs ou utilise DataBinding)
Communication View appelle Controller, Model notifie View View appelle l'interface Presenter, Presenter rappelle l'interface View View observe LiveData/Flow du ViewModel
Testabilité Faible (Controller lié à la View) Bonne (Presenter est une logique pure) Excellente (ViewModel sans dépendance UI)

Différence d'observation entre MVC et MVVM : Dans MVC, la View détient directement l'écouteur de données et les callbacks du Model. Dans MVVM, la View n'a pas le Model, elle écoute les changements de données du ViewModel. Le ViewModel n'est pas conscient de l'existence de la View et peut être réutilisé pour n'importe quelle View concernée.

8.1.2 Est-ce que cela n'affecte que le rafraîchissement de l'UI ?

Bien plus que cela. Le choix détermine la manière dont le code est organisé et les responsabilités sont réparties :

Dimension d'impact MVC MVP MVVM
Emplacement de la logique métier Dans le Controller, risque de devenir un "Massive View Controller" Dans le Presenter, responsabilités claires Dans le ViewModel, naturellement testable, peut gérer une logique métier complexe
Gestion de l'état Model et View gèrent chacun de leur côté, risque d'incohérence Presenter gère uniformément, mais nombreux callbacks ViewModel est la source unique d'état, piloté par les données
Testabilité Faible, logique liée à l'UI Bonne, il suffit de mocker l'interface View Excellente, ViewModel sans dépendance UI
Quantité de code Moins, mais désordonné Plus, avec une couche d'interfaces Modérée, la liaison de données économise du code
Extensibilité Ajouter une fonctionnalité modifie le Controller, qui grossit Ajouter une fonctionnalité ajoute une méthode au Presenter Ajouter une fonctionnalité ajoute un état et une logique au ViewModel
Cycle de vie La View peut obtenir des données obsolètes directement du Model Presenter gère manuellement le cycle de vie ViewModel gère automatiquement le cycle de vie, LiveData est sensible au cycle de vie
Collaboration d'équipe Front et back mélangés Front gère la View, back gère le Presenter Front gère la View et la liaison, back gère le ViewModel

En résumé : choisir MVC/MVP/MVVM détermine le "système de division du travail" du code, pas seulement la manière dont l'UI est rafraîchie.

8.1.3 Inconvénients de chaque pattern

Inconvénients de MVC- Controller : Le Controller gère la logique, la coordination et les transitions. La View peut contenir des milliers de lignes de code UI, métier et de mise à jour de données, rendant le débogage et la maintenance difficiles.

  • Couplage View-Model : La View peut accéder directement au Model, rendant les modifications du Model impactantes pour la View.
  • Difficulté de test : La logique est dans le Controller, qui est étroitement lié à la View, empêchant les tests unitaires.
  • Scénarios d'utilisation : Pages simples, prototypes rapides, pages d'affichage avec une logique métier minimale.

Inconvénients de MVP- Explosion d'interfaces : Il faut définir une interface View pour chaque page. Avec de nombreuses méthodes (showLoading, showError, showData...), l'interface devient volumineuse.

  • Enfer des callbacks : Le Presenter appelle le Model de manière asynchrone, et doit rappeler la View. Avec une longue chaîne d'appels, cela devient complexe.
  • Gestion manuelle du cycle de vie : Le Presenter ne sait pas si la View est toujours active, nécessitant une vérification manuelle (isAttached).
  • Code répétitif : Chaque page nécessite une interface View, une implémentation Presenter et un Contrat, entraînant un travail répétitif.
  • Scénarios d'utilisation : Projets Java, collaboration d'équipe nécesssitant des contrats clairs, pages avec une logique UI complexe.

Inconvénients de MVVM- Débogage difficile de la liaison de données : Il est difficile de savoir quel LiveData a déclenché un changement d'UI, le chemin de débogage n'est pas intuitif.

  • Inflation de l'état : Le ViewModel peut contenir une douzaine de LiveData, chacun gérant un état d'UI, ce qui peut devenir désordonné avec le temps.
  • Courbe d'apprentissage : La liaison de données, les coroutines et Flow ont une courbe d'apprentissage plus raide que les callbacks.
  • Sur-réactivité : Un changement d'état peut déclencher trois observateurs, qui à leur tour déclenchent deux autres, entraînant des éventails difficiles à suivre.
  • Pièges des flux de données complexes : Dans les grands projets avec des niveaux de données multiples et une logique métier complexe, des problèmes comme l'échec de l'écoute des données, l'échec de la mise à jour des données ou des actualisations d'UI fréquentes mais sans changement réel des données peuvent survenir.
  • Augmentation des niveaux et des classes : Une petite fonctionnalité peut nécessiter seulement M+V en MVC, mais un MVVM standard nécessite M+V+VM.
  • ViewModel potentiellement très volumineux : Nécessite une décomposition.
  • Scénarios d'utilisation : Projets Kotlin, UI déclarative, nécessité d'une haute testabilité.

8.1.4 Comment choisir

Scénario Choix
Architecture existante de l'équipe Suivre l'architecture existante
Fonctionnalités simples, démos MVC ou MVP suffisent
Grands projets Si les conditions le permettent, utiliser MVVM, mais il faut bien gérer les scénarios complexes de mise à jour des données.

Ne pas architecturer pour le plaisir d'architecturer : Pour une petite fonctionnalité, forcer MVVM et écrire la logique d'une page en trois classes n'est pas nécessaire.

8.2 Haute Cohésion, Faible Couplage

8.2.1 Pourquoi la haute cohésion et le faible couplage ?

Cohésion : Les éléments au sein d'un module sont-ils étroitement liés ?

Couplage : Les modules dépendent-ils les uns des autres ?


Haute cohésion, faible couplage → Modifier un module n'affecte que lui-même, pas les autres.

Faible cohésion, fort couplage → Modifier une ligne entraîne la modification de dix classes, engendrant des bugs insolubles.

L'objectif principal du découplage est d'isoler les changements et d'améliorer la réutilisabilité.

Pourquoi découpler :

  • Éviter les références circulaires
  • Créer des bibliothèques de composants communes et réutilisables, évitant la duplication et facilitant la maintenance
  • Isoler une fonctionnalité/un service pour une utilisation par plusieurs modules
  • Répondre aux besoins réels de l'entreprise, faciliter le développement collaboratif
  • Éviter qu'une seule classe ne devienen trop volumineuse
  • Faciliter la localisation des problèmes – lorsqu'un problème survient, il faut pouvoir localiser approximativement la source : capacité de base ou capacité métier ? Couche de données ? Couche d'opération utilisateur ?

Exemple :

Haute Cohésion Faible Cohésion
BadgeManager Gère uniquement l'affichage, le calcul et le cache des indicateurs de non-lu. Gère les indicateurs de non-lu + la commutation d'onglets + le préchargement + le skin.
Modifier les indicateurs de non-lu n'affecte que BadgeManager. Modifier le préchargement nécessite de modifier BadgeManager ?
Faible Couplage Fort Couplage
BadgeManager Appelle le SDK marketing via une interface. Appelle directement new MarketingSDKImpl().
Changer de SDK ne nécessite que de modifier la classse d'implémentation. Changer de SDK nécessite de modifier BadgeManager.

8.2.2 Scénarios de découplage

Découplage de modules/composants Dans les grands projets avec de nombreuses fonctionnalités et une collaboration multi-développeurs, la modularisation et la composabilité sont essentielles pour le découplage – projet "coque", séparation front-back, compilation et exécution indépendantes.
Découplage des services de base et des capacités métier Distinguer les besoins métier de l'équipe produit et les capacités de base. Les classes de service servent de fournisseurs de fonctionnalités de base, potentiellement utilisées par plusieurs activités métier, et doivent donc être découplées.

Piège : Une capacité de base, pour répondre à un besoin métier, appelle directement l'interface de tracking d'une autre activité métier. Lorsque cette capacité de base est utilisée par une autre activité, la logique de tracking de cette dernière est déclenchée à tort. C'est un piège courant pour les fournisseurs de capacités de base.

Solution : Les capacités de base ne fournissent que des points d'extension (interfaces de rappel, écouteurs), et l'implémentation concrète est injectée par la couche métier supérieure. Cela suit le principe de l'inversion de dépendance.

Exemples : scan de données, lecteur vidéo, service de session, service de skin, capacités marketing, SDK de lecteur vidéo, capacités de base de rendu...

Découplage par couches Découplage des interactions utilisateur, des données, et du rafraîchissement de l'UI (courant). Cela évite de placer la logique métier dans l'interface utilisateur, ce qui peut entraîner une duplication de code.

Quand extraire la couche de données :

  • Les données doivent être partagées entre plusieurs écrans/composants (par exemple, l'état de connexion de l'utilisateur).
  • Les données nécessitent une transformation, une agrégation ou une mise en cache complexe (par exemple, agréger des données provenant de plusieurs interfaces).
  • La logique métier doit être modifiée fréquemment et nécessiter une couverture de tests complète.

Dans ces cas, passer de MVC/MVP à MVVM et extraire la couche de données ainsi que la couche d'interaction utilisateur est approprié.

Découplage des interfaces/paramètres Pour les activités métier complexes ou celles qui nécessitent une précision élevée, il est nécessaire de décomposer les grandes interfaces en interfaces plus petites.

Exemple : Un écran affiche des dizaines de données. Selon les interactions de l'utilisateur et les règles métier, à un moment donné, seule une partie des données doit être mise à jour, sans rafraîchir les autres parties (pour éviter le clignotement). Dans ce cas, il faut des interfaces faiblement couplées pour chaque source d'information.

Pourquoi ne pas utiliser le diff ?

  1. Les opérations de diff courantes peuvent être coûteuses en temps, surtout sur les appareils bas de gamme.
  2. Lorsque la complexité métier est très élevée, la logique de diff intégrée au code métier peut être peu élégante, et cette logique devrait idéalement être gérée par les capacités de base.
Quand découpler vs quand ne pas découpler Le jugement doit être basé sur les besoins métier spécifiques et le contexte. Lors du découplage, il est préférable de suivre le style de conception global, les capacités existantes de l'entreprise, les capacités du langage/framework et les styles de conception courants. Les fonctionnalités simples ne nécessitent pas de découplage – cela introduirait plutôt une surcharge de paquets, de bibliothèques et de classes, rendant la maintenance difficile.

Signaux indiquant qu'un découplage est nécessaire :

Signal Explication
La même fonctionnalité est répétée à plusieurs endroits Extraire une bibliothèque commune
Une classe/un module est fréquemment modifié Responsabilité non unique, nécessite une décomposition
La modification d'un endroit entraîne des problèmes dans plusieurs autres endroits Couplage trop serré
Conflits fréquents lors de la modification du même fichier par plusieurs personnes Nécessité de modularisation
Impossible d'écrire des tests unitaires Trop de dépendances, nécessite un Mock

Signaux indiquant qu'un découplage n'est pas nécessaire :

Signal Explication
Fonctionnalité simple et stable Le découplage introduit de la complexité, ce qui n'en vaut pas la peine.
Utilisation unique Pas besoin d'extraire une bibliothèque commune.
Augmentation significative du nombre d'interfaces/callbacks après découplage Quantité de code doublée, difficile à maintenir.
Petite équipe, fonctionnalité simple Sur-conception.

Critère : Le bénéfice du découplage doit être supérieur au coût du découplage.

8.2.3 Métriques de découplage

Comment savoir si le couplage est élevé ? Voici quelques métriques :

Métrique Explication Valeur idéale
Lignes de code Complexité d'une méthode < 80 lignes
Nombre de lignes de code d'une classe < 1000 lignes
Afflux (Fan-in) Combien de modules dépendent de ce module Afflux élevé = beaucoup de réutilisation, c'est bien
Débordement (Fan-out) Combien de modules ce module dépend Débordement élevé = couplage fort, à contrôler
Portée des modifications Combien d'autres éléments sont affectés lors de la modification d'un module Moins c'est mieux
Dépendances Combien de processus métier en dépendent ≥ 2 processus métier
Dépendances circulaires A → B → A L'apparition indique une absence d'architecture
Collaboration Combien de personnes développent simultanément ≥ 2 personnes

Valeurs expérimentales :

  • Soyez prudent lorsque le débordement est manifestement élevé, cela peut indiquer qu'il est temps de décomposer.
  • La modification d'une classe affectant plus de 3 autres classes suggère que la responsabilité n'est pas unique.
  • Une dépendance circulaire doit être résolue immédiatement.

8.2.4 Mesures de découplage (par niveau)

Niveau Objet du découplage Méthode Exemple
Méthode Signification des paramètres de méthode Décomposer en différentes méthodes, paramètres différents, scénarios d'énumération Trop de paramètres nécessitent de décomposer la méthode
Code Entre classes Paramètres, énumérations, interfaces, inversion de dépendance, pattern Observer, broadcast A dépend de l'interface de B, pas de l'implémentation concrète
Module Entre modules Composants, modularisation, routage Le module A ne dépend pas directement du module B, il navigue via le routeur
Processus Entre processus AIDL, Binder, ContentProvider, Service + Pattern Façade Le lecteur de musique dans un processus séparé, le SDK
Application Entre applications Intent implicite, Broadcast Appeler WeChat pour le partage
Couche métier Métier et capacités de base Généralement divisé en base, common, xxx_module/composant métier Les capacités de base ne fournissent que des points d'extension
Couche de code UI, interaction, logique, source de données MVVM, MVP, MVC Séparation de la couche de données et de la couche UI

Le Pattern Façade peut rendre les interfaces découplées plus claires – l'appelant sait quelles fonctionnalités sont disponibles sans connaître les détails internes. Cela s'applique aux SDK, aux interfaces exposées par les ViewModel en MVVM, aux couches abstraites de l'inversion de dépendance, et au découplage des ressources.

Étiquettes: architecture mvc mvp MVVM conception logicielle

Publié le 10 juin à 17h24