Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les modèles anémiques

Poster un commentaire

Ce terme désigne un style de programmation en général considéré comme une mauvaise pratique. On va dans cet article essayer de comprendre de quoi il s’agit et comment faire évoluer un domaine anémique vers un domaine riche.

 Et d’abord c’est quoi un domaine?

Oui au fait.

C’est un terme typique du domaine de l’architecture logicielle. Le domaine que l’on appelle aussi le modèle est un ensemble de classes et de code qui partagent un but commun, une même responsabilité.

Par exemple dans une application de type vente en ligne va proposer différentes fonctionnalités:

  1. Présenter des produits
  2. Proposer une fonction de  panier
  3. Avoir un tunnel de commande
  4. Proposer un suivit de la commande au client
  5. Faire du merchandizing

Chacune de ces fonctionnalité va correspondre à un métier particulier chez le client, disposer de son propre vocabulaire, de son propre savoir-faire….

Nous, le développeur, on va modéliser tout cela avec des classes et du code. Chacune des 5 fonctionnalités précédentes va donc exister dans son propre ensemble de code. C’est cet ensemble que l’on appelle un modèle.

Ma définition est bien jolie, mais elle passe sous silence la question de l’étendue de cette définition:

  • l’application toute entière peut-elle être considérée comme un domaine?
  • Si je considère la fonctionnalité « tunnel de commande », puis-je considérer que livraison et paiement sont deux domaines différents ou bien faut -il les regrouper dans un domaine « tunnel de commande »

En tant qu’architecte vous vous poserez en permanence ce genre de question… et vous découvrirez qu’il n’y a jamais de réponses définitives.

Restez donc zen et décontracté, n’ayez pas peur de vous tromper, faites comme vous sentez les choses.

Peut être bien qu’un développeur junior viendra vous faire remarquer que …. et que donc ceci aurait été mieux. Et après? Vous aurez au moins appris un truc. Par ailleurs un choix d’architecture c’est aussi un choix de compromis. Ils peuvent être justifiés à un certain moment et moins à a version N+X du projet. Vous êtes pas devin non plus, juste architecte.

Je dirai juste qu’un domaine ne correspond pas à une micro fonctionnalité (par exemple: ajouter la TVA), mais plutôt à un ensemble de compétences spécifiques. Il faut également que ce soit un tout et un tout cohérent. Le block envoie d’email + impression de facture ne constitue pas un domaine a priori.

En programmation objet on sépare les domaines en couches logiques: les espaces de noms. Si deux ensembles se trouvent dans le même espace de noms principal, il s’agit probablement du même domaine.

Si vous pouvez imaginer séparer deux ensembles dans deux librairies différentes sans lien entre elles, vous avez probablement affaire à deux domaine.

Libre à vous de trouver plus pratique de découper en sous-ensembles plus petits et baptiser ça également le domaine de XXXXX. Le bon sens est votre meilleur guide: est-ce logique de mettre dans le même sac ces deux fonctionnalités?

Aparté: dans la littérature on rencontre les termes tiers et layer. A ne pas confondre. Tiers correspond à une couche physique (deux dll différentes), tandis que layer est une séparation logique. Les deux termes ne sont d’ailleurs pas exclusifs.
Il n’y a aucune obligation a priori à ce qu’un domaine soit disposé dans une couche physique spécifique.

Remarque: vous avez du constater que mon approche du domaine a en fait une granularité plus fine de celle d’autres architectes. Pour moi un domaine est essentiellement une collection de sous domaines qui renvoient à une fonctionnalité particulière.
Je trouve plus logique et intéressant de raisonner ainsi.

Premier contact avec un domaine anémique

Premier contact est vite dit, nous en avons tous déjà écrits sans le savoir et pire: on trouvait cela très bien!

Un domaine anémique est un domaine dont les classe contiennent essentiellement des propriétés. Elles ne gèrent pas elle-même leurs états, elles n’assurent pas elle-même la cohérence de leurs états.

Le mieux est d’examiner un exemple concret, du code quoi!

On va modéliser une classe commande (Order) et un détail de la commande qui sera pour l’essentiel une collection de OrderItem.

Voici nos classes:

public class Order
{
 public Order()
 {
	Items = new List<OrderItem>();
 }

 public double TotalOrder { get; set;}
 public List<OrderItem> Items { get; private set; }
}

La classe Order est pour l’essentiel une collection de détails de la commande. Ces détails sont modélisés par la classe OrderItem ci-dessous:

public class OrderItem

{

    public double UnitPrice { get; set; }

    public int Quantity { get; set; }

    public string Name { get; set; }

}

Des classes vraiment lights.

Bien sûr on a besoin de les faire vivre, on a par exemple besoin de calculer le montant de la commande. Dans le modèle anémique cette fonction n’est pas hébergée dans les classes, mais dans une classe à part que l’on appelle souvent: Service, Manager….

