lundi 2 novembre 2015

Persister un pur domain model : le pattern State-Interface

Dans le précédent article, nous avons vu comment mettre en place un compromis sur la conception objet afin d'obtenir rapidement de la persistance sur des applications dites "court terme". Nous allons maintenant aborder le pattern State-Interface qui permet la persistance de modèles du domaine parfaitement encapsulés, notamment dans le contexte d'applications plus importantes.

Sommaire


Le pattern State-Interface

Ce pattern est composé de 3 acteurs :
  • Un Domain Model : classe qui contient de la logique métier lié à un domaine
  • Un Persistent Model : classe responsable de la persistance des informations du domaine
  • Une interface IModelState représentant les états commun entre Domain Model et Persistent Model. Les états du domaine à persister.

Voici un petit schéma pour résumer les interactions de ces acteurs.


Le pattern State-Interface se décompose en 4 étapes :
  1. Déterminer les états du domaine à persister et en faire une interface IModelState
  2. Implémenter explicitement l'interface IModelState dans le Domain Model
  3. Implémenter normalement (implicitement) l'interface IModelState dans un Persistant Model
  4. Implémenter la copie d'information d'un IModelState à un autre IModelState


Adaptons l'exemple

Nous allons faire une nouvelle implémentation de notre exemple de commande de produits, en utilisant le pattern State-Interface. Nous utiliserons EntityFramework comme outil de persistance.

Dans un premier temps, nous devons définir les interfaces qui représentent les données à persister :
    public interface IOrderStates<TOrderLine> where TOrderLine : IOrderLineStates
    {
        Guid Id { get; set; }
        OrderStatus OrderStatus { get; set; }
        DateTime? SubmitDate { get; set; }
        double TotalCost { get; set; }
        IEnumerable<TOrderLine> Lines { get; set; }
    }
et
    public interface IOrderLineStates
    {
        Product Product { get; set; }
        int Quantity { get; set; }
        DateTime CreationDate { get; set; }
    }
Nous implémentons de manière explicite IOrderStates dans la classe Order. De cette manière, nous n'exposons pas au monde extérieur ses données :
    public class Order : IOrderStates<OrderLine>
    {
        Guid IOrderStates<OrderLine>.Id
        {
            get { return Id; }
            set { Id = value; }
        }
        OrderStatus IOrderStates<OrderLine>.OrderStatus
        {
            get { return _orderStatus; }
            set { _orderStatus = value; }
        }
        DateTime? IOrderStates<OrderLine>.SubmitDate
        {
            get { return SubmitDate; }
            set { SubmitDate = value; }
        }
        double IOrderStates<OrderLine>.TotalCost
        {
            get { return TotalCost; }
            set { TotalCost = value; }
        }
        IEnumerable<OrderLine> IOrderStates<OrderLine>.Lines
        {
            get { return _lines.ToArray(); }
            set { _lines = value.ToList(); }
        }
        
        // ...
    }
De même pour la classe OrderLine avec l'interface IOrderLineStates :
    public class OrderLine : IOrderLineStates
    {
        int IOrderLineStates.Quantity
        {
            get { return Quantity; }
            set { Quantity = value; }
        }
        DateTime IOrderLineStates.CreationDate
        {
            get { return _creationDate; }
            set { _creationDate = value; }
        }
        Product IOrderLineStates.Product
        {
            get { return Product; }
            set { Product = value; }
        }
        
        // ...
    }
Puis dans la classe OrderPersistantModel dédiée à la persistance, nous implémentons IOrderStates de manière classique :
    public class OrderPersistentModel : IOrderStates<OrderLinePersistentModel>
    {
        public IEnumerable<OrderLinePersistentModel> Lines { get; set; }
        public Guid Id { get; set; }
        public OrderStatus OrderStatus { get; set; }
        public DateTime? SubmitDate { get; set; }
        public double TotalCost { get; set; }
        public List<OrderLinePersistentModel> Lines { get; set; }
    }
De même pour la classe OrderLinePersistentModel avec l'interface IOrderLineStates 
    public class OrderLinePersistentModel : IOrderLineStates
    {
        public Product Product { get; set; }
        public int Quantity { get; set; }
        public DateTime CreationDate { get; set; }

        // EF properties
        public Guid OrderId { get; set; }
    }

Remarque: comme nous utilisons EntityFramework, nous devons ajouter un champs OrderId permettant de gérer la relation de la table OrderLine vers Order.

Nous pouvons maintenant, écrire le mapping EntityFramework directement sur les classes de persistance. Aucun problème ici car les propriétés sont toutes accessibles en lecture/écriture.

Mapping OrderPersistentModel:
    public class OrderMapping : EntityTypeConfiguration<OrderPersistentModel>
    {
        public OrderMapping()
        {
            this.ToTable("Order");
            this.HasKey(x => x.Id);
            this.Property(x => x.OrderStatus);
            this.Property(x => x.TotalCost);
            this.Property(x => x.SubmitDate);
            this.HasMany(x => x.Lines).WithRequired().HasForeignKey(x=>x.OrderId);
        }
    }

Mapping OrderLinePersistentModel:
    public class OrderLineMapping : EntityTypeConfiguration<OrderLinePersistendModel>
    {
        public OrderLineMapping()
        {
            this.ToTable("OrderLine");
            this.HasKey(x => new {x.OrderId, x.Product});
            this.Property(x => x.OrderId);
            this.Property(x => x.Product);
            this.Property(x => x.Quantity);
            this.Property(x => x.CreationDate);
        }
    }
