Accueil Nos publications Blog Introduction à l’architecture MVVM sur iOS

Introduction à l’architecture MVVM sur iOS

MVVM_couvBeaucoup d’entre vous se sont déjà familiarisés avec les concepts du modèle MVC à la façon Cocoa. C’est le modèle de base qu’Apple encourage et nous pousse à adopter dans sa documentation. La maîtrise du MVC, le plus connu et le plus utilisé des patrons de conception, vous fera gagner beaucoup de temps et vous garantira une meilleure réutilisabilité de votre code. Vous pouvez avoir plus de détails sur l’architecture MVC en iOS en suivant ce lien.

Par ailleurs, MVC peut s’avérer disproportionné pour certains contextes. Au cours de ma mission actuelle, j’ai été amené à développer en TDD (Test Driven Development) et j’ai rencontré beaucoup de difficultés à tester mes UIViewControllers et ce, même en essayant de les alléger et de déléguer beaucoup de fonctionnalités à des classes de services. C’est ce qui m’a poussé à adopter une nouvelle structuration de mon code : le Modèle MVVM.

Le modèle MVVM, communément appelé Model-View-ViewModel, est un modèle de conception de l’interface utilisateur (UI). Il fait partie d’une grande famille de modèles connus collectivement par le préfixe MV, comme Model View Controller (MVC) et Model View Presenter (MVP). Chacun de ces modèles a pour but de séparer la logique métier de l’interface utilisateur afin de rendre les applications beaucoup plus faciles à maintenir et surtout à tester.

Qu’est-ce que le modèle MVVM ? Pourquoi avoir recours à ce patron de conception ? MVC n’est-il plus adapté aux nouvelles exigences du marché ? Quelle est sa valeur ajoutée ?
Dans cet article, nous allons voir ensemble quelles sont les motivations pour avoir recours à MVVM et passer en revue les différences entre les deux patterns.

Motivation pour utiliser le pattern MVVM

Les technologies de développement de nos jours essayent de faciliter au mieux la vie des développeurs, en leur offrant des environnements de développement intuitifs et ergonomiques. C’est le cas notamment de Xcode, qui offre une expérience par défaut qui conduit le développeur sur un chemin de paresseux qui peut être nocif. Par un simple “drag and drop”, on peut facilement créer notre interface utilisateur et lier ses éléments à des événements qu’on aurait implémentés dans la même classe, et c’est justement là qu’on commence à s’emmêler les pinceaux.

Au fur et à mesure que les applications croissent en taille et en portée, les problèmes complexes de maintenance commencent à surgir. Ces problèmes comprennent le couplage impérieux entre la couche métier et les composants graphiques (GUI), ce qui majore le coût pour effectuer des modifications de l’interface utilisateur et complexifie la possibilité de faire des tests unitaires ou même fonctionnels.

thinkingJe ne sais pas pour vous mais, au cours de mon expérience, j’ai connu des moments de doute en rapport avec le code que j’avais produit. J’avais l’impression que je n’étais pas en train de structurer mon application. Pourtant, je me pliais complètement à ce qu’Apple ne cesse d’encourager : appliquer MVC. Et je m’apercevais qu’après un bon moment, je n’arrivais plus à me situer dans mon propre code. Peut-être parce que j’étais pressé par le temps et que j’entassais toutes les fonctionnalités dans le même UIViewController ? Peut-être aussi que je n’avais pas vraiment compris la vision MVC d’Apple ? Pourtant je téléchargeais leur code et je regardais leurs vidéos. Il y avait quelque chose qui m’échappais !

Dès lors, j’ai commencé à appréhender de nouvelles méthodes de structuration. J’ai essayé d’alléger les UIViewControllers, de déléguer des fonctionnalités à des classes à part entière. Mais même avec ça, je me retrouvais encore dans la difficulté de standardiser ces classes de délégation pour pouvoir les réutiliser ailleurs. Et ce parce que, tout simplement, elles dépendaient du contexte de l’application.

