Dot.Blog

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

Intégrité bi-directionnelle. Utiliser IEnumerable et des propriétés read-only (C#)

[new:16/10/2010]Un peu de C#, ça faisait longtemps que je n’avais pas bloggé sur le sujet. Aujourd’hui quelques points essentiels dans la conception des classes…

Relations entre entités et invariants : un exemple

Pour éviter de trop nous perdre dans les méandres des explications, le plus simple est de regarder directement le code ci-dessous :

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Province { get; set; }
    public List<Order> Orders { get; set; }
 
    public string GetFullName()
    {
        return LastName + ", " + FirstName;
    }
}
 
public class Order
{
    public Order(Customer customer)
    {
        Customer = customer;
        customer.Orders.Add(this);
    }
 
    public Customer Customer { get; set; }
}

La question est : qu’est-ce qui ne va pas avec ces deux classes ?

Setter sur une collection

La première chose qui ne va pas est la présence d’un setter sur la collection Orders de la classe Customer. On ne fait jamais ça (mais on le voit hélas souvent). N’importe qui peut remplacer l’objet collection lui-même et couper l’herbe sous les pieds de Customer. Ici l’exemple est simpliste, mais imaginez que Customer gère des événements propres à la collection…

Rendre le setter d’une collection publique, voire tout simplement lui adjoindre un tel setter quel que soit sa visibilité est le plus généralement une grosse erreur de conception.

Publier une liste concrète

Second problème toujours posé par cette liste Orders : elle est visible sous la forme de son implémentation concrète, à savoir List<Order>. Que se passera-t-il si pour faire évoluer notre code dans le futur nous souhaitons utiliser une ObservableCollection ou tout autre structure à la place de List<T> ? Tout le code dépendant de Customer sera à revoir !

On ne fait jamais cela non plus. Les collections de ce type, sauf obligation dûment commentée et justifiée, sont toujours publiées sous forme d’interfaces, par exemple IList<T>.

Relation bi-directionnelle instable.

Il est évident à la lecture du code de Order que le constructeur de cette classe établit une relation bi-directionnelle avec Customer. Publier un setter pour la propriété Customer est une erreur, n’importe quel code pourra modifier le client attaché à la commande de façon anarchique ce qui laissera l’ensemble des données dans un bel état !

La solution

Voici comment les deux classes pourraient être corrigées pour éviter les problèmes indiqués :

public class Customer
{
    private readonly IList<Order> _orders = new List<Order>();
 
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Province { get; set; }
    public IEnumerable<Order> Orders { get { return _orders; } }
 
    public string GetFullName()
    {
        return LastName + ", " + FirstName;
    }
 
    internal void AddOrder(Order order)
    {
        _orders.Add(order);
    }
}
 
public class Order
{
    public Customer Customer { get; protected set; }
 
    public Order(Customer customer)
    {
        Customer = customer;
        customer.AddOrder(this);
    }
}

Le setter sur la collection a été supprimé, la liste est publiée sous la forme d’une interface IEnumerable<Order> et le constructeur de Order oblige a passer un Customer et c’est ce constructeur qui établit de façon définitive le lien bi-directionnel entre les entités. De même la propriété Customer de Order possède désormais un setter “protected” évitant toute manipulation depuis l’extérieur.

Regardons de plus près

Il y a ici un autre invariant qui a été pris en compte : c’est le fait qu’une commande ne peut pas exister sans client. Le constructeur de Order donne corps à cet invariant puisqu’il n’est pas possible de créer une commande sans passer un Customer et que Order s’occupe d’appeler les méthodes de Customer pour assurer le lien bi-directionnel.

Imaginons un instant qu’après d’âpres discussions au sommet entre experts, il soit décidé qu’une commande ne peut pas non plus exister sans lignes de commandes.

Cela ne pose pas trop de problèmes puisque nous savons maintenant comment résoudre la question : en ajoutant un nouveau paramètre au constructeur de Order pour qu’une liste de lignes de commandes soit passée. On ajoutera aussi les appels nécessaires pour que le lien bi-directionnel soit assuré.

C’est là que tout cela pose un problème de conception… Parce que si notre API semble parfaite vue de l’extérieur, à l’intérieur ça va commencer à se compliquer et à devenir plus difficile à maintenir à force d’ajouter des paramètres, des appels à des méthodes ici et là…

Méthodes d’exentions, génériques, expressions Lambda et Reflexion

En voici une belle brochette !

On se met à rêver quelques instants et on se demande si devant le fatras qui s’annonce dans les classes ainsi modifiées on ne ferait pas mieux de prévoir tout cela dès le départ et de se créer une petite boite à outils versatile pour régler une bonne fois pour toute les problèmes soulevés…