Maintenant que nous avons déclarer nos classes de mappings, nous pouvons implémenter le repository de la classe Order :
    public class EntityFrameworkOrderRepository : IOrderRepository
    {
        private readonly IOrderMapper _orderMapper;

        public EntityFrameworkOrderRepository(IOrderMapper orderMapper)
        {
            _orderMapper = orderMapper;
        }

        public Order Get(Guid id)
        {
            using (var dataContext = new DataContext()) {
                var persistentModel = dataContext
                    .Set<OrderPersistentModel>()
                    .Include("Lines")
                    .FirstOrDefault(x => x.Id == id);

                if (persistentModel == null) {
                    return null;
                }
                return _orderMapper.ToDomainModel(persistentModel);
            }
        }

        public void Add(Order order)
        {
            var persistentModel = _orderMapper.ToPersistentModel(order);
            using (var dataContext = new DataContext()) {
                dataContext.Set<OrderPersistentModel>().Add(persistentModel);
                dataContext.SaveChanges();
            }
        }
    }
Vous avez surement remarqué le méthode ToDomainModel et ToPersistentModel de l'interface IOrderMapper. C'est la dernière étape qui consiste à convertir un Order vers un OrderPersistentModel et réciproquement. Même chose pour les concepts OrderLine et OrderLinePersistentModel. Ce qui nous intéresse ici est de copier les informations de l'interface IOrderStates et IOrderLineStates.

Pour cela, plusieurs solutions :
  • AutoMapper : petite librairie permettant de faire de la copie d'objets.
  • Copie à la main via une méthode d'extension sur l'interface IOrderStates et IOrderLineStates
Ce code technique n'est pas très intéressant. Si vous souhaitez en savoir plus, vous pouvez jeter un coup d’œil sur l'exemple complet (Cf fin de l'article).

Un très bon article de blog d'Aurélien Boudoux présente une manière de faire grâce à une interface IMergeableCopy qui utilise la reflection pour trouver les champs d'une interface à copier.


Critique de cette approche


Avantages :
  • La classe Order reste parfaitement encapsulée 
  • Il y a une séparation claire entre le modèle du domaine et le modèle de persistance (Domain model vs Persistant model) 
  • Les méta-données requises par les ORM pour permettre la persistance sont ajoutées dans les Persistent Models et ne polluent pas nos Domain Models.

Inconvénients :
  • Une interface est sensée représenter un comportement. L'utiliser pour masquer l'état interne d'un objet n'est pas très propre. 
  • Le code d'implémentation implicite n'est pas très lisible dans l'objet métier Order. 
  • Une partie de la stratégie repose sur une copie de données d'une interface à une autre, un code technique à peu de valeur ajoutée.


Conclusion

Ce pattern est particulièrement intéressant car il crée une nette séparation entre Domain Models et Persistent Models. Nous masquons ainsi les problématiques de persistance à nos objets métiers. Nous pouvons ainsi tisser un domaine métier complexe, à travers l’interaction de nombreuses classes purement concentrées l'expression du métier.

Cependant, l'implémentation explicite de l'interface est un code technique sans valeur ajoutée qui complexifie la lisibilité de notre domaine. De plus, si à chaque objet du domaine, nous devons lier un objet de persistance, nous pouvons rapidement obtenir un très grand nombre de classes à gérer.

Tout n'est pas parfait dans ce pattern, mais je vous recommande de le tester afin de forger votre propre point de vue.

En résumé : vous êtes libre de designer vos objets du domaine comme vous le souhaitez, ... tant que vous respecter les interfaces de données.


Code

Vous pouvez retrouver l'intégralité de l'exemple avec EntityFramework et Dapper sur Github ici : https://github.com/pierregillon/DomainModelPersistencePatterns/tree/master/Patterns/StateInterface


La suite

Une autre solution intéressante et qui se rapproche beaucoup du pattern State-Interface, se nomme le State-Shapshot. Une différence ? Plus d'implémentation explicite dans nos classes métiers !

Persister l'état interne d'un objet via le pattern State-Snapshot <<



Persister un pur domain model : compromis sur la conception objet

Comme nous l'avons vu dans l'article précédent, il est compliqué de persister un pur modèle du domaine, qui suit les principes de POO, SOLID, Tell don't ask. Quelles solutions permettent de répondre à ces besoins de bonne conception et de persistance ? La première solution simple consiste à faire un compromis sur la conception objet.


Sommaire


Un compromis

Dans beaucoup de cas, nous souhaitons faire une application simple. Quelques classes, quelques centaines de lignes de code, c'est tout. Il est souvent préférable de faire un compromis sur la conception objet et d'en exposer une partie des états internes afin d'en faciliter la persistance, via un ORM par exemple.

Tant que la complexité du domaine n'est pas importante, ce compromis peut faire gagner beaucoup de temps et reste "contrôlable".


Adaptons l'exemple

Nous allons faire une nouvelle implémentation de notre exemple de commande de produits, en utilisant un compromis sur la conception objet. Nous utiliserons EntityFramework comme outil de persistance.

Faire un compromis est très simple : exposer les états internes de notre Order, via des propriétés publiques accessibles en lecture et écriture (get/set en public). Voilà le résultat :

    public class Order
    {
        public Guid Id { get; set; }
        public OrderStatus OrderStatus { get; set; }
        public DateTime? SubmitDate { get; set; }
        public double TotalCost { get; set; }
        public List<OrderLine> Lines { get; set; }

        // ...
    }

Je n'ai pas repris le code métier qui est exactement identique à la précédente classe Order, (cf article d'introduction). Ce n'est pas le but ici.
Une fois les propriétés définies, nous pouvons persister la classe Order via EntityFramework, en déclarant un simple mapping :

    public class OrderMapping : EntityTypeConfiguration<Order>
    {
        public OrderMapping()
        {
            this.ToTable("Order");
            this.HasKey(x => x.Id);
            this.Property(x => x.OrderStatus);
            this.Property(x => x.TotalCost);
            this.Property(x => x.SubmitDate);
            this.HasMany(x => x.Lines).WithRequired().HasForeignKey(x=>x.OrderId);
        }
    }