Ensuite, j’ai commencé à travailler pour un client qui exigeait de la qualimétrie. J’ai été amené à intensifier l’écriture de mes tests unitaires. Et là, je me suis rendu compte qu’il fallait absolument que je minimise la granularité de mes méthodes et que je sépare davantage les responsabilités. C’est à partir de là que j’ai commencé à m’intéresser aux techniques de développement pilotées par les tests (TDD). Et le meilleur moyen de produire un code testable serait, vraisemblablement, d’appliquer MVVM.

Le pattern MVVM

Pour mieux comprendre le modèle MVVM, il est nécessaire de revenir aux origines. MVC a été le premier modèle de conception de l’interface utilisateur. Ses origines reviennent au langage Smalltalk des années 70. L’image ci-dessous illustre les principales composantes du modèle MVC :

MVC_interactions

Le modèle MVC, comme vous le savez, vise à écarter les données et leur vie (Modèle) de leur présentation (Vue) par l’intermédiaire d’un gestionnaire d’événements (Contrôleur). L’un des grands problèmes de ce patron de conception est qu’il prête à confusion, surtout en développement iOS. Les concepts sont très clairs, mais souvent quand on se met à l’implémenter, avec les techniques et les moyens mis à disposition par le langage, on fait souvent l’amalgame dans un grand désordre.

Martin Fowler, un consultant britannique dans la conception de logiciels d’entreprise et conférencier renommé, a eu l’ingénieuse idée de présenter une nouvelle variation du modèle MVC baptisé Presentation Model. Elle a été adoptée et publiée par Microsoft sous le nom MVVM. L’objectif principal de ce modèle est de séparer davantage les responsabilités, et surtout la logique de présentation. Dans une application typique MVC, on a tendance à placer tout ce qui est en rapport avec la logique de présentation dans des UIViewControllers. Par exemple, formater une date en provenance d’un modèle pour la présenter à l’utilisateur, ou encore effectuer un traitement sur une chaîne de caractères de type adresse pour en extraire chaque partie. C’est légitime, on ne peut pas mettre ce genre de traitement dans des classes modèles ou des vues.

Pareillement, le code qui exploite les web services est généralement placé dans des contrôleurs. Enfin, vous pouvez essayer d’alléger un peu votre contrôleur et exporter ce genre de traitement dans des classes modèles. En revanche, vous serez confrontés à divers problèmes de cycle de vie des différents objets que vous allez instancier, surtout quand les appels réseaux sont asynchrones.

C’est là justement que le besoin d’une architecture comme MVVM s’est fait sentir. MVVM vise à exporter les traitements de la logique de présentation dans des entités à part entière qui s’appellent les ViewModels, et qui seront placées entre la Vue/Contrôleur (qui désormais constitue une seule entité) et le modèle :

MVVM_interactions

Ça ne vous rappelle pas un autre diagramme ? Remontez un tout petit peu en haut… voilà ! En fait, MVVM n’est autre qu’une version reformulée de MVC. C’est pourquoi, vous pouvez intégrer facilement cette architecture dans une application du type MVC. Globalement, nous avons dépourvu le contrôleur de l’intelligence qu’il avait. Et nous avons introduit une nouvelle entité (ViewModel) qui va s’occuper de toute la logique UI.

D’accord… et qui fait quoi ?

Le modèle (MVVM) : Le modèle reste le même que celui de MVC. C’est la couche qui décrit et qui se porte responsable de l’intégrité des données manipulées par l’application. L’interaction avec la base de données serait l’une de ses responsabilités. Selon l’envergure du projet, et selon ce que vous avez l’habitude de faire, les modèles peuvent ou non encapsuler certaines responsabilités logiques en plus. Personnellement, je tends à les utiliser plus comme une construction pour contenir des informations représentant un modèle de données. Et je garde toute la logique métier dans une classe séparée du type gestionnaire, généralement en utilisant des catégories.

