Skip to content

Migrer vers le Event Sourcing, partie 1: l’application DDD “légère”

by Julien on juillet 13th, 2010

An english version of this post is available here.

  • Part 1: the DDD “light” application.

    Ce billet va présenter un exemple d’une architecture essayant de supporter le DDD. Vous pouvez télécharger l’application complète ici. Vous pouvez utiliser ce code comme bon vous semble, y compris pour des applications commerciales fermées. Mais si vous la publiez, vous devez me mentionner dans les crédits. Bien sûr, je vous déconseille d’utiliser le code du présent billet, puisque le but des prochains billets est de présenter ce que je pense est une meilleure alternative.

    Ce premier billet est long, puisque j’ai besoin de décrire toute l’architecture. Les prochains seront plus succincts, puisqu’ils décriront une évolution à la fois.

    Cet exemple est écrit en C#, avec NHibernate comme ORM, Windsor Castle comme Container d’IdC, et ASP.Net MVC pour l’interface usager.

    Je vais supposer que vous êtes déjà familier avec les concepts de Mapping Objet-Relationnel, d’Inversion de Contrôle, et du patron Modèle-Vue-Contrôleur.

    L’architecture (simplifiée) ressemble à ceci:

    Rentrons maintenant dans les détails.

    1 – Le coté “requêtage”

    Quand vous voulez afficher de l’information dans cette application simple, vous faites une requête sur vos entité. Habituellement, vous le faites en utilisant le patron “repository”.Par exemple :

    public interface IStudentRepository : IRepository<Student>
    {
        IPaginable<Student> ByNameLike(string name);
    }

    L’interface de base ressemble à:

    public interface IRepository<T> where T : IAggregateRoot
    {
        T ById(Guid id);
        void Add(T toAdd);
        void Remove(T toRemove);
        IPaginable<T> All();
    }

    L’idée derrière un repository est (entres autres) de grouper toutes les requêtes à votre entité. Les MOR comme NHibernate ou le Framework ADO.Net Entity facilitent l’implémentation.

    Vous avez sans doute remarqué que les requêtes pouvant retourner plusieurs entités renvoient un IPaginable<T>. L’idée est que vous avez besoin de paginer la requête, mais sans exposer un IQueryable<T> ce qui amènerait immanquablement votre code à être parsemé de requêtes. Encore une fois, dans mon expérience, il est bien plus maintenable de regrouper les requêtes dans un seul repository. L’interface du  IPaginable<T> ressemble à ceci:

    public interface IPaginable<T>
    {
        int Count();
        T UniqueValue();
        IEnumerable<T> ToEnumerable();
        IEnumerable<T> ToEnumerable(int skip, int take);
        IEnumerable<T> ToEnumerable(int skip, int take, string sortColumn,  SortDirection? sortDirection);
    }

    Tout comme avec un IQueryable<T>, il est facile d’implémenter un proxy de pagination pour l’interface usager comme le IPagination<T> de la librairie MvcContrib. Le code de vos actions dans vos contrôleurs devient trés simple. Par exemple:

    public ViewResult Index(int Page = 1, string Name = null)
    {
        var model = new StudentSearchModel()
        {
            Name = Name,
            Students = studentRepository.ByNameLike(Name).AsPagination(Page)
        };

        return View(model);
    }

    public ViewResult Details(Guid studentId)
    {
        var student = studentRepository.ById(studentId);

        return View(student);
    }

    Et voilà pour l’affichage!

    2 – Le coté transactionnel

    Évidemment, la plupart des applications ne se contentent pas d’afficher des données. Elles implémentent généralement des règles d’affaire, des processus et des calculs. Essayons de voir comment cela fonctionne ici.

    La règle est que pour chaque action que l’utilisateur a besoin d’effectuer, il doit y avoir une et une seule méthode dans la racine de l’agrégat correspondant. Si vote interface supporte les mises à jours en batch, alors cette même méthode peut être appelée sur plusieurs instances de l’agrégat.

    Cela rends le code de vos contrôleurs très simple. Par exemple, pour assigner une classe à un étudiant:

    public RedirectToRouteResult DoRegisterToClass(RegisterToClassModel  model)
    {
        var student = studentRepository.ById(model.StudentId);
        var @class = classRepository.ById(model.ClassId);
        student.RegisterTo(@class);
        return RedirectToRoute(new
        {
            controller = "Student",
            action = "Index"
        });
    }

    Qu’arrive t-il dans l’agrégat étudiant?

    public virtual void RegisterTo(Class @class)
    {
        // Business rules
        @class.Validation().NotNull("class");
        if (registrations.Where(x => x.Class.Id == @class.Id).Count() > 0)
        {
            throw new InvalidOperationException("You can not register a student to a class he already registered");
        }
        if (passedClasses.Where(x => x == @class.Id).Count() > 0)
        {
            throw new InvalidOperationException("You can not register a student to a class he already passed");
        }

        // State changes
        registrationSequence = registrationSequence.Next();
        registrations.Add(new Registration(registrationSequence.ToId(), @class));
    }

    Vous pouvez voir que toutes les méthodes publique de racine d’agrégat sont divisées en 2 parties:

    • Validation des règles d’affaire. Cette partie vérifie que l’action demandée peut être effectuées. Cette partie peut lancer des exceptions.
    • Changement de l’état de l’agrégat. Cette partie effectue l’action. Comme les règles d’affaire ont été vérifiée, cette partie ne peut lancer d’exception.

    L’idée est de ne pas modifier une entité tant que l’on est pas certain de pouvoir la mettre à jour avec succès. Nous allons voir dans les prochains billets que cela facilite les refactorisations futures.

    Vous avez peut être remarqué qu’aucun code ici ne fait appel directement à NHibernate ou à une transaction quelconque. C’est parce que cette application utilise le patron “session par requête”. Le code qui ouvre et ferme la session NHibernate se trouve dans un IHttpModule:

    void context_BeginRequest(object sender, EventArgs e)
    {
        CurrentContainer.Container.Build<IPersistenceManager>().Open();
    }

    void context_EndRequest(object sender, EventArgs e)
    {
        var pm = CurrentContainer.Container.Build<IPersistenceManager>();

        try
        {
            if (HttpContext.Current.Error == null) pm.Commit();
        }
        finally {pm.Close();}
    }

    Le IPersistenceManager est une simple abstraction du mécanisme de persistance.

    Et voilà pour le coté transactionnel!

    3 – Racines d’agrégat et entités

    Il y a quelques subtilités dans l’implémentation avec NHibernate des agrégats.

    Pas de setters!

    Les setters sont un anti patron en général, et plus encore en DDD. DDD ne se concentre pas principalement sur la structure des données (les noms). La méthodologie permet de modéliser des comportements, ce qui s’exprime avant tout par des verbes. Donc, l’interface publique de vos agrégats devrait ne contenir que des méthodes, chacune correspondant à un cas d’utilisation. Si vous ressentez le besoin d’appeler un setter dans un contrôleur ou un service applicatif, c’est probablement que vous manquez un cas d’utilisation pour cet agrégat. Un des effets de bord positif de cette règle est qu’il devient très facile de s’assurer que votre agrégat reste en tout temps parfaitement valide, et que tous les invariants soient respectés en tout temps.

    Les plus perspicaces noterons que si DDD se concentre sur les comportements, alors les getters aussi devraient être des anti patron. C’est le cas. Malheureusement, nous avons encore besoin des getters car nous utilisons les entités pour l’interface usager. Nous verrons dans le prochain billet comment nous en débarrasser.

    Identifiant des racines d’agrégat

    Les racines devraient avoir un identifiant universel, c’est à dire un identifiant qui peut être utilisé partout dans votre compagnie, et même par vos client et fournisseurs. On peut donc oublier le brave entier séquentiel ici. La meilleure option est probablement le Guid. De plus, un Guid a le bon goût de pouvoir être généré directement dans le code du domaine, sans avoir besoin de faire une requête à votre base de donnée, et sans verrou global. De cette manière, vous serrez prêts pour vous intégrer avec d’autre systèmes plus tard, à la SOA.

    Identifiant des autres entités

    Si vous avez lu le livre d’Eric Evans, vous savez que toutes les entités doivent avoir un identifiant. Ce que ne dit pas clairement le livre cependant, c’est que les entités à l’intérieur d’un agrégat peuvent avoir un identifiant local à l’agrégat. De cette manière, vous n’avez plus besoin de verrou global pour générer vos identifiants. Vous pourriez là aussi utiliser un Guid mais vous ne voulez probablement pas payer le prix de génération et de stockage d’un guid pour chacune de vos sous-entités. Un simple entier fera l’affaire ici.

    Générer les identifiants

    Utiliser les générateurs d’identifiants de votre base de données (@@identity en SQL Server et les séquences dans Oracle) est une mauvaise idée. Cela consomme un appel à la dite base de donnée, et necessite un verrou global. 2 problèmes qui vont limiter votre capacité à monter en charge.

    Donc, le code pour générer un identifiant d’agrégat est simple:

    public abstract class AggregateRoot : IAggregateRoot
    {
        private DateTime? version = null;

        public AggregateRoot()
        {
            Id = Guid.NewGuid();
        }

        public virtual Guid Id {get; private set;}
    }

    Pour les identifiants d’entités, vous pouvez utiliser la technique décrite ici:

    public abstract class Entity<T> where T : AggregateRoot
    {
    protected DateTime? version = null;

    // NHibernate constructor.

    protected Entity() { }

    public Entity(T aggregateRoot, int id)
    {
    this.aggregateRoot =  aggregateRoot.Validation().NotNull("aggregateRoot");
    Id = id;
    }

    private T aggregateRoot;
    public virtual int Id {get; private set;}
    }

    La clef primaire d’une entité sera donc une composition de la clef de l’agrégat, puis de la clef “locale” de l’entité.

    C’est la responsabilité de la racine de l’agrégat de générer ces identifiants. Par exemple:

    private IdSequence registrationSequence;

    public virtual void RegisterTo(Class @class)
    {
        // Business rules here

        // State changes
        registrationSequence = registrationSequence.Next();
        registrations.Add(new Registration(registrationSequence.ToId(),  @class));
    }

    L’avantage est que vous associez ces identifiant directement dans le constructeur. Vous pouvez donc déjà les utiliser pour des associations faibles (= sans clef étrangère dans la base de données). Le problème est que cela peut générer des problèmes de concurrence. Lorsque 2 entités sont créées au même moment dans le même agrégat par 2 threads différents, vous obtenez un bris de clef primaire. Dans ce cas, la victime doit mettre à jour sa clef, et recommencer. Nous allons voir que le event sourcing va nous offrir une solution plus élégante à ce problème.

    Voilà, c’est tout pour l’application de départ. Le prochain billet parlera d’optimiser le coté requêtage de l’application.

    Démarrer l’application exemple

    Les sources pour toute la série de billets sont disponible sur GitHub à http://github.com/jletroui/TransitioningToEventSourcing
    .

    Vous aurez simplement besoin de créer une base de données vide intitulée “DDDPart1″ dans SQLExpress avant de lancer l’application.

    Crédits

    • Pour l’idée et l’implémentation originelle du paginable et du repository: Benoit Goudreault-Emond
    • Pour les identifiants locaux dans les entités: Greg Young

    Liste des billets “migrer vers le Event Sourcing”:

  • From → Event Sourcing

    Leave a Reply

    Note: XHTML is allowed. Your email address will never be published.

    Subscribe to this comment feed via RSS