Idem pour la sous-classe OrderLine : nous exposons ses états internes, via des propriétés publiques :

    public class OrderLine
    {
        public DateTime CreationDate { get; set; }
        public Product Product { get; set; }
        public int Quantity { get; set; }
        public Guid OrderId { get; set; }

        // ...
    }

Avec sa classe de mapping :

    public class OrderLineMapping : EntityTypeConfiguration<OrderLine>
    {
        public OrderLineMapping()
        {
            this.ToTable("OrderLine");
            this.HasKey(x => new {x.OrderId, x.Product});
            this.Property(x => x.OrderId);
            this.Property(x => x.Product);
            this.Property(x => x.Quantity);
            this.Property(x => x.CreationDate);
        }
    }

Une fois les mappings écrits, un repository de la classe Order s’implémente de la manière suivante :

    public class EntityFrameworkOrderRepository : IOrderRepository
    {
        public Order Get(Guid id)
        {
            using (var dataContext = new DataContext()) {
                return dataContext
                    .Set<Order>()
                    .Include("Lines")
                    .FirstOrDefault(x => x.Id == id);
            }
        }

        public void Add(Order order)
        {
            using (var dataContext = new DataContext()) {
                dataContext.Set<Order>().Add(order);
                dataContext.SaveChanges();
            }
        }
    }

EntityFramework fait ensuite le reste, il s'occupe de mapper les propriétés publiques directement dans les colonnes des tables appropriées.


Critique de cette approche

Cette solution est très simple et fonctionnelle. Nous pouvons désormais persister nos objets du domaine Order et OrderLine. Mais à quel prix ? Il est important d'en connaître les avantages et les inconvénients.

Avantages :
  • Simplicité d'implémentation = gain de temps
  • Un seul modèle pour la logique métier et la persistance : simplicité, pas de mapping vers d'autres modèles

Inconvénients :
  • Les états internes peuvent désormais être modifié depuis l'extérieur, sans évaluer les règles métiers définies dans les méthodes.
  • Notre modèle du domaine devient un modèle de persistance, couplé à la structure de la table Order.
  • Les méta-données requises par les ORM pour permettre la persistance sont ajoutées directement dans les modèles du domaine.

Conclusion

Pour certains puristes, ce compromis est une fausse solution. Je considère que cela dépend. De quoi ? Du contexte bien sûr.
Quand cette solution est adoptée de manière réfléchie, en toute connaissance de cause, et uniquement sur des projets de taille réduite, elle atteint ses objectifs sans nécessité de complexifier la stratégie de persistance.

Un prototype, un proof of concept, un début d'application, nous ne sommes pas obligé de commencer par un design de persistance avancé. Restons pragmatique. Le plus important est de bien concevoir son application, de la tester, afin d'adapter l'architecture au fur à mesure des nouveaux besoins. Tant que la complexité du domaine n'est pas importante, ce compromis peut faire gagner beaucoup de temps.

Malheureusement, aujourd'hui, cette solution est trop souvent systématisée, sans avoir conscience de ses gros inconvénients. J'ai travaillé avec plusieurs clients ayant conçus de bonnes architectures mais dont la couche d'infrastructure et de persistance n'ont pas été réfléchies. Des centaines de classes métiers se transforment en modèles anémiques (Anemic Domain Model Antipattern) et la logique métier se voit alors déportée dans des services ou même dans des méthodes d'extension ! Ce résultat est souvent provoqué par la mauvaise utilisation des ORMs, notamment EntityFramework pour le monde .NET.

Pour résumer, le compromis sur la conception objet pour permettre la persistance de modèles métiers,  est à utiliser, avec parcimonie ; principalement dans le cadre de petits projets avec un domaine métier à faible complexité.


Code

Vous pouvez retrouver l'intégralité de l'exemple avec EntityFramework et Dapper sur Github ici : https://github.com/pierregillon/DomainModelPersistencePatterns/tree/master/Patterns/Compromise


La suite

Fini, les solutions temporaires, place aux solutions de long terme. Le prochain article concernera le pattern State-Interface permettant d'extraire et de persister les états internes d'un objet en concervant une bonne encapsulation.

Persister un pur domain model : pas si simple !

En programmation orienté objet (POO), nous avons l'habitude de manipuler des objets qui représente des instances du monde réel. Nous essayons de les concevoir le plus fidèlement possible, afin de résoudre plus facilement les problématiques liées au métier dans lequel nous évoluons.

Comme un système ne s'exécute que pour une durée déterminée, il nous faut persister ces objets du domaine dans une structure de données afin de sauvegarder l'état général du système. Ainsi, à n'importe quel moment, nous pourrons restaurer le système dans l'état où nous l'avions laissé.

Avez-vous déjà conçu un modèle parfaitement encapsulé, puis été obligé de le corrompre afin de permettre sa persistance ? Comment éviter d'exposer les états internes de ses objets ?


Exemple de modèle bien encapsulé

Pour bien comprendre cette problématique, prenons un exemple simple. Nous nous y référerons tout au long de cet article. Soit un ensemble de classes permettant de commander des produits. Voici un petit schéma relationnel :



Un commande (Order) est composé de plusieurs lignes (OrderLine) qui définie chacune un produit (Product) avec une quantité. Un catalogue de prix (PriceCatalog) permet de trouver le prix d'un produit.

Par la suite, nous n'utiliseront que les termes anglais.

On peut définir l'objet métier Order sous forme d'une interface :

    public interface IOrder
    {
        Guid Id { get; }
        double TotalCost { get; }
        DateTime? SubmitDate { get; }

        void AddProduct(Product product, int quantity);
        void RemoveProduct(Product product);
        int GetQuantity(Product product);
        void Submit();
    }

Pour simplifier l'exemple en C#, nous considérerons que Product est un enum :

    public enum Product
    {
        Tshirt,
        Jacket,
        Computer,
        Shoes
    }