La vue/contrôleur (MVVM) : Cette couche représente le contexte de l’interface utilisateur et ses interactions. Désormais, nous considérons les vues et leurs contrôleurs comme une seule entité : UIViewController. Tiens ça tombe bien, même son nom lui va mieux dans cette architecture (UIView-Controller). Chaque vue est couplée avec un ViewModel. En l’occurrence, non seulement les vues n’ont pas de référence vers le modèle, mais les contrôleurs non plus. Le seul vis-à-vis qui garantira la connexion entre les vues et leurs modèles c’est le ViewModel.

Le View Model (MVVM) : Le terme en lui-même peut prêter à confusion, puisque c’est un mix entre deux termes que nous connaissions déjà. C’est un nouveau type de modèle, qui n’a rien à voir avec l’intégrité des données de l’application. C’est plutôt une représentation logique de la vue. Il contient des propriétés qui détaillent l’état de chaque composant d’interface utilisateur. Par exemple, le texte formaté d’un champ UITextField, ou l’état d’un UISwitch (activé ou pas). Il expose également les actions que la vue peut effectuer, comme les UIButtons et les UIGestureRecognizer.

L’une des responsabilités du ViewModel est celle d’un modèle statique représentant les données nécessaires à la vue pour s’afficher. Mais il est également responsable de la collecte, l’interprétation et la transformation de ces données. Cela laisse la vue/contrôleur avec une tâche plus clairement définie, celle de présenter les données qui lui sont fournies par le View Model et gérer les interactions utilisateurs.

Pour résumer, le rôle qui incombe au ViewModel est de récupérer les données à partir du modèle et de les rendre disponibles à la vue sous une meilleure forme. De cette manière, on aura des UIViewControllers beaucoup moins chargées qu’avant. On aurait allégé les tests UI puisqu’on a transformé une grande partie des contrôleurs en un modèle de données testable unitairement. Nous allons également assister à la naissance des UIViewControllers, plus réutilisables qu’avant, de composants graphiques facilement manipulables et d’une meilleure flexibilité de l’interface utilisateur.

Relation entre les différentes couches

La relation entre les différentes couches du modèle MVVM est beaucoup plus simple à appréhender que celle du modèle MVC, il suffit de garder à l’oeil les règles suivantes :

  1. La vue/contrôleur possède une référence vers le ViewModel mais surtout pas l’inverse.
  2. La vue/contrôleur ne communique pas avec le modèle. L’unique accès est assuré à travers le ViewModel.
  3. Le ViewModel possède une référence vers le modèle mais pas l’inverse.
  4. Le modèle ne communique avec aucune couche, comme en MVC.

Avec une telle architecture, on assure une meilleure séparation des responsabilités. En MVC, on était parfois obligé d’instancier des objets de type modèle dans le contrôleur pour pouvoir les présenter à la vue. Avec MVVM, ce n’est plus le cas. C’est ce qui permet de concevoir des vues/contrôleurs à faible dépendance et en l’occurrence, plus réutilisables.

Les tests UI peuvent s’avérer notoirement difficiles, du fait que tester un UIViewController implique de tester un objet qui rajoute et configure des vues qui dépendent de l’état précédent de la scène de l’application. Avec MVVM on aurait beaucoup allégé les tests UI, puisqu’une grande partie de la logique fonctionnelle est déjà encapsulée par le ViewModel, en l’occurence testable unitairement. Ce n’est pas une révolution ça ?

Non mais attendez… Si le ViewModel n’a pas de référence vers la vue, comment pourrait-it mettre à jour l’interface utilisateur si le modèle change ? C’est indiqué dans votre schéma !

Vous avez touché à un autre aspect de l’architecture MVVM. Je vais pas en entrer dans le détail, peut-être je consacrerai un autre article sur le sujet. Ce qu’il faut savoir c’est que MVVM repose sur le mécanisme de Data-Binding. Le Data-Binding est un concept extrêmement puissant qui connecte automatiquement des propriétés d’objets à des contrôles UI. Il existe par défaut dans le framework .Net (initialisateur de l’architecture MVVM).