Par exemple il serait vraiment sympa de pouvoir ajouter un item à un IEnumerable depuis une entité en relation sans avoir besoin d’appeler des méthodes internes, non ? Il pourrait être pratique de pouvoir modifier des propriétés protégées (non pas privées, là ça serait pousser le bouchon trop loin). Existe-t-il un moyen de créer un infrastructure simplifiant ensuite la prise en charge des problèmes soulevés à la fois par la première implémentation de Customer et Order et par l’implémentation de la solution qui en soulève d’autres ?

Comme nous ne souhaitons pas exposer des fonctionnalités aussi dangereuses que celles évoquées dans toute notre application, il s’agit juste de nous aider à créer des implémentations “propres” tout en préservant la clarté de notre API, nous allons ajouter tout cela dans notre Layer Supertype.

Layer Supertype ?

Dans ”Patterns of Enterprise Application Architecture” de Martin Fowler, la description de cette pattern est donnée page 475. Comme il n’existe pas de version française, le numéro de page devrait être le bon si vous possédez cet indispensable ouvrage chez vous…

En gros il n’est pas idiot pour tous les objets dans un même layer de posséder des méthodes que vous ne voulez pas dupliquer dans tout le système. Dans ce cas vous pouvez toutes les déplacer dans un Layer Supertype commun, une classe spécifique du layer en question qui regroupe donc tous les comportements utilisables ici et là dans le layer mais uniquement dans ce layer.

La lecture du livre de Fowler vous en dira bien plus que quelques lignes ici. C’est une pattern très intéressante (comme le reste du bouquin d’ailleurs) mais que je ne peux pas traiter en profondeur dans ce billet.

Les méthodes du SuperType

protected void SetInaccessibleProperty<TObj, TValue>(TObj target, TValue value,
    Expression<Func<TObj, TValue>> propertyExpression)
{
    propertyExpression.ToPropertyInfo().SetValue(target, value, null);
}
 
protected TValue GetInaccessibleProperty<TObj, TValue>(TObj target,
    Expression<Func<TObj, TValue>> propertyExpression)
{
    return (TValue)propertyExpression.ToPropertyInfo().GetValue(target, null);
}
 
protected void AddToIEnumerable<TEntity, TValue>(TEntity target, TValue value,
    Expression<Func<TEntity, IEnumerable<TValue>>> propertyExpression)
{
    IEnumerable<TValue> enumerable = GetInaccessibleProperty(target, propertyExpression);
 
    if (enumerable is ICollection<TValue>)
        ((ICollection<TValue>)enumerable).Add(value);
    else
        throw new ArgumentException(
            string.Format("Property must be assignable to ICollection<{0}>", typeof(TValue).Name));
}
 
protected void RemoveFromIEnumerable<TEntity, TValue>(TEntity target, TValue value,
    Expression<Func<TEntity, IEnumerable<TValue>>> propertyExpression)
{
    IEnumerable<TValue> enumerable = GetInaccessibleProperty(target, propertyExpression);
 
    if (enumerable is ICollection<TValue>)
        ((ICollection<TValue>)enumerable).Remove(value);
    else
        throw new ArgumentException(string.Format("Property must be assignable to ICollection<{0}>",
            typeof(TValue).Name));

Il est sûr que vu comme ça, au petit déjeuner, ça peut sembler un peu indigeste. Mais relisez ce code au calme, vous verrez ça a du sens :-)

Surtout, ce code va nous service à construire quelque chose de plus intelligent :

protected void AddManyToOne<TOne, TMany>(
    TOne one, Expression<Func<TOne, IEnumerable<TMany>>> collectionExpression,
    TMany many, Expression<Func<TMany, TOne>> propertyExpression)
{
    AddToIEnumerable(one, many, collectionExpression);
    SetInaccessibleProperty(many, one, propertyExpression);
}
 
protected void RemoveManyToOne<TOne, TMany>(
    TOne one, Expression<Func<TOne, IEnumerable<TMany>>> collectionExpression,
    TMany many, Expression<Func<TMany, TOne>> propertyExpression)
    where TOne : class
{
    RemoveFromIEnumerable(one, many, collectionExpression);
    SetInaccessibleProperty(many, null, propertyExpression);
}
 
protected void RemoveManyToMany<T1, T2>(
    T1 entity1, Expression<Func<T1, IEnumerable<T2>>> expression1,
    T2 entity2, Expression<Func<T2, IEnumerable<T1>>> expression2)
{
    RemoveFromIEnumerable(entity1, entity2, expression1);
    RemoveFromIEnumerable(entity2, entity1, expression2);
}
 
