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 ?
- Les opérations de diff courantes peuvent être coûteuses en temps, surtout sur les appareils bas de gamme.
- 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.