Nous considérerons aussi que le catalogue de prix est un simple dictionnaire en mémoire Product/Price. Les prix ne changent pas au cours du temps.

    public class PriceCatalog
    {
        private readonly Dictionary<Product, double> _prices = 
            new Dictionary<Product, double>
        {
            {Product.Tshirt, 3.00},
            {Product.Jacket, 200.50},
            {Product.Computer, 688.00},
            {Product.Shoes, 120.20},
        };

        public double GetPrice(Product product)
        {
            double price;
            if (_prices.TryGetValue(product, out price)) {
                return price;
            }
            return 0;
        }
    }

Quelques règles métiers sont implémentées :
  • L'ajout d'un produit nécessite une quantité valide (> 0)
  • L'ajout d'un produit déjà existant incrémente sa quantité
  • L'ajout, la suppression d'un produit et la soumission de la commande ne peuvent se faire que si la commande n'a pas déjà été soumise.
  • Le montant total est cohérent avec l'ensemble des produits ajoutés à la commande en fonction du catalogue.

Une implémentation de cette commande sans aucune notion de persistance peut se faire de la manière suivante :

    public class Order : IOrder
    {
        private readonly PriceCatalog _catalog = new PriceCatalog();
        private readonly List<OrderLine> _lines = new List<OrderLine>();

        private OrderStatus _orderStatus;

        public Guid Id { get; private set; }
        public DateTime? SubmitDate { get; private set; }
        public double TotalCost { get; private set; }

        // ----- Constructor
        public Order()
        {
            Id = Guid.NewGuid();
        }

        // ----- Public methods
        public void AddProduct(Product product, int quantity)
        {
            CheckIfDraft();
            CheckQuantity(quantity);

            var line = _lines.FirstOrDefault(x => x.Product == product);
            if (line == null) {
                _lines.Add(new OrderLine(product, quantity));
            }
            else {
                line.IncreaseQuantity(quantity);
            }

            ReCalculateTotalPrice();
        }
        public void RemoveProduct(Product product)
        {
            CheckIfDraft();

            var line = _lines.FirstOrDefault(x => x.Product == product);
            if (line != null) {
                _lines.Remove(line);
            }

            ReCalculateTotalPrice();
        }
        public int GetQuantity(Product product)
        {
            var line = _lines.FirstOrDefault(x => x.Product == product);
            if (line == null) {
                return 0;
            }
            return line.Quantity;
        }
        public void Submit()
        {
            CheckIfDraft();
            SubmitDate = DateTime.Now;
            _orderStatus = OrderStatus.Submitted;
        }

        // ----- Internal logic
        private void CheckIfDraft()
        {
            if (_orderStatus != OrderStatus.Draft)
                throw new OrderOperationException("The operation is only allowed if the order is in draft state.");
        }
        private void CheckQuantity(int quantity)
        {
            if (quantity < 0) {
                throw new OrderOperationException("Unable to add product with negative quantity.");
            }
            if (quantity == 0) {
                throw new OrderOperationException("Unable to add product with no quantity.");
            }
        }
        private void ReCalculateTotalPrice()
        {
            if (_lines.Count == 0) {
                TotalCost = 0;
            }
            TotalCost = _lines.Sum(x => _catalog.GetPrice(x.Product)*x.Quantity);
        }
    }

Ce que nous pouvons dire de cette implémentation de la classe Order :
  • Elle encapsule parfaitement ses états internes
  • Elle expose des méthodes pour effectuer des actions sur l'objet en tenant compte des règles métiers
  • Elle expose des propriétés publiques en lecture seule pour effectuer des requêtes.
Maintenant, nous souhaitons persister les instances de cette classe.


Et là ... problème ...

Que souhaitons-nous sauvegarder ?

Probablement les données internes de l'objet Order et de ses sous-objets OrderLine :
  • Id
  • OrderStatus
  • SubmitDate
  • TotalCost
  • OrderLines
    • OrderId
    • Product
    • Quantity
    • CreationDate

- " Et alors, quel est le problème ?"

Si nous voulons ajouter un objet Order dans une base relationnelle en utilisant par exemple EntityFramework, nous devons déclarer deux classes de mapping, une pour Order et une pour OrderLine :

    public class OrderMapping : EntityTypeConfiguration<Order>
    {
        public OrderMapping()
        {
            this.ToTable("Order");
            this.HasKey(x => x.Id);
            this.Property(x => x.OrderStatus);
            this.Property(x => x.TotalCost);
            this.Property(x => x.SubmitDate);
            this.HasMany(x => x.Lines).WithRequired().HasForeignKey(x=>x.OrderId);
        }
    }

    public class OrderLineMapping : EntityTypeConfiguration<OrderLine>
    {
        public OrderLineMapping()
        {
            this.ToTable("OrderLine");
            this.HasKey(x => new {x.OrderId, x.Product});
            this.Property(x => x.OrderId);
            this.Property(x => x.Product);
            this.Property(x => x.Quantity);
            this.Property(x => x.CreationDate);
        }
    }

Le problème est que toutes les propriétés des objets Order et OrderLine ne sont pas forcément accessibles. Par exemple, OrderStatus et la collection Lines de OrderLine de la classe Order sont des attributs privés. De même pour OrderId et CreationDate de la classe OrderLine. Nous ne pouvons donc pas lire les états de l'objet à persister.

Si nous remplaçons EntityFramework par un autre ORM (micro) comme Dapper, le problème reste le même. Par exemple :

    public class DapperOrderRepository : IOrderRepository
    {
        public void Add(Order order)
        {
            using (var connection = new SqlConnection(@"Server=localhost\\SQLEXPRESS;database=DomainModelPatterns.Compromise;")) {
                connection.Execute("INSERT INTO [dbo].[Order] (Id, OrderStatus, TotalCost, SubmitDate) " + 
                                   "VALUES (@Id, @OrderStatus, @TotalCost, @SubmitDate)", 
                new {
                    Id = order.Id,
                    OrderStatus = order.OrderStatus,
                    TotalCost = order.TotalCost,
                    SubmitDate = order.SubmitDate
                });

                connection.Execute("INSERT INTO [dbo].[OrderLine] (CreationDate, Product, Quantity, OrderId) " +
                                   "VALUES(@CreationDate, @Product, @Quantity, @OrderId)", 
                    order.Lines.Select(line=> new {
                        CreationDate = line.CreationDate,
                        Product = line.Product,
                        Quantity = line.Quantity,
                        OrderId = line.OrderId
                    }));
            }
        }
    }

