SOLID, un principe invoqué dans de nombreux articles, dans les discussions autour de la machine à café, élevé par certain au rang saint Graal. Mais vous, qu’en connaissez-vous de SOLID ? Que seriez-vous capable d’en dire d’un peu … solide dans un entretien par exemple ?
Un sigle flou, des concepts clairs
Avouons-le, de but en blanc si on vous demande de définir SOLID, d’éclater le sigle, je pari que nombre d’entre vous resterons un peu secs. Ne vous en faites pas notre métier est plein de sigles tous plus essentiels les uns que les autres et on n’en comprend que la moitié. Tout le problème c’est que si on tombe sur un fan on passe pour un inculte…
Heureusement Dot.Blog est là !
D’abord précisons que si SOLID est un sigle flou, les concepts qu’il promeut sont souvent déjà compris par la majorité des développeurs. Mais de comprendre à appliquer il y a souvent un monde. Quant à expliquer c’est encore tout un voyage !
Un but principal : rendre le code plus facile à lire et à maintenir.
Mais par quels moyens ? Que représente chaque lettre de SOLID ? Que cache chacune, qui une fois révélée, ouvre la voie vers plus d’intelligence dans le code ?
Ce sont ces concepts que je vais tenter de clarifier plus loin dans cet article.
Une dernière question : n’avez-vous jamais eu l’impression terrible en maintenant un code de jouer au Mikado ? On transpire en essayant de tirer ce petit bâton de bois sans faire tomber les autres… parfois ça marche, et parfois… Patatras ! Ou bien, plus vicieux, tout semble se passer bien on lâche la pression on pose à côté de soi le bâtonnet et d’un seul coup sans que personne ne touche à rien le tas s’affaisse et on a perdu !
Le développement ne devrait jamais être une partie de Mikado, c’est cela que nous dit SOLID !
S.O.L.I.D.
Chaque lettre de ce sigle renvoie à un concept amenant à une architecture du code de meilleure qualité. Encore faut-il entendre chacun de ces concepts derrière le sigle !
Le sigle lui-même a été forgé par C.Martin dans les années 1990 comme un moyen mnémotechnique de grouper plusieurs impératifs d’architecture menant d’un code lourd, peu aisé à maintenir vers un code à couplage faible, opérant de façon cohérente et encapsulant les besoins réels de l’entreprise de façon appropriée.
A la base on retrouve bien entendu tout ce que l’on sait sur le couplage fort ou faible, les grands principes de Programmation Object, etc. Mais tout cela est épars et Martin les a groupés pour leur donner un nouveau sens, en faire un tout lui-même cohérent de règles partageant toutes un même but à atteindre.
S comme SRP
Single Responsabilité Principle. Le principe de responsabilité unique.
Une classe doit absolument se concentrer sur une tâche, un but, ne prendre en charge qu’une seule responsabilité. C’est essentiel.
Prenons l’exemple d’une classe “Client” telle qu’on peut la rencontrer sous ce nom ou un autre dans de nombreux logiciels que j’ai pu auditer :
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public int Insert()
{
try
{
// insertion en base de données
// retourne l'ID
return 0;
} catch (Exception e)
{
System.IO.File.WriteAllText(@"c:\temp\log.txt",e.ToString());
return -1;
}
}
}
Quel(s) problème(s) pose cette classe ?
Elle viole une fois SRP et on peut la soupçonner de réitérer ce crime peut-être deux fois…
La première fois est évidente : dans le bloc “catch” la classe écrit dans un log. Ce n’est définitivement pas de sa responsabilité de s’occuper d’écrire des choses dans un fichier de log.
La seconde fois est soupçonnée puisque le code est juste suggéré : c’est l’accès à la base de données. S’il y a insertion via ADO.NET ou autre avec du code SQL par exemple, ou l’enregistrement dans un fichier XML ou encore une autre technique de ce genre il y aura bien une seconde violation de SRP.
Mais pas de procès d’intention, concentrons-nous sur ce qui est sûr : la violation de SRP dans le ‘'”catch”.
Quelle est la responsabilité de la classe Client ? De stocker des informations sur un Client. Pas d’écrire des fichiers de log.
Comment réparer cela ?
En respectant SRP qui ici nous impose de créer une seconde classe qui aura la responsabilité d’écrire le Log :
public static class Log
{
public static void Error(Exception ex)
{
System.IO.File.WriteAllText(@"c:\temp\log.txt",ex.ToString());
}
}
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public int Insert()
{
try
{
// insertion en base de données
// retourne l'ID
return 0;
} catch (Exception e)
{
Log.Error(e);
return -1;
}
}
Désormais la classe Client n’a plus la responsabilité de l’écriture dans le Log, c’est une classe uniquement dédiée à cela qui s’en charge.
Les avantages sont nombreux et évidents, comme le fait de pouvoir faire évoluer l’écriture des Logs sans avoir à toucher à toutes les classes qui comme Client s’en chargeaient directement à tort.
Si nos soupçons s’avéraient fondés, il faudrait agir de la même façon sur la partie insertion en base de données.
Attention : les principes d’architecture ne sont que rarement des absolus qu’on peut suivre à la lettre sans faire preuve d’un minimum de jugeote… Par exemple ici (et considérant que l’insertion des données ne pose pas de problème) vous trouverez toujours un maniaque de SRP qui vous dira que gérer les erreurs n’est pas de la responsabilité de la classe Client.
Aurait-il tort ? Personnellement je ne le pense pas. Le code “try/catch” protège l’accès aux données. Si SRP est correctement respecté alors cet accès aux données sera effectué par un objet du DAL ou en tout cas un code spécialisé dans les accès aux données des clients. Et c’est bien entendu ce code qui devra gérer les erreurs d’insertion ou autres opérations CRUD sur les clients…
De fait le respect net de SRP oblige à complexifier un peu l’exemple :
public static class Log
{
public static void Error(Exception ex)
{
System.IO.File.WriteAllText(@"c:\temp\log.txt",ex.ToString());
}
}
public static class CrudClient
{
public static int Insert(Client client)
{
try
{
//accès DAL ou autre
return 0;
} catch (Exception e)
{
Log.Error(e);
return -1;
}
}
}
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public int Insert()
{
return CrudClient.Insert(this);
}
}
Désormais la classe Client respecte SRP. Elle maintient les informations d’un client et sait les persister.
L’écriture dans le Log, la réalisation des opérations CRUD et la gestion de leurs erreurs tout cela a été transféré à d’autres classes qui n’ont qu’une seule responsabilité elles aussi (écrire un log, persister un client…).
Peut-on être encore plus tatillon ?
Oui, et on le doit.
En réalité la classe Client est une classe BOL (une classe métier), elle ne doit en aucun cas savoir comment se persister ou se réhydrater. Nous avons un embryon de solution avec notre classe CrudClient. Il suffit tout bêtement de supprimer la méthode Insert de Client pour enfin avoir un code propre qui respecte SRP !
void Main()
{
var c = new Client{Name="E-Naxos"};
CrudClient.Insert(c);
}
public static class Log
{
public static void Error(Exception ex)
{
System.IO.File.WriteAllText(@"c:\temp\log.txt",ex.ToString());
}
}
public static class CrudClient
{
public static int Insert(Client client)
{
try
{
//accès DAL ou autre
return 0;
} catch (Exception e)
{
Log.Error(e);
return -1;
}
}
}
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
}
Qui persistera le Client ? Le code qui en aura besoin. Comment le fera-t-il ? En invoquant CrudClient.Insert(instanceClient); C’est tout. C’est propre. Évolutif. Dans notre exemple ci-dessus c’est Main() qui créée une instance, l’initialise et demande qu’elle soit persistée.
La classe Client devient pure, uniquement dédiée à représenter un client. Les intentions sont désormais clarifiées. Chaque classe possède sa propre responsabilité et pourra évoluer librement sans créer de problème.
Respecter SRP est essentiel et lorsque vous regardez le petit bout de code dont je suis parti et l’architecture à laquelle j’arrive on peut comprendre pourquoi un logiciel bien écrit par un pro peut coûter plus cher qu’un code qui “semble” correct écrit par un débutant… Et à quel point il peut être difficile de justifier la différence auprès d’un profane qui ne voit que le temps consommé (le coût) et non le savoir mis en œuvre (la qualité)…
Cet exemple vous fait aussi toucher du doigt les raisons (nombreuses) qui me font considérer le Unit Testing comme un cache misère dans la plupart des cas. En effet, face à de mauvaises architectures il semble légitime de barder le code de tests unitaires. Cela évite les régressions en cas de maintenance.
Mais je suis convaincu qu’un code bien architecturé n’a pas un besoin fondamental de Unit Testing tout simplement parce qu’il est conçu pour être maintenable sans régression… On peut, et on doit à mon sens, tester des opérations de haut niveau pour s’assurer qu’elles fonctionnent toujours (des opérations qui ont un sens pour l’utilisateur, créditer un compte, supprimer un compte, attribuer un taux de ristourne à un compte et vérifier que les factures en prennent bien compte, etc). Mais tester chaque méthode, chaque bout de code est du temps perdu et ne peut que cacher une architecture bancale dès le départ. Et même bien testé un soft mal architecturé restera toujours mal architecturé…
O comme Open Closed Principle
Continuons avec notre classe Client. Supposons désormais qu’on puisse avoir à gérer plusieurs types de clients qui disposent de ristournent différentes.
Une première approche pourrait être celle-ci :
public enum CustomerCategory {Normal,Silver, Vip}
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public CustomerCategory Category {get; set;}
public double GetDiscount()
{
if (Category==CustomerCategory.Normal) return 0d;
else if (Category==CustomerCategory.Silver) return 5d;
else return 10d;
}
}
On remarquera que j’ai fait les choses proprement en déclarant une Enum plutôt que d’utiliser des entiers qui ne veulent rien dire…
Notre client possède donc une Category qui est initialisée par défaut à “Normal” et qui n’offre aucun discount. S’il devient un jour Silver ou VIP il obtiendra 5 ou 10 % de remise.
Qu’est-ce qui peut bien clocher ici ? SRP est respecté.
Mais ce qui ne va pas c’est la façon dont GetDiscout() est écrite. D’abord tous ces tests sont affreux, un switch serait peut-être plus approprié, mais là n’est pas la question.
La question c’est que la programmation objet bien faite est une programmation SANS “IF” !
Vous allez me dire, oui, c’est vrai le taux devrait être rangé dans une autre classe pour n’avoir qu’un point de modification et GetDiscount devrait appeler cette classe pour retourner le taux.
Beurk !
Non, je vous parle de Programmation Objet pas de passer le dimanche à jouer avec ce que vous avez acheté le samedi chez M. Bricolage !
L’Open Closed Principle nous parle d’extensibilité. De la facilité à étendre un code sans avoir à revisiter le code existant. Car c’est ce genre de choses qui justifient le Unit Testing… Le pompier incendiaire… Je fous le feu pour justifier mon poste de pompier !
La programmation objet est plus noble et réclame un peu plus de réflexion aussi. Plutôt que de créer un code bancal qu’il faudra tester et retester à la moindre modif tellement il tient debout par l’opération du saint esprit, est-il possible de trouver une ARCHITECTURE qui autorise les extensions sans prendre de risques ?
Oui bien entendu. Cela s’appelle l’héritage et le polymorphisme…
Dans une programmation véritablement objet il n’y a pas de IF (je pousse à l’extrême mais ici le IF va bien sauter !). Pourquoi ? Tout bêtement parce qu’en utilisant les principes de base de la PO et POO il n’y a le plus souvent pas besoin de ce type de construction, en tout cas au niveau architectural s’il est bien pensé.
De fait, nos clients normaux, Silver et VIP ne doivent pas se distinguer par une propriété mais par un héritage. Et le IF disparaît. L’avantage ? On peut ajouter demain un type de client Gold qui s’intercalerait entre Silver et VIP, ou ajouter un Premium au-dessus de VIP le tout sans avoir besoin de retester tout l’ensemble car ce qui aura été écrit ne bougera pas ! Adieu régression et Unit Testing bas niveau qui ne fait que cacher la misère…
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public virtual double Discount { get {return 0d;} }
}
public class ClientSilver : Client
{
public override double Discount { get {return 5d;} }
}
public class ClientVip : Client
{
public override double Discount { get {return 10d;} }
}
N’est-ce pas plus joli ainsi ?
Plus d’énumération à maintenir, plus de risque de faire une bêtise dans le IF lorsqu’on ajoute un type de client, etc…
Si un client est Silver, sont taux de ristourne est immédiat. Et si demain on ajoute un nouveau type seul le code de ce nouveau type devra être testé, pas le reste qui n’aura pas bougé. Et quand je dis testé… il faudrait juste relire la constante quoi… D’ailleurs cette constante peut être judicieusement externalisée de telle façon à ce que tous les taux de remise soient centralisés en un seul point et facile à modifier.
Ce qui donne alors :
public static class DiscountRate
{
public static double Normal = 0d;
public static double Silver = 5d;
public static double Vip = 10d;
}
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public virtual double Discount { get {return DiscountRate.Normal;} }
}
public class ClientSilver : Client
{
public override double Discount { get {return DiscountRate.Silver;} }
}
public class ClientVip : Client
{
public override double Discount { get {return DiscountRate.Vip;} }
}
Désormais, les taux de remise étant nommés clairement et centralisés dans une classe en ayant la seule responsabilité on peut rêver à toutes les améliorations comme le stockage des remises dans une table, un fichier XML, proposer un écran de modification des taux et milles choses qui auraient été impossible avec la première écriture.
Le principe énoncé par le O de SOLID, Open Closed Principle, fait passer de l’âge de la modification à celui de l’extension.
C’est une évolution essentielle comme SRP pour construire un code de bonne qualité. Ce ne sont pas les seules règles à appliquer, mais sans elles ont peut être certain d’avoir un code bancal et difficile à maintenir même barder de Unit Testing à tous les coins de rue !
L comme Liskov
Le Liskov Subtitution Principle, principe de substitution de Liskov. Je dirais même de Barbara Liskov. Une femme, il faut donc le préciser dans ce monde de macho. Et même deux pour le prix d’une puisque c’est avec une copine à elle Jeannette Wing qu’elle a écrit un article dans lequel on retrouve le fameux principe. Je vous laisse lire la page Wikipédia dédié à ce dernier, c’est un peu technique mais précis, pour les exemple on va voir cela ensemble !
Supposons que nous souhaitons ajouter à notre gestion de clients la notion de prospect. Dans un premier temps et en fonction de paramètres décidés ailleurs, un prospect reçoit aussi une catégorie (selon le CA qu’il semble promettre de générer par exemple). C’est un objet de type client et il semble bien naturel de créer le prospect comme un descendant de client. Toutefois nous ne souhaitons pas que le prospect soit ajouté à la base de données, il faudra pour cela qu’il soit transformé en véritable client. Peu importe la réalité de cet exemple, ne compliquons pas trop les choses.
La première implémentation qui pourrait venir en tête (de quelqu’un qui ne connait pas SOLID !) serait quelque chose chose comme ça :
public static class Log
{ ... }
public static class CrudClient
{
public static int Insert(Client client)
{
if (client is Prospect) return;
try
{
//accès DAL ou autre
return 0;
} catch (Exception e)
{
Log.Error(e);
return -1;
}
}
}
public static class DiscountRate
{
public static double Normal = 0d;
public static double Silver = 5d;
public static double Vip = 10d;
public static double Prospect = 0d;
}
public class Client
{
public int Id {get; set;}
public string Name {get; set;}
public virtual double Discount { get {return DiscountRate.Normal;} }
}
public class ClientSilver : Client
{ ... }
public class ClientVip : Client
{ ... }
public class Prospect : Client
{
public override double Discount { get {return DiscountRate.Prospect;} }
}
Mes premières adaptations ont été tellement sévères qu’il m’est presque impossible de violer le principe de Liskov il faudrait repartir du premier code mal écrit pour que cela soit plus évident… En effet si la méthode Insert avait été laissée dans Client j’aurais démontré par un override de cette dernière qu’en déclenchant une exception j’interdisais l’écriture en base de données. Mais comme S et O ont été respectés à la lettre, Client n’a maintenant plus la responsabilité de Insert. De fait je ne peux mettre en avant la construction fautive comme je le voudrais.
Mais qu’à cela ne tienne, mon code correct m’oblige à aller bidouiller la méthode Insert de CrudClient pour tester si l’instance est de type Prospect pour éviter l’insertion… Tordu, donc mauvais de toute façon.
Le levage d’une exception dans Prospect aurait malgré été plus parlante car le principe de Liskov interdit que de nouvelles exceptions soient levées par une classe héritée. Mais la page Wikipédia indiquée plus haut vous dira tout ce qu’il faut savoir sur ce sujet.
Bref, ici je pense que Prospect est naturellement un descendant de Client puisqu’il en partage presque tout à la différence de la persistance.
Mais on le voit, en considérant un Prospect comme un Client cela pose des problèmes et notre code commence à contenir des curiosités qui le rendent bancal. Bricoler la méthode Insert de CrudClient n’est vraiment pas une bonne solution. Cela sera difficile à maintenir dans le temps. Et puis c’est une verrue. Horrible.
Le principe de Liskov dans la pratique est d’une grande subtilité, l’exemple de violation donné par Wikipédia est parfait et le démontre bien. J’ai un Rectangle avec une largeur et une hauteur, je suis tenté quand je créée la classe Carré d’en faire un enfant de Rectangle. Après un un Carré n’est qu’un cas particulier de Rectangle. Mais dans Carré hauteur et largeur ne peuvent évoluer seules. Ma classe contiendra donc du code pour empêcher les variations non coordonnées de ces valeurs.
Or en disant qu’un Carré est un Rectangle je sous-entend qu’on peut utiliser un Carré partout où un Rectangle l’est. L’utilisateur s’attend à manipuler un Rectangle et ne comprendra pas pourquoi dans l’interface il ne peut plus modifier comme il veut la largeur et la hauteur.
C’est en réalité une fausse bonne idée cet héritage.
En réalité tout comme notre couple Prospect / Client, le couple Carré / Rectangle sont des types totalement différents. Cela ne veut pas dire qu’on renie les relations de proximité, ni même qu’un prospect est un client en puissance pas plus qu’un carré n’est qu’un cas particulier de rectangle, non, mais il faut se résoudre à admettre que leur nature est différente. Un Prospect représente un prospect et non un client et un client représente un client et non un prospect.
Revenons à notre exemple. Comment traduire cette réalité en supprimant la verrue ajoutée à CrudClient ?
Il y a bien entendu plusieurs possibilités mais l’une d’entre elles est très élégante lorsqu’on a des problèmes d’héritage : l’utilisation d’interfaces.
La solution ci-dessous peut se discuter, comme tous les points d’architecture d’ailleurs, mais elle montre comment respecter le L de SOLID sans violer le S et O…
void Main()
{
var Tous = new List<IBase> ();
Tous.Add(new Client{Name="toto"});
Tous.Add(new ClientVip{Name="titi"});
Tous.Add(new Prospect{Name="fifi"});
}
public static class Log
{
public static void Error(Exception ex)
{
System.IO.File.WriteAllText(@"c:\temp\log.txt",ex.ToString());
}
}
public static class CrudClient
{
public static int Insert(Client client)
{
try
{
//accès DAL ou autre
return client.Id;
} catch (Exception e)
{
Log.Error(e);
return -1;
}
}
}
public static class DiscountRate
{
public static double Normal = 0d;
public static double Silver = 5d;
public static double Vip = 10d;
public static double Prospect = 0d;
}
public interface IBase {
string Name {get; set;}
double Discount {get;}
}
public interface IClient { int Id {get; set; } }
public class Client : IBase, IClient
{
public int Id {get; set;}
public string Name {get; set;}
public virtual double Discount { get {return DiscountRate.Normal;} }
}
public class ClientSilver : Client
{
public override double Discount { get {return DiscountRate.Silver;} }
}
public class ClientVip : Client
{
public override double Discount { get {return DiscountRate.Vip;} }
}
public class Prospect : IBase
{
public string Name {get; set;}
public double Discount { get {return DiscountRate.Prospect;} }
}
Le Main() montre qu’il est possible de manipuler à la fois des Clients et ses variantes et des Prospects. Ici une liste est constituée mélangeant client de base, client VIP et un prospect. On peut donc fournir des affichages triés, des listings etc sans problème.
On remarque que Prospect ne descend plus de Client mais ne fait qu’implémenter IBase, et c’est suffisant pour montrer un nom et un taux de remise…
La classe Client quant à elle supporte une seconde interface exposant l’ID par cohérente et pour signifier que seuls les clients sont persistés puisque c’est à ce moment là qu’ils reçoivent l’ID (en général l’ID unique de la base de données qui n’est souvent pas ce qu’ici on appellerait un “code client” qui peut aussi être unique mais qui a un sens fonctionnel et non technique comme l’ID unique de la base).
On remarque en outre que la classe CrudClient n’a pas changée, elle revient à ce qu’elle était, sans bricolage. Rien à faire. En effet, elle a été conçue pour persister des instances de Client. Or celles de Prospect ne peuvent être acceptées. Pas besoin de lever des exceptions ou autre magouille malsaine, la façon dont nous avons architecturé le logiciel rend tout simplement IMPOSSIBLE la confusion et donc la persistance d’un Prospect. Pas besoin de IF et encore moins de Unit Testing pour le comprendre. C’est le compilateur qui le refusera…
Que nous sommes loin du petit bout de code original que je vous invite à regarder à nouveau pour vous rendre compte de sa métamorphose !
I comme ISP
L’Interface Segregation Principe ou principe de ségrégation des interfaces vient tout naturellement compléter les principes déjà présentés.
Expliquer autrement l’ISP nous dit que les interfaces sont immuables. On n’ajoute pas après coup une méthode ou une propriété à une interface car dans ce cas là on casse tout le code qui utilise déjà l’interface. Or dans de nombreux cas, surtout en entreprise, il peut y avoir plusieurs équipes, plusieurs départements qui utilisent des DLL de base qui sont partagées. On ne sait jamais qui utilise quoi avec assurance. Modifier une interface c’est casser le code de quelqu’un d’autre avec des conséquences qu’on ne peut prévoir.
Imaginons donc maintenant que notre IBase doivent exposer un numéro de téléphone. Modifier IBase est un très mauvais choix et nous venons de voir pourquoi. Alors comment le gérer ? Simplement en créant une nouvelle interface. Les interfaces se versionnent par ajout d’une nouvelle interface.
public interface IBase {
string Name {get; set;}
double Discount {get;}
}
public interface IBaseV2 : IBase
{ string Phone { get; set; } }
public interface IClient { int Id {get; set; } }
public class Client : IBase, IClient
{
public int Id {get; set;}
public string Name {get; set;}
public virtual double Discount { get {return DiscountRate.Normal;} }
}
public class ClientSilver : Client
{...}
public class ClientVip : Client
{...}
public class Prospect : IBase, IBaseV2
{
public string Name {get; set;}
public double Discount { get {return DiscountRate.Prospect;} }
public string Phone {get; set; }
}
L’interface IBaseV2 est créée par héritage de IBase, elle ajoute la propriété Phone.
Client et ses descendants supportent toujours IBase alors que Prospect supporte IBase et IBaseV2.
Ce double support pourrait troubler, n’est-ce pas une répétition inutile ?
Non car on sait que du code existant utilise Prospect comme IBase, il ne faut pas supprimer ce support car nous casserions du code existant !
Et en ajoutant IBaseV2 nous n’avons rien à ajouter d’autre que le champ supplémentaire, IBaseV2 a des exigences qui sont déjà supportées par la classe qui supportait IBase. Le compilateur se débrouille très bien et n’impose bien entendu du pas de déclarer deux fois les mêmes propriétés ou méthodes ! (ce qui ne marcherait pas de toute façon).
Ici nous faisons gentiment évoluer notre code. Les nouvelles fonctionnalités utiliseront naturellement IBaseV2 et verront les clients et prospects sous cet angle là, les anciens programmes continueront à fonctionner normalement.
Là encore les bons choix d’architecture évite d’avoir à tester bêtement tous les méthodes de toutes les classes. Nous ne risquons pas d’introduire la moindre régression en travaillant de la sorte…
D comme DIP
Dependency Inversion Principe. Principe d’inversion de dépendance.
L’injection de dépendance est un sujet que j’ai déjà traité souvent je ne m’étendrais pas et je renverrai le lecteur intéressé à tous les articles qui abordent ce sujet comme par exemple (liste non exhaustive) :
Windows Phone, Android et iOS & Injection de dépendances et conteneurs IoC
MVVM, Unity, Prism, Inversion of Control…
l’IoC avec MvvmCross, de WinRT à Android en passant par Windows Phone
MVVM, Unity, Prism, Inversion of Control…
Dans notre exemple de code on pourrait par injection de dépendances passer au constructeur de CrudClient l'instance d'un Log, cela serait fait via un conteneur d'IoC par exemple. Le découplage serait toujours respecté. Mais tout cela ce sont les avantages déjà abordés de l'injection de dépendance, l'inversion de contrôle, etc.
Conclusion
Appliquer les principes SOLID est une base non négociable, encore faut-il comprendre chacun de ces principes !
J'espère que ce petit tour d'horizon vous permettra de mieux comprendre SOLID et de mieux architecturer votre code...
Stay Tuned !