En OS X, on peut utiliser les Cocoa bindings. Malheureusement, nous ne disposons pas de ce luxe en iOS. Pour y remédier, nous pourrions utiliser les KVO : à chaque changement de propriété, on enregistre une notification. Cela peut s’avérer très coûteux et peut alourdir notre application, il faut également avoir en tête qu’une notification enregistrée doit être suivie, ce qui nous rajoutera des contraintes de développement.

Au lieu de ça, on pourrait utiliser un framework tiers. Le plus connu est ReactiveCocoa, un framework de programmation réactive. Je vous conseille de jeter un coup d’oeil. Encore une fois, rien ne vous oblige à utiliser ces frameworks, vous pouvez concevoir votre propre système de data-binding.

Assez parlé ? Et si on passait un peu à la pratique?

MVVM par la pratique

Supposons maintenant qu’on est en train de développer une application d’un site de vente en ligne d’articles (peu importe le domaine). Intéressons-nous plus précisément à l’écran du profil de l’utilisateur, dont on nous demande d’afficher le nom et le prénom (en une seule ligne), l’ancienneté, et une image. Du coup, l’interface du modèle sera comme suit :


@interface User : NSObject

- (instancetype)initwithLastName:(NSString *)lastName FirstName:(NSString *)firstName Gender:(char)gender RegistrationDate:(NSDate *)registrationDate andImage:(NSString *)imageLink;

@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) char gender;
@property (nonatomic, readonly) NSDate *registrationDate;
@property (nonatomic, readonly) NSString *imageLink;

@end
</pre>
<p>

Vous remarquez qu’on a mis à disposition une méthode d’instanciation pour peupler l’objet, et nous avons limité l’accès aux propriétés (readonly). Rappelez-vous que l’encapsulation est l’un des piliers de la programmation orientée objet.

Maintenant pour afficher les informations d’un utilisateur donné, en MVC, nous procéderions ainsi :


@interface UserViewController ()

@property (nonatomic, retain) IBOutlet UILabel *lblName;
@property (nonatomic, retain) IBOutlet UILabel *lblRegistrationDate;
@property (nonatomic, retain) IBOutlet UIImageView *imgUser;

@end

@implementation UserViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSString *gender = (_user.gender == 'M') ? @"Mr." : @"Mme";
    _lblName.text = [NSString stringWithFormat:@"Bonjour %@ %@ %@", gender, _user.lastName, _user.firstName];

    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"dd/MM/yy"];
    _lblRegistrationDate.text = [dateFormatter stringFromDate:_user.registrationDate];
    
    NSURL *url = [NSURL URLWithString:_user.imageLink];
    UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
    _imgUser.image = image;
}

@end
</pre>
<p>

Tout le traitement est fait dans la méthode viewDidLoad. Nous formattons le texte à mettre en haut de l’écran, à savoir “Bonjour (Madame / Monsieur) X”. Nous formattons également la date d’inscription, là nous avons opté pour le format français d’affichage, mais nous pourrions toujours penser à rajouter l’internationalisation dans notre application, ce qui entrainera davantage de traitements sur le champ date. Et finalement, nous affichons l’image du profil en utilisant une URL, en mode synchrone. Dans une future version, nous voudrions également améliorer l’expérience utilisateur en utilisant des appels asynchrones.

Toutes les améliorations qu’on voudrait apporter à l’application conduiront forcément à explorer encore une fois le code existant dans UserViewController et, croyez-moi, après un certain temps, ce n’est pas du tout évident, même dans notre propre code.

Maintenant, essayez d’écrire des tests unitaires pour ce contrôleur. Eh oui, c’est pénible quand même. Rappelez-vous que dès que l'on aura besoin d'afficher une vue pour effectuer un test, on basculera immédiatement dans le cadre des tests UI. Et là, toute notre logique est dans la méthode viewDidLoad, donc on aura forcément besoin d’afficher la vue pour effectuer des tests “unitaires”.

Essayons d’exporter tout le traitement qu’effectue la classe UserViewController dans une entité à part. Vous l’aurez deviné, c’est le fameux ViewModel. Notre fichier .h serait alors :