protected void AddManyToMany<T1, T2>(
    T1 entity1, Expression<Func<T1, IEnumerable<T2>>> expression1,
    T2 entity2, Expression<Func<T2, IEnumerable<T1>>> expression2)
{
    AddToIEnumerable(entity1, entity2, expression1);
    AddToIEnumerable(entity2, entity1, expression2);
}

Déjà on commence à voir l’intérêt de la manœuvre. Cette “seconde couche” exploite les première méthodes pour autoriser des comportements de plus haut niveau, notamment l’ajout et la suppression d’éléments à des IEnumerable sans avoir accès aux implémentations concrètes !

Vous noterez que toutes les méthodes sont “protected” donc uniquement utilisable dans les classes dérivées, celles du layer en cours qui descendent donc toutes du Layer SuperType…

En réalité il y a un petit morceau qui fait exception et qui est tellement pratique qu’il a été transformé en méthode d’extension :

public static class ExpressionExtensions
{
    public static PropertyInfo ToPropertyInfo(this LambdaExpression expression)
    {
        var prop = expression.Body as MemberExpression;
 
        if (prop != null)
        {
            var info = prop.Member as PropertyInfo;
            if (info != null)
                return info;
        }
 
        throw new ArgumentException("The expression target is not a Property");
    }
}

Ce code est très simple, par le biais de la Reflection il permet d’atteindre une propriété passée sous la forme d’une expression Lambda et de modifier son contenu. C’est un peu tordu mais c’est très utile, vous allez le voir dans l’exemple ci-dessous.

La solution améliorée par le SuperType et ses méthodes

Le SuperType s’appelle DomainBase, une convention que chacun pourra utiliser ou non (mais après avoir lu le livre de Fowler !).

public class Customer : DomainBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Province { get; set; }
    public IEnumerable<Order> Orders { get; protected set; }
 
    public string GetFullName()
    {
        return LastName + ", " + FirstName;
    }
}
 
public class Order : DomainBase
{
    public Customer Customer { get; protected set; }
 
    public Order(Customer customer)
    {
        AddManyToOne(customer, x => x.Orders, this, x => x.Customer);
    }
}

La nouvelle implémentation a tout de même fière allure ! Elle est plus simple à lire, son API est aussi claire que son contenu et nous avons préparé le terrain avec le SuperType pour régler de façon aussi élégante les mêmes problèmes dans tout le layer concerné !

Conclusion

Comme toute solution générique démontrée sur un pauvre ensemble de deux mini classes, forcément, on semble utiliser un tank pour tuer une mouche.

Si votre logiciel ne contient qu’une classe Customer et qu’une classe Order comme ici, alors nous ne parlons pas du même type de logiciel et je vous présente mes excuses, vous n’en êtes certainement qu’au second ou troisième chapitre de “C# pour les Nuls”. Je ne pouvais pas le deviner :-)

Bien entendu, pour tous les autres lecteurs, nous savons tous que ce type de solution ne prend son intérêt que dans de larges solutions exposants de dizaines voire des centaines de classes.

La solution présentée ici a un impact sur les performances, c’est certain. De combien ? Je n’ai pas mesuré. Mais si faire la balance entre performances brutes et code maintenable est toujours quelque chose de grave et délicat, j’opte systématiquement pour la maintenabilité et la clarté du code. D’autres développeurs préféreront dupliquer du code partout dans le fol espoir de gratter quelques millisecondes. Chacun ses choix, cela ne me dérange pas. Mais je serai curieux de voir alors combien de lignes dupliquées contiendra ce code et au final combien de clients et de commandes un tel code sera capable de traiter réellement par secondes…

Bref, si l’idée de Fowler du SuperType est à exploiter sans trop de contrainte, les transformations à outrance présentées ici mélangeant Expression Lambda, Reflexion, méthodes d’extensions le tout à la sauce générique sont plutôt à prendre comme des possibilités et des idées d’implémentation ouvrant de nouvelles façons de concevoir des couches riches en classes (un BOL, un DAL par exemple). Tout n’est peut-être pas à prendre au pied de la lettre et le but du jeu était principalement de vous faire réfléchir à ces possibilités.

Si ce but est atteint alors c’est une bonne chose !

PS: j’ai donné les coordonnées du livre de Fowler par l’hyperlien qu’il suffit de suivre (ça tombe chez Amazon France pour le commander). Concernant le billet lui-même je me suis inspiré d’un publication de Sean Blakemore, non par paresse ou manque d’idée mais parce qu’il me semblait que son propos valait largement l’intérêt d’être proposé dans notre belle langue pour en faire profiter tous les lecteurs de Dot.Blog qui, je le sais, ne sont pas tous des amis de l’anglais…

Stay Tuned !

blog comments powered by Disqus