On trouve donc quelque chose comme:

public static class OrderService

{

    public static void CalculateTotal(Order order)

    {

        double totalOrder = 0;

        foreach (OrderItem orderItem in order.Items)

        {

            double itemTotal = orderItem.UnitPrice * orderItem.Quantity;

            totalOrder += itemTotal;

        }

         order.TotalOrder = totalOrder;

    }

}

Qui se charge de calculer le montant de la commande comme ceci:

Order order = new Order();
OrderItem item = new OrderItem();
item.Name = « Livre »;
item.Quantity = 2;
item.UnitPrice = 50;
 
order.Items.Add(item);
OrderService.CalculateTotal(order);
Console.WriteLine(order.TotalOrder);

L’idée de ce modèle est de séparer la question du stockage des états de sa manipulation. L’idée n’est pas en soi mauvaise, elle est juste mal exploitée.

Mal exploitée parce que le code pourrait se poursuivre de la façon suivante:

item.Quantity = 5;

// oula, c’est faux!

Console.WriteLine(order.TotalOrder);

On a juste oublié d’appeler à nouveau CalculateTotal. C’est tout le problème, une architecture qui repose sur le fait que le développeur va appeler au bon moment une certaine procédure pour que ça marche n’est pas une bonne idée, mais plutôt une machine à bugs.

C’est quoi au juste qui cloche la dedans?

Ce qui cloche est que Order expose des états, mais n’a aucun moyen de garantir que ceux-ci sont exacts. Ceci est précisément un contre-sens dans la programmation objet: un objet EST responsable de ses états sinon ce n’est pas un véritable objet. En fait un objet dont on ne peut pas faire confiance à ses états n’a pas bien grande utilité.

Par ailleurs, un objet ce n’est pas juste des propriétés, mais aussi des méthodes.

Nous avons rien de tout cela ici.

Remarque: évidemment on ne parle pas de domaine anémique parce que dans un domaine UNE classe particulière n’a que des propriétés. Ce n’est pas non plus bien important que dans le domaine un sous domaine particulier n’ai que des classes de type DTO, ça peut même arriver. Il faut vraiment voir les choses au plus haut niveau: est-ce une caractéristique générale du code ou bien juste un incident localisé? Existe t’il quelque part ailleurs un code que l’on aurait pu/du mettre dans ces classes de la façon que nous verrons dans cet article?

 Une première idée

Une première idée est de réécrire nos classes de cette manière:

public class Order

{

    public Order()

    {

        Items = new List<OrderItem>();

    }

    public void Add(OrderItem item)

    {

        Items.Add(item);

        IsDirty = true;

    }

    private double Total;

    public double GetTotal()

    {

        if (IsDirty)

        {

            double orderItemTotal = 0;

            foreach (var item in Items)

            {

                orderItemTotal += item.Total;

            }

            Total = orderItemTotal;

            IsDirty = false;

        }

        return Total;

    }

    public List<OrderItem> Items { get; private set; }

    private bool IsDirty { get; set; }

}

Et (mais sur le fond ce n’est pas indispensable):

public class OrderItem

{

    public double UnitPrice { get; set; }

    public int Quantity { get; set; }

    public string Name { get; set; }

    public double Total

    {

        get

        {

            return UnitPrice * Quantity;

        }

    }

}

Plus besoin de pseudo-classe service, Order se charge de faire le travail et au fond il est tout de même assez logique qu’une classe Order sache afficher un total de commande.

Notez l’emploi de la propriété IsDirty qui évite de refaire le calcul à chaque appel de GetTotal(). Nous avons également enrichit OrderItem d’une propriété qui calcule le montant de l’article acheté afin de simplifier le travail de GetTotal.

On pourrait alors écrire ceci:

Order order = new Order();

OrderItem orderItem = new OrderItem();

orderItem.Name = « Livre »;

orderItem.Quantity = 2;

orderItem.UnitPrice = 50;

order.Add(orderItem);

Console.WriteLine(order.GetTotal());

Code que je trouve sur le fond plus naturel que le précédent. En tout cas si vous aviez à le développer vous même en vous appuyant sur les classes existantes, il semble plus logique de rechercher avec IntelliSense une méthode genre GetTotal() qu’une classe OrderService.

Mais à t’on pour autant résolu nos problèmes?

J’ai tenu à passer par cette étape intermédiaire, parce qu’un tel code est en fait assez fréquent. Moi-même j’en ai écrit des tas. Mais ce code n’est pas correct car les états de Order ne sont pas suffisamment contrôlé.

Regardez comment on pourrait prolonger ce code:

// les ennuis commencent

orderItem.Quantity = 5;

Console.WriteLine(order.GetTotal()); 

// partez pas, y a ça aussi

order.Items[0].Quantity = 7;

Console.WriteLine(order.GetTotal());