#import <Foundation/Foundation.h>
#import "User.h"

@interface UserViewModel : NSObject

- (instancetype)initWithUser:(User *)user;

@property (nonatomic, readonly) User *user;

@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *registrationDateText;
@property (nonatomic, readonly) NSData *photoData;

@end
</pre>
<p>

et notre fichier UserViewModel.m sera :


#import "UserViewModel.h"

@implementation UserViewModel

- (instancetype)initWithUser:(User *)user
{
    self = [super init];
    if (self)
    {
        NSString *gender = (_user.gender == 'M')? @"Mr." : @"Madame";
        _nameText = [NSString stringWithFormat:@"Bonjour %@ %@ %@", gender, _user.lastName, _user.firstName];
                
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"dd/MM/yy"];
        _registrationDateText = [dateFormatter stringFromDate:_user.registrationDate];

        _photoData = [NSData dataWithContentsOfURL:[NSURL URLWithString:_user.imageLink]];

    }
    return self;
}

@end
</pre>
<p>

Comme je l’avais mentionné précédemment, il faut toujours garder à l’esprit que le ViewModel relate la logique métier de l’interface graphique. Du coup, chaque composant est représenté par une propriété. Ici, nous avons placé tout le traitement dans la méthode d’initialisation, mais vous pouvez toujours exporter le traitement de chaque composant dans une méthode à part. Je vous le conseille fortement si vous voulez mettre en place des tests unitaires. Gardez toujours les bonnes pratiques en vue : une méthode est censée s’occuper d’une seule chose.

Comme convenu, le ViewModel a une référence vers le modèle “User”, mais pas vers le UIViewController ! Ne jamais importer la librairie UIKit dans un ViewModel. La vue/contrôleur est censée s’occuper uniquement de la présentation graphique des différents composants à l’utilisateur. C’est exactement pour ça que la photo de la fiche utilisateur n’est pas représentée dans le ViewModel. Au lieu de ça, nous avons consacré une propriété qui serait une représentation sous forme de data de l’image. Par conséquent, si nous avons à changer la manière avec laquelle nous allons extraire l’image (image enregistrée dans le bundle, appel asynchrone...) il suffit de changer finalement le view Model.

Voyons maintenant ce qu’est devenu notre contrôleur :


#import "UserViewController.h"

@interface UserViewController ()

@property (nonatomic, retain) IBOutlet UILabel *lblName;
@property (nonatomic, retain) IBOutlet UILabel *lblRegistrationDate;
@property (nonatomic, retain) IBOutlet UIImageView *imgUser;

@end

@implementation UserViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _lblName.text = _userViewModel.nameText;

    _lblRegistrationDate.text = _userViewModel.registrationDateText;
    
    _imgUser.image = [UIImage imageWithData:_userViewModel.photoData];
}

@end
</pre>
<p>

Une ligne par composant! N’est-ce pas élégant, tout ça ? Notre contrôleur est devenu beaucoup plus léger et surtout réutilisable.

Conclusion

Les résultats de l'utilisation du modèle MVVM, sont une légère augmentation de la quantité totale du code et une diminution globale de sa complexité. Le ViewModel est un excellent endroit pour traiter la logique de validation pour l'entrée utilisateur, la logique de présentation pour la vue, les demandes réseau et les fonctionnalités diverses. Les seules choses qui ne lui appartiennent pas sont les références à la vue/contrôleur elle-même. En d'autres termes, si vous importez la librairie UIKit dans votre ViewModel, c’est que vous sortez du cadre MVVM.

MVVM est un modèle de conception qui présente de nombreux avantages pour développer une application iOS. Cependant, comme dans tous les modèles, il faut bien comprendre les limites et la mise en œuvre appropriée dans chaque projet et pour chaque spécification. Par exemple, MVVM peut s’avérer inemployable pour les projets qui traitent des fonctionnalités complexes avec un nombre limité de vues. J’espère que vous trouverez MVVM une approche utile dans l’un de vos projet iOS.

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT