Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

MVVM : Service, Factory, IoC, injection de dépendances, oui mais pourquoi ?

Il n’y a pas de “S” dans MVVM et pourtant aujourd’hui la notion de Service est fortement liée à cette architecture même si elle ne lui est pas réservée…

MVVM n’est pas que M-V-VM

MVVM est une guideline, une méthode pour concevoir des applications dans lesquelles, au départ, était ciblé principalement le découplage fort entre le code d’une part et l’IHM de l’autre.

Mais partant d’un si bon pied il aurait été assez bête de s’arrêter là. C’est un ainsi que le “découplage fort” en tant que concept global s’est imposé pour s’étendre non plus seulement au lien code / IHM mais au code lui-même.

Comment le code peut-il se “découpler” de lui-même ?

C’est une bonne question en effet…

C’est qu’il y a “code” et “code” ! Au premier abord on voit mal la différence entre les deux je l’accorde, mais en y regardant de plus près on finit par trouver des tas de choses qui peuvent se regrouper par thème, par notion, par but, par domaine, etc… Des découpages sémantiques ou fonctionnels du code se sont ainsi imposés avec le temps, certains existants avant MVVM, d’autres venant de l’application de MVVM. Le besoin récent de faire des développements cross-plateforme ou cross-form-factor a aussi renforcé l’intérêt de certains patterns. Toutes ces influences se retrouvent dans MVVM aujourd’hui faute d’un nom plus générique.

De fait, appliquer MVVM ne consiste plus à se concentrer uniquement sur le découplage code / IHM mais sur tous les découplages possibles même au sein du code lui-même. Et ces découplages imposent à leur tour d’autres guidelines et d’autres patterns.

MVVM en tant que méthode ou pattern est donc dans la pratique bien plus que ce qu’il contenait à l’origine. Implicitement des notions comme celle de Service ou d’Injection de Dépendances par exemple se sont greffées au noyau de MVVM pour former un tout bien plus vaste que ce que les trois initiales M-V-VM pouvaient signifier il y a plusieurs années de cela lorsque John Gossman en fit la description sur son blog en 2005.

A ce titre il est bon de noter que Gossman ne fut qu’un rapporteur de la méthode alors qu’on lui prête souvent la paternité de cette dernière car c’est sous sa plume qu’en naquit la première description. En réalité ce sont des architectes travaillant chez Microsoft, Ken Cooper et Ted Peters, qui ont créé MVVM pour simplifier le modèle de programmation dit event-driven des UI dans le cas particulier de WPF en tirant profit du binding de XAML.

Maitriser MVVM et l’appliquer correctement réclame donc de comprendre tous ces autres design patterns ou guidelines. Ce n’est pas une option, cela fait partie de la connaissance de MVVM qu’on appelle toujours de cette façon alors que pour bien faire il aurait fallu trouver un nom pour cette nouvelle “méta-méthode” dont MVVM (canal historique) n’est plus qu’une composante. Mais pour l’instant c’est le sens de MVVM qui évolue sans pour autant le dire tout en le faisant. C’est inconfortable pour cerner le concept et ce qu’il implique, mais l’inventeur d’un nom mieux adapté ne s’est pas encore fait connaitre.

Le découplage fort

La majorité des notions qui se sont ajoutées à MVVM prennent donc leur origine dans l’extension au code lui-même du découplage code/IHM à l’origine de la méthode. Pourquoi “fort” parce ce découplage généralisé est une version “forte” du découplage limité au couple code/IHM. C’est une amplification de ce premier pas vers la séparation de tout code en unités aux responsabilités mieux définies n’ayant plus aucun lien direct entre elles. Le découplage fort consiste à faire ce découpage mais aussi à mettre en œuvre des techniques permettant à ces unités isolées de communiquer comme elles le faisaient avant d’être séparées. Et cette communication doit être mise en place sans que les unités ne se connaissent ou n’ait à se connaitre, à aucun moment. Sans exception.

D’où l’adjectif “fort” ajouté au mot “découplage” pour exprimer cet absolu qu’on doit appliquer avec la même ardeur à tous les niveaux du code.

D’ailleurs le découplage fort consiste pour l’essentiel en ces techniques de communication “sans contact” entre différents codes. L’isolation du code en petites unités aux responsabilités limitées et bien définies découle de la simple mise en œuvre correcte des principes de l’OOP. Et cela montre d’ailleurs la cohérence de tout cela puisque finalement l’objectivation du code contenait déjà en graine toutes les notions qui se sont précisées autour de MVVM, du découplage faible puis fort, et des autres bonnes pratiques comme les Services ou l’Injection de dépendances justement.

Ce qui habilement nous ramène au sujet du jour !

La notion de Service

Prenons les choses sous l’angle pratique pour mieux comprendre le besoin.

Imaginons une application qui a souvent besoin d’utiliser des nombres aléatoires.

Le premier ViewModel concerné va très certainement contenir un code du type :

public class MainViewModel : INotifyPropertyChanged
{
 	public event PropertyChangedEventHandler  PropertyChanged;

	private Random random = new Random();

	private void UneMéthode()
	{
		var i = random.Next(0,5);
		// blabla
	}
	
	// ...
}

J’ai pris l’appel à Random comme exemple car cela évite de développer toute une application exemple pour expliquer le concept. Random est déjà en service et il se plie donc bien aux besoins de notre histoire. On pourrait imaginer milles autres choses plus complexes (par exemple aller chercher un contenu sur internet, le traiter et retourner un résultat).

Je ne ferai pas de digression sur Random et les bonnes pratiques qui entourent son utilisation cela serait hors sujet mais précisons tout de même qu’ici il n’est pas utilisé en respectant les guidelines qui lui sont particulières mais que ce n’est pas grave, c’est un exemple, une abstraction de situations plus complexes et non un code à réutiliser.

La première réflexion sur ce bout de code que nous impose la notion de Service est la suivante : Si l’application contient 10 ou 20 (ou même plus) ViewModel ou autres codes (dans les Models par exemple) faisant appel à des nombres aléatoires cela va poser plusieurs problèmes :

  • D’une part ici la création de plusieurs instances de Random ce qui n’est pas une bonne idée
  • D’autre part la multiplication d’un code qu’on croyait local et unique et qui va par copier/coller augmenter le désordre dans le code
  • Mais pire, si demain on constate que Random n’est pas un générateur aléatoire suffisamment performant et qu’on décide d’utiliser autre chose à sa place ce sont des tas de codes qu’il faudra modifier sans en oublier…

Mauvaise utilisation des ressources, désordre, oublis. Diantre ça commence mal, même très mal, pour cette application !  Tout à fait dans le genre de code que je vois parfois en Audit et qui me file des sueurs froides.

première séparation

Pour tenter de régler ces problèmes ont sera tenté de faire une première séparation entre ce besoin de nombres aléatoires et le code qui l’utilise. Ce n’est pas encore le divorce, juste une séparation.

// fichier MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler  PropertyChanged;
	
	private void UneMéthode()
	{
		var cr = new ClassicRandom();
		var i = cr.GetNext(0,5);
		// blabla
	}
	
	// ...
}

// fichier SecondViewModel.cs
public class SecondViewModel : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;

	private void UneAutreMéthode()
	{
		var cr = new ClassicRandom();
		var i = cr.GetNext(15, 50);
		// blabla
	}

	// ...
}

// fichier ClassicRandom.cs
public class ClassicRandom
{
	private Random random = new Random();

	public int GetNext(int min, int max)
	{
		return random.Next(min, max);
	}
}

Ici nous avons créé une classe, ClassicRandom, qui propose une méthode GetNext(int,int) qui retourne le nombre aléatoire dont maintenant deux ViewModel (Main et Second) font usage.

C’est déjà plus propre et répond au moins à l’un des problèmes soulevés plus haut :

  • La multiplication d’un code local au départ qui par copier/coller se multiple un peu partout n’est plus qu’un souvenir. Ouf !

Et c’est tout !

Le problème de la création de multiples instances de Random reste entier et celui de la modification éventuelle du procédé pour obtenir des nombres aléatoires n’est pas même effleuré.

Les mauvaises idées

J’entends déjà des petits malins qui cogitent et qui se disent, si je rends la classe ClassicRandom statique je règle les problèmes d’instances multiples…

Certes, mais d’autres problèmes plus complexes vont surgir, notamment le fait que cette classe statique ne sera pas thread-safe par magie. il va falloir le prendre en compte et cela débouche sur tout un pan de la programmation qui est assez mal maitrisé en général (le multi-threading, ce qui est thread-safe ou pas et toutes ces choses merveilleuses).

Mauvaise idée donc.

Ah… j’entends d’autres ingénieux qui proposent d’ajouter un constructeur à la classe ClassicRandom, avec un paramètre du genre entier pour choisir la méthode de génération aléatoire. Au début ça ne servira à rien mais on pourra ajouter un Switch ou des tas de If/Else dans le futur …

Aie… pitié, ça me fait mal !

Un paramètre qui ne sert à rien, qui sera peut être même ignoré par celui qui ajoutera une autre méthode de génération aléatoire plus tard car ne sachant pas à quoi sert ce paramètre il préfèrera,dans l’urgence, ajouter un nouveau paramètre “bien à lui” pour faire cette distinction… Et puis le code spaghetti s’installe, les If/Else vont se succéder, les Switch … jusqu’au moment où on devra porter le code sous une autre plateforme et qu’on s’apercevra que le procédé numéro 23 ne passe pas la compilation sous Xamarin et son framework .NET au profil légèrement différent…

Mauvaise seconde idée donc.

C’est là qu’on s’aperçoit que faire du découplage fort n’est pas juste prendre une hache pour saccager le code… il faut acquérir un réel savoir-faire et avoir une bonne expérience de la chose sinon on sera persuadé d’avoir “bien appliqué tout MVVM” (je l’entends souvent ce truc là) et on aura produit au final un code pourri, disons-le simplement et avec franchise.

Les bonnes pratiques

Déjà dans un premier temps si on veut créer une instance unique de notre “service” ClassicRandom il faudrait non pas juste une classe statique mais un singleton. Mais comme ce n’est pas la bonne solution je ne m’étendrai pas.

Prenons les choses dans l’ordre.

Découplage fort = interface (le concept de programmation pas l’IHM).

C’est la première règle à bien comprendre. Il n’y a pas de code fortement découplé sans utilisation des interfaces.

Cela permettra de répondre à la question des implémentations différentes. Et ce sans if/else, sans Switch ou autres horreurs. La programmation objet est une programmation sans IF ! Je le répète souvent et mon pote Julien le disait encore dernièrement dans l’un de ses billets. Et il dit beaucoup de choses sensées. Je vous conseille la lecture de ce dernier billet (Quelques principes de base de la programmation orientée objet) et des autres aussi d’ailleurs.

Sans IF ? Oui car comme on va le voir les affreux tests que certains envisageaient disparaissent totalement dans une approche objet respectée.

Il faut donc créer une interface, disons IRandomProvider, qui proposera une méthode GetNext(int,int). La classe ClassisRandom déjà existante supporte déjà cette interface (par le fait même qu’elle nous sert de modèle pour créer l’interface) mais il faudra le déclarer.

Nous avons donc maintenant une interface et une implémentation de celle-ci :

// fichier IRandomProvider.cs
public interface IRandomProvider
{
	int GetNext(int min, int max);
}

// fichier ClassicRandom.cs
public class ClassicRandom : IRandomProvider
{
	private Random random = new Random();

	public int GetNext(int min, int max)
	{
		return random.Next(min, max);
	}
}

C’est beaucoup mieux mais comment utiliser tout cela ? Une interface hélas ça ne s’instancie pas par magie… Il faut une classe d’implémentation. Et justement veut éviter d’être en contact avec cette dernière.

Ce que nous venons de faire ne sert donc à rien ?

Pas vraiment. C’est inutile en l’état car il manque quelque chose…

Factory ou Service ou Injection de dépendances ?

Il faudra bien instancier ClassicRandom quelque part c’est évident, mais comme cela ne peut pas être dans les ViewModel on le place où ce code ?

Plusieurs possibilités s’offrent à nous. Parmi celles-ci on peut citer de façon non exhaustive :

  • La Factory. C’est un pattern intéressant qui peut être mis en œuvre via un singleton par exemple. Il y a plus élégant mais ce n’est pas une mauvaise solution.
  • Le Service. Soit il est implémenté exactement comme la Factory, soit il est placé dans un conteneur d’inversion de contrôle ce qui est déjà mieux.
  • L’injection de dépendance. Quitte à avoir un conteneur IoC autant aller jusqu’au bout et faire de l’injection de dépendance, c’est le plus élégant et le plus découplé.

Si on adopte pour l’instant la version simple, celle de la Factory, nous aurons un code qui va devenir le suivant :

// fichier MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler  PropertyChanged;
	
	private void UneMéthode()
	{
		var i = RandomFactory.Instance.GetProvider.GetNext(0,5);
		// blabla
	}
	
	// ...
}

// fichier SecondViewModel.cs
public class SecondViewModel : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;

	private void UneAutreMéthode()
	{
		var i = RandomFactory.Instance.GetProvider.GetNext(15, 50);
		// blabla
	}

	// ...
}