Cette problématique est la même lors de la récupération des données depuis une structure quelconque : nous voulons injecter ces données lues dans l'objet. Or, même les propriétés publiques sont en lecture seule. Et ne parlons pas des champs privés, non visibles.

- " Bah, tu n'as qu'à mettre ces états en public et puis basta! "

Le problème que nous avons ici n'est pas d'ordre technique, mais plutôt d'ordre philosophique.
Si nous rendons les états de la classe Order, publiques, rien ne garantira que ses consommateurs interagiront avec les méthodes contenant les règles métiers ! L'objet pourrait donc devenir incohérent car modifier directement depuis les propriétés. Est-ce vraiment une bonne solution ?

Les instances des classes Order et OrderLine, définies plus haut, ne peuvent pas être persister telles quelles. Nous devons apporter des modifications à notre design pour permettre la persistance.


Pistes de réflexion

Cette problématique m'a beaucoup tracassé et j'ai longtemps réfléchi, parcouru de nombreux articles sur le web, expérimenté différentes approches ... De mes réflexions sont ressorties différentes solutions que j'aborderais dans les prochains articles.
  • Faire un compromis sur la conception objet
  • Le pattern State-Interface
  • Le pattern State-Snapshot
  • L'event sourcing


La suite

Étudions tout d'abord une solution de cours terme, pour des projets à petite échelle avec un domaine métier très simple.





jeudi 29 mai 2014

Comment modifier du code spaghetti ? Méthode ETRA




En tant que prestataire, vous êtes-vous déjà retrouvé chez un client pour assurer de la TMA (Tierce Maintenance Applicative) sur un projet existant depuis plus de 3 ans, et n'appliquant aucune des bonnes pratiques de développement et de test ?
Sûrement.

Dans ce cas, vous a-t-on demandé de faire des évolutions ou des corrections de bugs sur du code non professionnel (code spaghetti) et non testé ?





Dans ce cas, qu'avez-vous fait ? Avez-vous :
  • Accepté et modifié le code sans rechigner :  "C'est temporaire avant d'attendre (de vendre) une refonte complète"
  • Refusé : "Je ne suis pas là pour faire du code de mauvaise qualité"


Expérience

Il y a quelques mois, la question s'est posée à moi.

Je suis intervenu sur une application critique, en architecture client/serveur.
Coté serveur, au cœur de l'application, j'ai découvert une classe de 13 000 lignes de VB.NET avec 280 méthodes publiques, concentrant de la logique métier, du requêtage de base de données (en clair), ainsi que des règles de validation.
Imaginez le degré de risque lorsque l'on m'a demandé de modifier des règles métiers centrales de l'application dans une méthode de cette classe de 800 lignes. Tout ça, sans le moindre test.