Bien sûr on pourrait corriger le problème en supprimant le IsDirty et en refaisant le calcul à chaque fois. Mais c’est dommage et mieux vaut résoudre le fond du problème qui est que Order doit gérer lui-même ses états.

De plus c’est assez fragile comme construction. Dans un exemple moins trivial que celui-ci on n’est pas à l’abris qu’un développeur tente une « optimisation » d’apparence anodine, sauf qu’elle met en panne du code qui tourne dans une appli qui n’a rien à voir à part utiliser votre dll.

Bien sûr on a une violation flagrante du principe ouvert/fermé, mais que pouvez vous y faire?

Un vrai modèle riche

On va réécrire notre code ainsi:

public class Order

{

    public Order()

    {

        Items = new List<OrderItem>();

    }

    private List<OrderItem> Items { get; set; }

    private bool IsDirty { get; set; }

    private double total;

    public double GetTotal()

    {

        if (IsDirty)

        {

            double orderItemTotal = 0;

            foreach (var orderItem in Items)

            {

                double itemTotal = orderItem.Total;

                orderItemTotal += itemTotal;

            }

            total = orderItemTotal;

            IsDirty = false;

        }

        return total;

    }

    public void AddItem(OrderItem orderItem)

    {

        if (orderItem == null)

        {

            throw new ArgumentException(« orderItem non null »);

        }

        Items.Add(orderItem);

        IsDirty = true;

    }

    public ReadOnlyCollection<OrderItem> GetItems()

    {

        return new ReadOnlyCollection<OrderItem>(Items);

    }

}

Et:

public class OrderItem
{
    public OrderItem(double price, int quantity, String name)
    {

        if (name == null)
        {
            throw new ArgumentException(« name non null »);
        }

        if (price < 0)
        {
            throw new ArgumentException(« price >=0 »);
        }

        if (quantity < 1)
        {
            throw new ArgumentException(« quantity >=1 »);
        }

        UnitPrice = price;
        Quantity = quantity;
        Name = name;
    }

    public double UnitPrice { get; private set; }
    public int Quantity { get; private set; }
    public string Name { get; set; }

    public double Total
    {
        get 
        { 
            return UnitPrice * Quantity; 
        }
    }
}

Cette fois les propriétés qui peuvent affecter l’état de notre modèles exposent un setter private. Seul le constructeur permet de les alimenter.

Order conserve IsDirty et c’est très bien. Mais il protège la collection des articles dans une collection en lecture seule.

Vous pouvez vérifier, cette fois notre domaine composé de Order et OrderItem est cohérent en étant capable de garantir la fiabilité de ses états.

Mieux encore, on n’a plus affaire à un modèle anémique. Nos classes ont besoin de code pour s’auto gérer. Il faudrait en ajouter d’autres pour supprimer un article, le modifier….

Un modèle trop riche?

L’idée qui fait que l’on créée des modèles anémiques est en fait une bonne idée en soit: séparer les données des traitements. Après tout ce principe de séparation des responsabilités n’est t’il pas ce que l’on nous demande depuis des années?

De ce point de vue le modèle précédent peut paraître trop riche. Et pire encore il ressemble à une remise en cause de notre principe de séparation.

Comment sortir par le haut de cette situation embarrassante?

En remarquant qu’en réalité notre principe de séparation est mal exploité: on n’a pas utilisé la bonne architecture.

Je ne vais pas aborder le problème dans cet article qui est déjà bien long. Mais sachez que des architecture comme l’injection de dépendance est probablement une meilleure solution à votre problème: votre classe va rester capable de gérer elle-même ses états, mais vous pourrez externaliser le code qui fait effectivement le travail dans une autre dll, voire même l’écrire des années plus tard!

Je voudrai juste parler d’un point de détail certes, mais dont à mon avis pas assez d’architectes se préoccupent: a t’on sur le fond vraiment besoin de monter dans une architecture le calcul d’un total commande?

Ce que je veux dire est: la façon dont on calcule le total de la commande peut t’elle être une règle métier? Y a t’il X façons de faire les choses, quand bien même en aura t’on besoin?

A chacun de trouver sa réponse, mais avant de mettre en place une plomberie lourde pour injecter des dépendances de partout demandez vous si c’est vraiment utile.

Je parlais de compromis, et le travail de l’architecte n’est pas de trouver comment faire rentrer chaque ligne de code dans un pattern, mais de trouver les bons compromis qui vont permettre de livrer le produit à la date prévue compte tenu des contraintes externes et internes au projet.

Je ne fais pas la promotion de l’architecture spaghetti, mais je dis juste que l’architecture stratosphérique n’a autant que je sache aucune success story.

Voilà

L’idée de cet article (et certains exemples) doit beaucoup à un blog que j’ai lu il y a quelques temps, mais pas moyen de retrouver son lien pour le créditer. Désolé!

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s