// fichier RandomFactory.cs
public class RandomFactory
{
	private RandomFactory()	{}
	private static RandomFactory instance;
	private IRandomProvider provider;

	public static RandomFactory Instance
	{ get { return instance ?? (instance = new RandomFactory()); }	}
	
	public IRandomProvider GetProvider => provider ?? (provider = new ClassicRandom());
	
}

// fichier IRandomProvider.cs
public interface IRandomProvider
{
	int GetNext(int min, int max);
}

// fichier ClassicRandom.cs
public class ClassicRandom : IRandomProvider
{
	private Random random = new Random();

	public int GetNext(int min, int max)
	{
		return random.Next(min, max);
	}
}

 

La classe RandomFactory est la Factory comme son nom l’indique.

On remarquera que cette classe n’est pas statique… C’est une classe normale. Toutefois elle cache son constructeur en mode privé pour qu’aucune instance ne puisse être créée. A la place elle expose une propriété statique elle qui retourne une instance de la Factory existante ou fraichement créée. On reconnait ici l’implémentation classique du design pattern Singleton.

La Factory expose une propriété (non statique cette fois-ci… il faut suivre…) qui retourne une instance supportant IRandomProvider.

Le premier découplage fort intervient ici. On transite déjà dans la Factory par l’interface et non plus le nom de la classe d’implémentation.

De la tambouille qui se préparait on obtient en faisant les choses proprement trois fichiers et trois déclarations de classes ou d’interfaces :

  • La déclaration de l’interface IRandomProvider
  • La déclaration d’une classe d’implémentation ClassisRandom
  • La déclaration de la Factory RandomFactory

En quoi cela a-t-il réglé les problèmes ?

Reprenons, le code original posait au moins trois problèmes :

  • D’une part la création de plusieurs instances de Random
  • D’autre part la multiplication d’un code qu’on croyait local et unique et qui va par copier/coller augmenter le désordre dans le code
  • Mais aussi l’impossibilité de changer facilement l’implémentation du générateur aléatoire.

Dans un premier temps nous avions juste créer une classe pour supporter Random. Nous avions régler le second point. Et ce n’était pas suffisant.

En adoptant la stratégie de l’interface plus celle de la Factory nous répondons aux deux autres problèmes :

  • Via la Factory qui est un Singleton nous garantissons qu’il n’y a plus qu’une seule instance de Random pour toute l’application
  • Grâce à la Factory nous pouvons décider de changer le code de Random facilement sans jamais avoir à toucher le code existant utilisant ce service…

J’en veux pour preuve le code suivant :

// fichier CryptoRandom.cs
public class CryptoRandom : IRandomProvider
{
	public int GetNext(int min, int max)
	{
		using (var rng = new RNGCryptoServiceProvider())
		{
			var buffer = new byte[4];
			rng.GetBytes(buffer);
			var res = BitConverter.ToUInt32(buffer,0) / (double)UInt32.MaxValue;
			return min + (int)((max-min)*res);
		}
	}
}

Nous définissons ici une classe supportant l’interface IRandomProvider mais qui utilise maintenant les services de cryptographie de .NET pour obtenir un nombre aléatoire réputé de meilleur qualité que ceux retournés par Random.

Et il ne nous reste plus qu’à changer en un point unique, la Factory, le code d’instanciation de ClassicRandom par CryptoRandom pour qu’immédiatement l’ensemble de l’application et ces dizaines d’appels bien cachés au générateur de nombres aléatoires se mettent à utiliser le nouveau code sans même s’en apercevoir et sans risque d’oubli…

// fichier RandomFactory.cs
public class RandomFactory
{
	private RandomFactory()	{}
	private static RandomFactory instance;
	private IRandomProvider provider;

	public static RandomFactory Instance
	{ get { return instance ?? (instance = new RandomFactory()); }	}
	
	public IRandomProvider GetProvider => provider ?? (provider = new CryptoRandom());
	
}

Et le tour est joué…

Conteneur IoC et injection de dépendances

La stratégie de la Factory est excellente il n’y a rien à redire non ?

Hélas si. La Factory est uniquement un moyen de déléguer la création d’une instance d’objet. En passant par une interface nous arrivons à réaliser un couplage faible entre les classes utilisatrices et les classes d’implémentation mais c’est bien notre code par l’appel de la Factory qui finalement créé l’instance (ou obtient dans notre cas l’instance unique mais cela n’est pas le problème).