Si j'avais accepté de faire la modification comme l'entreprise le souhaitait, les conséquences auraient été multiple:
  • Risque très élevé d'introduction de régression
  • Des journées de réflexion intense pour quelques lignes modifiées (projection mentale pour essayer d'anticiper les éventuelles régressions)
  • Si le moindre bug survient en production, mon image de prestataire aurait été ternie
Bien sûr, à part faire des préconisations d'amélioration de code, voire de refonte totale ou partielle, le contrat forfaitaire m'empêchait de refuser d'intervenir sur cette fonctionnalité.

J'ai donc décidé de faire les choses autrement.


Un code non testé, une peur de la régression

Le problème du code non testé est multiple :
  1. Rigidité.  Lors de la modification de code, il est difficile voire impossible d'en détecter les régressions. La stratégie souvent adoptée est d'ajouter des fonctionnalités en touchant le moins de choses possibles. Une fois que cela marche, on ne touche plus à rien !
  2. Complexité. La conséquence directe de la rigidité, c'est la complexité. On empile les choses les unes sur les autres sans jamais faire de refactor. Cette complexité va donc en grandissant et la qualité en diminuant.
  3. Mauvaise architecture. Mettre en place de la testabilité sur un projet nécessite de penser en amont à une architecture adaptée. L'idée maîtresse est le faible couplage. Sans tests et malgré une conception initiale correcte, l'architecture dérive très vite vers un énorme plat de spaghettis, composé de classes statiques, de singletons et d'objets à multiples responsabilités.
Intervenir sur un tel code est donc risqué.

En tant que prestataire ou indépendant, vous avez une image à défendre, il est donc indispensable de trouver un moyen à court terme de garantir que l'ajout de fonctionnalité n'introduira aucune régression, car la refonte totale ou partielle est souvent une option non viable (refusée par le client).


La méthodologie ETRA ("Extract, Test, Refactor, Add")

Le méthodologie est basée sur le principe suivant :
"On ne doit modifier du code que si l'on peut en mesurer précisément les régressions."
Les tests automatisés sont un moyen de contrôler cette régression. On ne peut modifier du code que s'il est testé. Il faut donc commencer par tester le code avant d'effectuer quelconques modifications. Mais qu'en est-il du code dit non testable car possédant de forts couplages ? Comment le rendre testable sans introduire de régression ?


Le "E" de ETRA : Extract

La première étape est de déplacer le code que l'on souhaite tester, dans un environnement propice aux tests, c'est à dire le plus souvent, dans une nouvelle classe. On procède donc comme suit :
  1. Identifier la portion de code à modifier
  2. Extraire ce code dans une nouvelle méthode d'une nouvelle classe
  3. Ajouter les dépendances de ce code en tant que paramètres de la méthode ou du constructeur
  4. Utiliser cette classe dans le code appelant
C'est l'étape la plus risquée car on effectue un déplacement de code depuis un environnement non testé. Il faut donc pouvoir garantir intellectuellement (à défaut de pouvoir faire mieux) qu'aucune régression n'a été introduite. Cette étape doit ne comporter aucune modification du comportement de l'application : même si on déplace du code, on doit le conserver tel qu'il a été écrit initialement.


Le "T" de ETRA : Test

La seconde étape consiste à tester le code déplacé afin d'en contrôler les régressions possibles.

Dans un premier temps, il faut contrôler les paramètres de la méthode afin de vérifier qu'ils sont instanciables dans un contexte de tests unitaires. Si ce n'est pas le cas, il faut créer une abstraction du concept via une interface. En mettant en place ces abstractions, vous rendez le code moins dépendant à des objets d'infrastructure, de requêtage, de service, etc. Lors de l'écriture de vos tests unitaires, vous devez créer de nouvelles implémentations de ces abstractions pour simuler des comportements bien définis. Vous pourrez alors vous concentrer sur l'essence du code et vérifier qu'il fonctionne, indépendamment des dépendances externes.

Une fois que vous pouvez "mocker" les dépendances de votre code, vous pouvez le tester. Il faut :
  1. Ajouter des tests à petite responsabilité
  2. Étendre la couverture de test du code déplacé qui doit s'approcher des 100% 
Cet ajout de testabilité est essentiel pour :
  • Contrôler la non régression de l'existant
  • Donner une meilleure vision du périmètre métier traité par le code
  • D'anticiper l'étape suivante : le refactor


Le "R" de ETRA : Refactor

La troisième étape est d'effectuer un refactor du code existant. 

Dans une application qui a plusieurs années, où un certain nombre de prestataires se sont succédés, le code est souvent complexe voir illisible, car écrit par plusieurs personnes ayant chacun son style de programmation. 

Modifier directement ce code est souvent beaucoup plus coûteux que d'effectuer d'abord une réécriture complète ou partielle puis d'effectuer la modification.

La couverture de tests de l'étape précédente permet d'effectuer cette étape avec sérénité.


Le "A" de ETRA : Add

La dernière étape consiste en l'ajout de la fonctionnalité.

Grâce à l'ajout de testabilité et au refactor, on peut traiter cette étape très efficacement. Le risque lié à la modification du code devient quasiment nul car les régressions sont détectées instantanément.
Afin de conserver une testabilité optimale, il est conseillé d'effectuer cet ajout en TDD.


Exemple

Pour illustrer la méthode ETRA, voici ci-dessous, un exemple de code très proche de ce que j'ai pu rencontrer lors d'une mission. Je l'ai tout de même simplifié pour qui soit plus compréhensible.

    public class GodClass
    {
        private readonly DatabaseAccess _databaseAccess;

        public void DoWork()
        {
            // ...
            // 300 lines of code before
            // ...
            if (_databaseAccess.Connect()) {
                var persons = _databaseAccess.GetPersons();
                var major = 0;
                var l_atLeast1CityLondon = false;
                var paris = 0;
                var maj_male = 0;
                var nb_person = 0;
                var averrageMajAndMale = 0;
                foreach (var person in persons) {
                    if (person.Age >= 18) {
                        major += 1;
                        if (person.Gender == 'M') {
                            maj_male = maj_male + 1;
                        }
                    }
                    nb_person += 1;
                    var l_addresses = FindAddressesForPerson(person);
                    if (l_addresses.Count() > 0) {
                        foreach (var address in l_addresses) {
                            if (address.City == "Paris") {
                                paris = paris + 1;
                            }
                            if (address.City == "London") {
                                l_atLeast1CityLondon = true;
                            }
                        }
                    }
                }
                if (l_atLeast1CityLondon) {
                    DoSomethingStrange();
                }
                averrageMajAndMale = maj_male/nb_person;
                _databaseAccess.SaveDatas(major, averrageMajAndMale, paris);
            }
            // ...
            // 200 lines of code after
            // ...
        }
    }

On remarque que ce code est très peu lisible avec plusieurs styles de programmation (nommage de variables). Néanmoins, on comprend à peu près qu'il a pour but de calculer des indicateurs basés sur des objets de type "Person" et "Address". La méthode "FindAddressesForPerson" est interne à la classe "GodClass".

Si on vous demandait de modifier ce code pour ajouter une nouvelle fonctionnalité : calculer la
moyenne d'âge des personnes vivants à Londres ; comment feriez-vous ?


Etape 1 : Extract

La première étape consiste à extraire le code responsable du calcul des indicateurs dans une classe à part. On procède de la manière suivante :

    public class GodClass
    {
        private readonly DatabaseAccess _databaseAccess;

        public void DoWork()
        {
            // ...
            // 300 lines of code before
            // ...
            if (_databaseAccess.Connect()) {
                var persons = _databaseAccess.GetPersons();
                var major = 0;
                var l_atLeast1CityLondon = false;
                var paris = 0;
                var averrageMajAndMale = 0;

                var indicatorCalculator = new IndicatorCalculator();
                indicatorCalculator.Calculate(
                    persons,
                    ref major,
                    ref averrageMajAndMale,
                    ref paris,
                    ref l_atLeast1CityLondon,
                    FindAddressesForPerson);

                if (l_atLeast1CityLondon) {
                    DoSomethingStrange();
                }
                _databaseAccess.SaveDatas(major, averrageMajAndMale, paris);
            }
            // ...
            // 200 lines of code after
            // ...
        }
    }

La nouvelle classe créée se définit de la manière suivante :

    public class IndicatorCalculator
    {
        public void Calculate(
            IEnumerable<Person> persons,
            ref int major,
            ref int averrageMajAndMale,
            ref int paris,
            ref bool l_atLeast1CityLondon,
            Func<Person, IEnumerable<Address>> findAddressesForPerson)
        {
            var maj_male = 0;
            var nb_person = 0;
            foreach (var person in persons) {
                if (person.Age >= 18) {
                    major += 1;
                    if (person.Gender == 'M') {
                        maj_male = maj_male + 1;
                    }
                }
                nb_person += 1;
                var l_addresses = findAddressesForPerson(person);
                if (l_addresses.Count() > 0) {
                    foreach (var address in l_addresses) {
                        if (address.City == "Paris") {
                            paris = paris + 1;
                        }
                        if (address.City == "London") {
                            l_atLeast1CityLondon = true;
                        }
                    }
                }
            }
            averrageMajAndMale = maj_male / nb_person;
        }
    }

L'idée principale de cette extraction, est de modifier le moins de code possible. La classe "GodClass" étant non testable, il faut conserver la logique du code au maximum pour ne pas introduire de régression.
Nous définissons ici une nouvelle classe responsable du calcul des indicateurs : "IndicatorCalculator". La méthode "Calculate" prend en paramètre toutes les dépendances, que ce soit les paramètres de sortie en référence, ou une signature de la fonction "FindAddressesForPerson".

Cette classe est maintenant testable.


Etape 2 : Test

L'objectif ici est de tester la classe "IndicatorCalculator" afin de pouvoir détecter toute régression future. A cette étape, nous ne sommes pas là pour détecter une quelconque anomalie métier dans le code existant. Nous devons considérer le code comme étant valide et créer une couverture de tests s'approchant de 100%. Un exemple de test pourrait être :

    [TestMethod]
    public void OnePersonMaleMajorWithOneAddressInParisGiveCorrectIndicators()
    {
        // Datas
        var person = new Person{Age = 18, Gender = 'M'};
        var addresse = new Address {City = "Paris"};
        var personMajorCount = 0;
        var anyLondonCity = false;
        var personLivingInParisCount = 0;
        var personMajorAndMaleAverrage = 0;
        var getAddressFunc =
            new Func<Person, IEnumerable<Address>>(p => new[]{addresse});

        // Actors
        var indicatorCalculator = new IndicatorCalculator();

        // Actions
        indicatorCalculator.Calculate(
            new[] { person }, 
            ref personMajorCount, 
            ref personMajorAndMaleAverrage, 
            ref personLivingInParisCount, 
            ref anyLondonCity, 
            getAddressFunc);

        // Asserts
        Assert.AreEqual(1, personMajorCount);
        Assert.IsFalse(anyLondonCity);
        Assert.AreEqual(1, personLivingInParisCount);
        Assert.AreEqual(1, personMajorAndMaleAverrage);
    }

Ce test n'est pas très propre, mais quand bien même, il permet de contrôler le comportement de la classe "IndicatorCalculator".
Une fois le code testé, nous pouvons nous retrousser les manches pour entamer le refactor.


Etape 3 : Refactor

Pour effectuer cette étape, il est intéressant de réfléchir à partir des tests. Ils constituent un des points d'entré vers la classe "IndicatorCalculator". Comment pouvons-nous modifier la classe pour qu'elle soit beaucoup plus élégante et simple à utiliser ?
Passer des références à une méthode n'est pas très propre. Il est préférable de créer un objet "Indicators" regroupant l'ensemble des valeurs calculées. De plus, il est grand temps d'uniformiser les noms de variables par des choses un peu plus parlantes :

    public class Indicators
    {
        public int PersonMajorCount { get; set; }
        public int PersonMajorAndMaleAverrage { get; set; }
        public int PersonLivingInParisCount { get; set; }
        public bool AnyPersonLivingInLondon { get; set; }
    }

La méthode "FindAddressesForPerson", passée sous forme de délégué à la classe "IndicatorProcessor" n'est pas non plus une façon très propre de procéder. On peut considérer l'ajout d'une interface de type "IAddressLocator" possédant cette fameuse méthode. Cette dépendance est ensuite fournie pour construire l'objet "IndicatorProcessor".

    public interface IAddressLocator
    {
        IEnumerable<Address> FindAddressesForPerson(Person person);
    }

L'interface "IAddressLocator" doit donc être mockée pour notre test. Une implémentation pourrait être :

    private class MockAddressLocator : IAddressLocator
    {
        private readonly Func<Person, Address[]> _getAddressFunc;

        public MockAddressLocator(Func<Person, Address[]> getAddressFunc)
        {
            _getAddressFunc = getAddressFunc;
        }
        public IEnumerable<Address> FindAddressesForPerson(Person person)
        {
            return _getAddressFunc(person);
        }
    }

On peut donc l'utiliser dans notre test. Il devient :

    [TestMethod]
    public void OnePersonMaleMajorWithOneAddressInParisGiveCorrectIndicators()
    {
        // Datas
        var person = new Person { Age = 18, Gender = 'M' };
        var addresse = new Address { City = "Paris" };

        // Actors
        var addressLocator = new MockAddressLocator(p => new[] {addresse});
        var indicatorsCalculator = new IndicatorCalculator(addressLocator);

        // Actions
        var indicators = indicatorsCalculator.Calculate(new[] { person });

        // Asserts
        Assert.AreEqual(1, indicators.PersonMajorCount);
        Assert.IsFalse(indicators.AnyPersonLivingInLondon);
        Assert.AreEqual(1, indicators.PersonLivingInParisCount);
        Assert.AreEqual(1, indicators.PersonMajorAndMaleAverrage);
    }


Le test est dorénavant beaucoup plus lisible.
Nous pouvons maintenant modifier le code de la classe "IndicatorCalculator" pour qu'il corresponde à notre test.

Il existe plusieurs manières d'effectuer ce refactor, et cela, en fonction du contexte du code. J'ai choisi de privilégier la lisibilité du code avec LINQ, au détriment des performances car je pars du principe que la liste de personnes et d'adresses seront faibles (<10000 éléments). On obtient :

    public class IndicatorCalculator
    {
        private readonly IAddressLocator _addressLocator;

        public IndicatorCalculator(IAddressLocator addressLocator)
        {
            _addressLocator = addressLocator;
        }

        public Indicators Calculate(IEnumerable<Person> persons)
        {
            var localPersons = persons.ToList();
            var indicators = new Indicators();
            indicators.PersonMajorCount = localPersons.Count(person => person.Age >= 18);
            indicators.PersonMajorAndMaleAverrage = localPersons.Count(person => person.Age >= 18 && person.Gender == 'M')/localPersons.Count();
            UpdateAddressIndicators(localPersons, indicators);
            return indicators;
        }

        private void UpdateAddressIndicators(IEnumerable<Person> persons, Indicators indicators)
        {
            var allAddresses = persons.SelectMany(_addressLocator.FindAddressesForPerson).ToList();
            indicators.AnyPersonLivingInLondon = allAddresses.Any(address => address.City == "London");
            indicators.PersonLivingInParisCount = allAddresses.Count(address => address.City == "London");
        }
    }

Enfin, la classe "GodClass" devient :

public class GodClass : IAddressLocator
    {
        private readonly DatabaseAccess _databaseAccess;

        public void DoWork()
        {
            // ...
            // 300 lines of code before
            // ...
            if (_databaseAccess.Connect()) {
                var persons = _databaseAccess.GetPersons();
                var indicatorCalculator = new IndicatorCalculator(this);
                var indicators = indicatorCalculator.Calculate(persons);
                if (indicators.AnyPersonLivingInLondon) {
                    DoSomethingStrange();
                }
                _databaseAccess.SaveDatas(
                    indicators.PersonMajorCount,
                    indicators.PersonMajorAndMaleAverrage,
                    indicators.PersonLivingInParisCount);
            }
            // ...
            // 200 lines of code after
            // ...
        }
    }


Etape 4 : Add

La dernière étape consiste à ajouter la fonctionnalité souhaitée. Dans notre exemple, "calculer la moyenne d'âge des personnes vivants à Londres" nécessite de modifier uniquement la méthode "UpdateAddressIndicators" de la classe "IndicatorProcessor" et d'ajouter la propriété dans l'objet "Indicators". Un exemple de code pourrait être :

private void UpdateAddressIndicators(IEnumerable<Person> localPersons, Indicators indicators)
    {
        var personLivingInLondonAges = new List<int>();
        foreach (var person in localPersons) {
            var addresses = _addressLocator.FindAddressesForPerson(person).ToList();
            if (addresses.Any(address => address.City == "Paris")) {
                indicators.PersonLivingInParisCount++;
            }
            if (addresses.Any(address => address.City == "London")) {
                indicators.AnyPersonLivingInLondon = true;
                personLivingInLondonAges.Add(person.Age);
            }
        }
        if (personLivingInLondonAges.Any()) {
            indicators.PersonLivingInLondonAgeAverrage = personLivingInLondonAges.Average();
        }
    }

Conclusion

Travailler sur un projet où aucune des bonnes pratiques de développement ou de tests n'a été mise en place n'a rien de plaisant. On a tous tendances à critiquer le code, râler, dire qu'on est obligé de mal coder, etc.

Mais finalement, coder proprement n'est qu'une question de choix personnel et de motivation. Peu importe l'environnement, il y a toujours des choses que l'on peut mettre en place afin de faire augmenter la qualité générale du projet.

ETRA est un processus de développement centré sur la sécurisation du code avant toute modification. Elle peut être coûteuse en fonction des projets, mais le gain est très important. Le refactor progressif augmente petit à petit la qualité générale du logiciel tandis que la testabilité donne de plus en plus de visibilité sur les régressions éventuelles liées à l'ajout / modification de fonctionnalités.

Une question reste en suspend : comment vendre cette méthodologie à un client ? Lui faire comprendre que l'on souhaite travailler de cette manière ? Je recommande simplement de ne pas en parler. Cette méthodologie de développement ne concerne que nous, développeur.
La seule chose à expliquer concerne le fait que le coût de développement sera de toute façon plus important que sur un projet dit "classique" (à définir) car les risques de régression liés à la modification de code dans cet environnement instable sont très élevés. Un simple audit de code permettra de donner cette vision au client.

Et maintenant, à vos claviers !


mercredi 2 octobre 2013

BDD : l'exemple du Kata Potter


Dans cet article, je vais présenter l’utilisation de la méthodologie « Behavior Driven Development » (BDD) au travers d’un exemple très connu : le Kata Potter.

J’effectuerais cet exemple en C#, sous Visual Studio 2012 et en utilisant le framework SpecFlow.
Cet exemple a pour but d’illustrer la puissance du BDD grâce à l’écriture de scénarios très clairs qui amèneront l’implémentation de l’exemple. Le but n’est pas de faire un design de classe parfait mais d’avoir une approche pédagogique.

dimanche 18 août 2013

BDD : Behavior Driven Development

Définition

Le "Behavior Driven Development" (BDD) est une méthode de développement logiciel, créée par Dan North en 2003, qui encourage la communication directe entre les personnes du besoin et l'équipe en charge d'y répondre. Le but est de créer ensemble un produit qui répond exactement aux attentes du client. Le maître mot : la compréhension des acteurs.

mercredi 29 mai 2013

Le « O » de SOLID : Open/Closed Principle (OCP)


Au sein de la programmation orientée objet, le principe « Ouvert/Fermé » est le deuxième principe de SOLID, et s'applique aux différents concepts manipulés : Assembly, Class, Method. Selon la définition de Bertrand Meyer :

« Software entities should be open for extension, but closed for modification ».