Il existe encore un niveau de découplage plus fort : celui qui permettrait que la création de l’objet soit totalement externe à notre code.

Dans un premier temps on peut remplacer la Factory par un conteneur IoC. Je ne vais pas aller trop loin dans ce déjà long article et pour simplifier à l’extrême considérez qu’il s’agit d’un Dictionnaire<key,value> central. A certains endroits dans le code (en général un seul pour éviter l’éparpillement) des classes ou des instances de ces classes sont enregistrées dans le dictionnaire. Le type, classe ou de préférence interface servant de clé.

Comme la Factory le conteneur IoC est visible par toutes les classes de l’application. Et lorsqu’elles ont besoin d’un service ou d’une instance d’une classe enregistrée dans le dictionnaire elles l’obtiennent via le conteneur qui en première approximation donc n’est qu’un dictionnaire.

Utilisé de cette façon un conteneur IoC revient à une simple Factory globale pour tous les services de l’application (messages à afficher, service de mail, générateur aléatoire…). Plutôt que d’avoir plein de Factories on a un seul conteneur. Mais si on s’en tient à cette vision des choses le gain est assez léger.

Dans la réalité un conteneur IoC sait créer les instances des types enregistrés mais il sait aussi inspecter les constructeurs de ces types. Et lorsqu’il repère des paramètres qui correspondent à des types qu’il connait il sait “s’auto-interroger” pour obtenir les instances et remplacer les paramètres par ces valeurs.

Dans notre exemple comment peut-on mettre cela en évidence ?

D’abord il faut un conteneur IoC… Or on entre ici dans de longues explications ne serait-ce que sur le choix de celui-ci. Il en existe plusieurs, chacun ayant ses avantages et ses inconvénients. Bâcler le sujet ici ne serait vraiment pas vous rendre service.

Il n’y aura donc pas d’exemple réel (le code présenté jusqu’ici fonctionne, testé et modifié sous LinqPad d’ailleurs plutôt que VS c’est nettement plus pratique).

Mais pour illustrer jusqu’au bout la transformation de notre exemple, disons que MainViewModel et SecondViewModel vont se voir doter d’un constructeur public qui prendra un paramètre de type IRandomProvider.

Les ViewModels ainsi que les services comme CryptoRandom seront tous enregistrés dans le conteneur IoC.

Ainsi, lorsqu’une instance de MainViewModel par exemple sera réclamée (notamment par un binding du datacontext de la fiche XAML correspondante par exemple) cela le sera via le conteneur IoC.

En analysant le constructeur le conteneur verra qu’il attend un paramètre de type IRandomProvider. Et comme il connait déjà ce type et la façon de l’instancier il passera automatiquement l’instance du générateur de nombres aléatoire au constructeur de MainViewModel (ou SecondViewModel). Dans ce constructeur on stocke en général dans une variable locale l’instance reçue. Puis on l’utilise tout au long du code.

Selon les conteneurs et leur sophistication le paramétrage et les enregistrements des types est plus ou moins pointu. On peut par exemple enregistrer un type et une instance déjà créée, ce qui permet de retourner toujours la même valeur, ou bien on peut enregistrer un type et une expression Lambda qui créée la bonne instance. L’expression sera appelée à chaque fois qu’une instance du type sera demandée, etc…

Le sujet est vaste et comme je le disais mérite plus que des raccourcis rapides pour le traiter.

Mais nous serons allé jusqu’au bout de l’explication malgré tout.

Conclusion

Le code final utilisant la Factory est bien éloigné du code original… Celui qui utiliserait de l’injection de dépendances avec un conteneur IoC serait encore plus radicalement différent (mais hélais trop long pour cet article).

Certains irréductibles se diront toujours que le premier code n’est certes pas “académique” mais qu’il est super facile et rapide à produire et qu’ils n’ont pas le temps, pas le budget, blabla, blabla.

A ceux là il n’y a donc rien à dire.

Les autres – la majorité même je l’espère ! – auront compris la démarche et son intérêt. Qui même sur un petit exemple démontre pourquoi le code de départ n’est pas correct, quels problèmes il soulève et comment on peut aisément les contourner en adoptant une architecture fortement découplée.

Ils ont compris qu’il n’y a rien d’ “académique” dans tout cela. Il y a juste et simplement au départ un mauvais code qu’il faut corriger.

Ecrivez du bon code,

Et Stay Tuned !

Faites des heureux, PARTAGEZ l'article !