InfOsaurus

Aller au contenu | Aller au menu | Aller à la recherche

Développement

.NET, Domain Driven Design... tout sur le code.

Fil des billets - Fil des commentaires

mercredi 2 juin 2010

Persistez votre domaine avec Raven DB

 

RavensIl y a peu, je vous parlais d'une possible synergie entre les bases de données NoSQL de type document et les Agrégats de Domain Driven Design.

Entretemps est sortie la base NoSQL pour la plateforme .NET d'Ayende Rahien, Raven DB, et elle confirme ces suppositions. Voici un extrait de la documentation de la base au corbeau :

« When thinking about using Raven to persist entities, we need to consider the two previous points. The suggested approach is to follow the Aggregate pattern from the Domain Driven Design book. An Aggregate Root contains several entities and value types and controls all access to the objects contained in its boundaries. External references may only refer to the Aggregate Root, never to one of its child objects.
When you apply this sort of thinking to a document database, there is a natural and easy to follow correlation between an Aggregate Root (in DDD terms) and a document in Raven. An Aggregate Root, and all the objects that it holds, is a document in Raven. »

Avec Raven, les données sont stockées dans des documents JSON qui ressemblent à ça :


Document

Contrairement à une base relationnelle dans laquelle les données résident dans des tables fragmentées qu'on peut relier par l'intégrité référentielle, il est possible de stocker dans ce type de document tout un graphe d'objets complexes et typés. Les préconisations pour la conception de documents Raven sont donc à l'inverse de celles qui président au design d'une base de données relationnelle : il s'agit de regrouper au sein d'un seul document les objets formant un tout logique. Par exemple, un Livre regroupe à la fois un titre mais aussi les objets que sont son Auteur et sa Catégorie. Il se trouve que la notion d'Agrégat et de Racine d'Agrégat de DDD recoupe en tous points ce concept.

Un des bénéfices qu'on constate immédiatement avec Raven DB est l'élimination pure et simple de la couche de mapping objet-relationnel, parfois source de bien des complications. En effet avec Raven, plus de défaut d'impédance entre les objets de l'application et leur équivalent persisté relationnel. La couche persistance s'en trouve bien allégée.


Exemple en C#

Comme un bon exemple vaut tous les discours, voici comment on peut implémenter un Entrepôt avec Raven et son client C# natif :


   1:      public class Livre
   2:      {
   3:          public string Id { get; set; }
   4:          public Auteur Auteur { get; set; }
   5:          public string Titre { get; set; }
   6:          public Categorie Categorie { get; set; }
   7:      }


   1:      public class EntrepotLivres
   2:      {
   3:          public EntrepotLivres(IDocumentSession documentSession)
   4:          {
   5:              session = documentSession;
   6:          }
   7:   
   8:          public void AjouterLivre(Livre livre)
   9:          {
  10:              session.Store(livre);
  11:              session.SaveChanges();
  12:          }
  13:   
  14:          public void SupprimerLivre(Livre livre)
  15:          {
  16:              session.Delete(livre);
  17:              session.SaveChanges();
  18:          }
  19:   
  20:          public Livre GetLivreParId(string id)
  21:          {
  22:              return session.Load<Livre>(id);
  23:          }
  24:   
  25:          private IDocumentSession session;
  26:      }


   1:  class Program
   2:  {
   3:      static void Main(string[] args)
   4:      {
   5:          var documentStore = new DocumentStore { Url = "http://localhost:8080" };
   6:          documentStore.Initialize();
   7:   
   8:          using (var session = documentStore.OpenSession())
   9:          {
  10:              EntrepotLivres entrepotLivres = new EntrepotLivres(session);
  11:              ...
  12:          }
  13:      }
  14:  }

Pas de fichier de mapping à gérer à côté de ça, l'entité Livre et tout ce qu'elle contient se persistera de façon immédiate... Simple, non ?

On remarque qu'on manipule une Session Raven. Il s'agit d'une implémentation du pattern Unit of Work assez similaire à ce qu'on trouve dans des frameworks d'ORM comme NHibernate. En fait, bon nombre de concepts et dénominations dans l'API client C# de Raven sont directement issus de NHibernate.


Index et requêtes

Pour l'instant, notre Entrepôt ne fait qu'ajouter des entités, les supprimer et les hydrater à partir de leur ID, ce qui se fait nativement avec Raven. Pour exécuter des requêtes plus complexes sur nos documents, il faut passer par des index. Les index sont des vues stockées sur disque semblables aux vues matérialisées des SGBDR. Ils sont taillés pour un type de recherche particulier.

Voici comment on définit en C# un index qui porte sur les catégories des livres :


   1:          documentStore.DatabaseCommands.PutIndex("LivresParCategorie", 
   2:              new IndexDefinition<Livre>
   3:              {
   4:                  Map = livres => from livre in livres
   5:                                  select new { livre.Categorie }
   6:              });

Cet index ne doit être créé qu'une fois, il peut aussi être défini via l'interface d'administration de Raven en utilisant la même syntaxe.

On peut maintenant ajouter une méthode à notre entrepôt qui retourne tous les livres d'une catégorie. Elle effectue une requête LuceneQuery sur l'index précédemment défini :


   1:          public IList<Livre> GetLivresParCategorie(Categorie categorie)
   2:          {
   3:              return session.LuceneQuery<Livre>("LivresParCategorie")
   4:                  .Where(l => l.Categorie == categorie)
   5:                  .ToList();
   6:          }

Il existe une autre technique d'indexation plus sophistiquée, map/reduce, dont je parlerai peut-être dans un autre billet.


Raven DB

Impressions


Ce que j'aime dans Raven DB :

  • Simplicité de persistance des objets et affranchissement complet de la couche d'ORM.
  • Les données sont accessibles sous forme RESTful (chaque document dispose d'une URL) et lisible par un humain.
  • Sans doute bien plus facilement scalable qu'un SGBDR du fait de la nature atomique et autonome d'un document.
  • API .NET et requêtage sur les indexes en Linq.
  • Raven se marie bien avec DDD et une partie de l'effort de design de la base est déjà fait si on a découpé ses Agrégats.

Ce qui me plait moins :

  • Pour exploiter pleinement Raven en termes de performances, il faut idéalement ramener un seul document de la base et que celui-ci contienne tout ce dont on a besoin. Cela peut mener à une tendance à calquer les documents sur les IHM.
  • Raven DB a peut-être un petit impact sur la persistance ignorance de notre domaine. Il semble qu'une entité qui fait référence à la racine d'un autre agrégat (donc située dans un autre document, en termes Raven) ne peut pas avoir une référence directe à cet objet mais est obligé de contenir son ID à la place. Dans notre exemple, l'Auteur d'un Livre est un objet entièrement contenu dans l'agrégat Livre parce qu'on ne stocke que son nom et prénom. Mais si l'Auteur devait faire l'objet d'un agrégat séparé à lui (par exemple si l'auteur est un Utilisateur du système), le Livre devrait alors contenir la clé de l'Auteur et plus l'Auteur lui même. Sachant que les IDs natifs de Raven sont très typiques ("auteurs/465" par exemple), on se retrouve avec une trace de Raven dans le graphe d'objets du domaine, et aussi la nécessité de passer par un entrepôt pour réhydrater l'objet dont on n'a que la clé.

Les doutes à lever à l'avenir :

  • Les performances. Ayende a publié des mesures de perfs prometteuses mais il va falloir qu'elles soient confirmées sur des projets à plus grande échelle. En particulier, il serait intéressant de voir comment le système de concurrence optimiste de Raven se comporte dans un contexte transactionnel intensif.
  • L'adoption. Je pense que les bases NoSQL ne survivront pas sans un écosystème solide à la fois en termes de communauté et d'outillage disponible. Si on ne peut pas faire avec les bases non relationnelles tout ce qu'on fait avec les SGBDR (monitoring, tuning, reporting, analyse de données...), elles resteront une bonne idée sur le papier mais un choix pauvre sur le terrain.

lundi 17 mai 2010

Bientôt l'été ! Quelques recettes pour faire mincir... ses TU

Régime

 

  1:  [Test]
  2:  public void GetTousLivres_Retourne_Tous_Livres()
  3:  {
  4:      EntrepotLivres entrepotLivres = new EntrepotLivres();
  5:      Auteur evans = new Auteur("Evans", "Eric");
  6:      CategorieLivre categorie = new CategorieLivre("Développement");
  7:      Adresse adresse = new Adresse("55 rue des Pommiers");
  8:      Librairie librairie = FabriqueLibrairie.Creer("Librairie la Pomme d'Or", adresse);
  9:      IList<Librairie> librairies = new List<Librairie>() { librairie };
 10:      Livre domainDrivenDesign = FabriqueLivre.Creer("Domain Driven Design", evans,
 11:          categorie, librairies);
 12:      Livre autreLivre = FabriqueLivre.Creer("autre livre", evans, categorie, librairies);
 13:      entrepotLivres.Ajouter(domainDrivenDesign);
 14:      entrepotLivres.Ajouter(autreLivre);
 15:   
 16:      IList<Livre> livresRetournes = entrepotLivres.GetTousLivres();
 17:   
 18:      Assert.AreEqual(2, livresRetournes.Count);
 19:      Assert.Contains(domainDrivenDesign, livresRetournes);
 20:      Assert.Contains(autreLivre, livresRetournes);
 21:  }

 

Mais que fait donc ce test unitaire ?

C'est la question que je me suis posée récemment en relisant un test du même genre.

Le titre peut nous donner une indication, mais le corps de ce test est lui-même peu lisible. Et même sans rentrer dans le détail du code, il y a fort à parier que la maintenabilité et la performance ne seront pas au rendez-vous avec ce gros bloc d'initialisation (le Arrange de Arrange Act Assert) qui plombe le test.

Le problème derrière tout ça, c'est que pour tester un comportement basique d'un objet (ici un Repository de livres), on est obligé de le remplir en initialisant toute une grappe d'autres objets (des livres, des auteurs, catégories...) assez complexes à construire. DDD ne nous facilite pas vraiment la tâche puisque si l'on veut assurer les invariants et garantir l'état valide du domaine, le seul point d'accès pour créer un objet complexe est normalement la Fabrique. Celle-ci va initialiser l'objet dans un état valide et demander beaucoup de paramètres, dont potentiellement d'autres objets qui doivent être construits eux aussi et... vous l'avez compris, le test devient rapidement surchargé.

 

Respectons l'esprit de TDD

 

Dans son livre XUnit Test Patterns et sur le site xunitpatterns.com, Gerard Meszaros met un nom sur ce genre de code smell : Obscure Test. Il propose une approche de l'écriture de tests fidèle aux principes de TDD pour éviter cet écueil :

« Ecrire les tests d'une manière "outside-in" peut nous éviter de produire des test obscurs qu'il faudrait ensuite refactorer. Dans cette approche, on commence par tracer les contours d'un Test à 4 Phases en utilisant des appels à des Méthodes Utilitaires de Tests non existantes.
Une fois qu'on est satisfait des tests, on peut commencer à écrire les méthodes utilitaires dont on a besoin pour les exécuter. En écrivant les tests d'abord, on obtient une meilleure compréhension de ce que les méthodes utilitaires doivent nous apporter pour rendre l'écriture des tests aussi simple que possible. »

En d'autres termes, on va créer le test le plus simple et le plus lisible possible dès le départ en s'aidant de méthodes utilitaires très expressives. Adieu la grappe d'objets, sa complexité sera cachée dans nos méthodes :

 

  1:  [Test]
  2:  public void GetTousLivres_Retourne_Tous_Livres()
  3:  {
  4:      Livre domainDrivenDesign = CreerLivre("Domain Driven Design");
  5:      Livre autreLivre = CreerLivre("Autre livre");
  6:      EntrepotLivres entrepotLivres = CreerEntrepotAvecDeuxLivres(domainDrivenDesign,
  7:          autreLivre);
  8:   
  9:      List<Livre> livresRetournes = entrepotLivres.GetTousLivres();
 10:   
 11:      Assert.AreEqual(2, livresRetournes.Count);
 12:      Assert.Contains(domainDrivenDesign, livresRetournes);
 13:      Assert.Contains(autreLivre, livresRetournes);
 14:  }

 

Et voici l'implémentation des méthodes utilitaires :

 

   1:  protected Livre CreerLivre(string titre)
   2:  {
   3:      Auteur auteur= new Auteur("Nom", "Prénom");
   4:      CategorieLivre categorie = new CategorieLivre("Une catégorie");
   5:      Adresse adresse = new Adresse("Une adresse");
   6:      Librairie librairie = FabriqueLibrairie.Creer("Une librairie", adresse);
   7:      IList<Librairie> librairies = new List<Librairie>() { librairie };
   8:      Livre livre = FabriqueLivre.Creer(titre, auteur, categorie, librairies);
   9:      return livre;
  10:  }
  11:   
  12:  protected EntrepotLivres CreerEntrepotAvecDeuxLivres(Livre livre1, Livre livre2)
  13:  {
  14:      EntrepotLivres entrepot = new EntrepotLivres();
  15:      entrepot.Ajouter(livre1);
  16:      entrepot.Ajouter(livre2);
  17:      return entrepot;
  18:  }

 

Coupons les cordons

 

Notre test est déjà beaucoup plus lisible, mais une autre question se profile à l'horizon. Est-il vraiment unitaire ? Peut-on dire qu'il est atomique alors qu'il s'appuie intensivement sur le bon fonctionnement d'objets externes, à savoir des Fabriques ?

Meszaros préconise de s'affranchir des dépendances aux objets annexes en recourant à des valeurs par défaut, ou en utilisant des Dummy Objects, des objets creux qui ne contiennent rien ou juste l'information nécessaire au test. Concrètement, on extrait une interface du vrai objet en question pour pouvoir créer des dummies sans contrainte de remplissage. Dans notre exemple c'est le Livre qui sera "dummifié" puisque l'Entrepot à tester contient directement des Livres :

 

   1:  public interface ILivre
   2:  {
   3:      ...
   4:  }
   5:   
   6:  public class DummyLivre : ILivre
   7:  {
   8:      //On construit un objet minimal
   9:      public DummyLivre(string titre)
  10:      {
  11:          this.titre = titre;
  12:      }
  13:  }
  14:   
  15:  //Nouvelle méthode du helper
  16:  protected DummyLivre CreerLivre(string titre)
  17:  {
  18:      return new DummyLivre(titre);
  19:  }

 

Cette solution ne me satisfait pas pleinement parce qu'il faut créer un interface ILivre et la faire implémenter à Livre, ce qui n'est pas forcément élégant au regard de la nécessité d'expressivité des classes du domaine dans DDD. A la place, on peut recourir à un framework d'isolation qui va instancier un proxy de notre Livre pour l'utiliser dans les tests sans besoin de recourir à une interface. Un exemple avec Moq :

 

   1:  protected Livre CreerLivre(string titre)
   2:  {
   3:      var mockLivre = new Mock<Livre>();
   4:      mockLivre.Setup(livre => livre.Titre).Returns(titre);
   5:      return mockLivre.Object;
   6:  }

 

NB : cela nécessite une petite modification de la classe Livre. Il faut rendre la property Titre virtual afin que Moq puisse la surcharger.

 

Stratégies d'extraction

 

Tout cela est très bien, mais nous avons toujours des méthodes utilitaires à l'intérieur de notre classe de test, ce qui n'est pas l'idéal pour la lisibilité et la réutilisabilité. Une solution pourrait être de rendre ces méthodes statiques et de les externaliser dans un Helper à part (on parle aussi de pattern ObjectMother) :

 

  1:  public static class LivreTestHelper
  2:  {
  3:      public static Livre CreerLivre(string titre) { ... }
  4:      public static EntrepotLivres CreerEntrepotAvecDeuxLivres(Livre livre1,
  5:          Livre livre2) { ... }
  6:  }

 

Voici maintenant à quoi ressemble le test :

 

  1:  [Test]
  2:  public void GetTousLivres_Retourne_Tous_Livres()
  3:  {
  4:      Livre domainDrivenDesign = LivreTestHelper.CreerLivre("Domain Driven Design");
  5:      Livre autreLivre = LivreTestHelper.CreerLivre("Autre livre");
  6:      EntrepotLivres entrepotLivres = LivreTestHelper.CreerEntrepotAvecDeuxLivres(
  7:          domainDrivenDesign, autreLivre);
  8:   
  9:      IList<Livre> livresRetournes = entrepotLivres.GetTousLivres();
 10:   
 11:      Assert.AreEqual(2, livresRetournes.Count);
 12:      Assert.Contains(domainDrivenDesign, livresRetournes);
 13:      Assert.Contains(autreLivre, livresRetournes);
 14:  }

 

Imaginons maintenant qu'on ait d'autres tests à écrire sur l'EntrepotLivres. Chacun va avoir besoin de son contexte d'initialisation particulier (livres avec une certaine catégorie, un certain auteur, un nombre de livres fixé...) Chaque test va ajouter ses méthodes au TestHelper et il y a fort à parier qu'on va vite se retrouver avec un helper obèse et beaucoup de duplication de code, chaque test ayant besoin d'un environnement légèrement différent des autres mais avec des choses communes.

C'est là que nous pouvons appeler le pattern Test Data Builder à la rescousse. Il va nous permettre de rendre modulaire la construction de notre contexte de test, sans trop nuire à la lisibilité.

Voici à quoi pourrait ressembler un test avec des DataBuilder :

 

   1:  [Test]
   2:  public void GetLivresParNomAuteur_Retourne_Bons_Livres()
   3:  {
   4:      Livre domainDrivenDesign = new LivreBuilder()
   5:          .AvecTitre("Domain Driven Design")
   6:          .AvecAuteur("Eric",  "Evans")
   7:          .Creer();
   8:   
   9:      Livre autreLivre = new LivreBuilder()
  10:          .AvecTitre("Autre titre")
  11:          .AvecAuteur("Autre", "Auteur")
  12:          .Creer();
  13:   
  14:      EntrepotLivres entrepotLivres = new EntrepotLivresBuilder()
  15:          .AvecLivre(domainDrivenDesign)
  16:          .AvecLivre(autreLivre)
  17:          .Creer();
  18:   
  19:      List<Livre> livresRetournes = entrepotLivres.GetLivresParNomAuteur("Evans");
  20:   
  21:      Assert.AreEqual(1, livresRetournes.Count);
  22:      Assert.Contains(domainDrivenDesign, livresRetournes);
  23:      Assert.IsFalse(livresRetournes.Contains(autreLivre));
  24:  }

 

Je ne sais pas ce que vous en pensez, mais je trouve ça plutôt agréable à l'oeil. Sous le capot, cela donne ça :

 

   1:  public class LivreBuilder
   2:  {
   3:      Mock<Livre> mockLivre = new Mock<Livre>();
   4:   
   5:     public LivreBuilder AvecTitre(string titre)
   6:      {
   7:          mockLivre.Setup(livre => livre.Titre).Returns(titre);
   8:          return this;
   9:      }
  10:   
  11:      public LivreBuilder AvecAuteur(string prenomAuteur, string nomAuteur)
  12:      {
  13:          var mockAuteur = new Mock<Auteur>();
  14:          mockAuteur.Setup(au => au.Prenom).Returns(prenomAuteur);
  15:          mockAuteur.Setup(au => au.Nom).Returns(nomAuteur);
  16:          mockLivre.Setup(livre => livre.Auteur).Returns(mockAuteur.Object);
  17:          return this;
  18:      }
  19:   
  20:      public Livre Creer()
  21:      {
  22:          return mockLivre.Object;
  23:      }
  24:  }
  25:   
  26:  public class EntrepotLivresBuilder
  27:  {
  28:      EntrepotLivres entrepotLivres = new EntrepotLivres();
  29:   
  30:      public EntrepotLivresBuilder AvecLivre(Livre livre)
  31:      {
  32:          entrepotLivres.Ajouter(livre);
  33:          return this;
  34:      }
  35:   
  36:      public EntrepotLivres Creer()
  37:      {
  38:          return entrepotLivres;
  39:      }
  40:  }

 

Un mot sur SetUp

 

On pourrait aussi être tenté d'instancier une partie du contexte des tests dans la méthode de SetUp de la classe de test (attribut [SetUp] avec NUnit).

Ce n'est pas forcément l'idéal en termes de lisibilité, et en tout cas on ne devrait construire dans cette méthode que les données qui sont le plus petit dénominateur commun entre tous les tests. En général, je préfère réserver le SetUp (et le TearDown) pour des opérations de plomberie générale des tests qui ne touchent pas au données, comme le démarrage et le rollback d'une transaction.

Dans ce sens, je suis assez d'accord avec Roy Osherove, auteur de The Art Of Unit Testing, qui écrit (p. 191) :

« Ma préférence personnelle est que chaque test crée ses propres mocks et stubs en appelant des méthodes de helpers dans le test lui-même, de façon à ce que le lecteur du test sache exactement ce qui se passe, sans avoir besoin de sauter du test au setup pour comprendre le tableau d'ensemble. »

Recette

Une alternative originale

 

Le Danois Mark Seemann propose une approche légèrement différente, qui fait partie d'un cortège de design patterns et conventions qu'il a appelé ZeroFriction TDD. Bien que je ne sois pas totalement persuadé par tous les patterns présentés (certains sont très orientés programmation défensive), un d'entre eux mérite qu'on s'y attarde : Fixture Object.

Seemann part du constat qu'en utilisant des helpers de tests statiques, on a forcément un contexte de test stateless, ce qui nous prive de certaines commodités. L'idée est de constituer un objet Fixture contenant l'état actuel des données de test et aussi la référence au système à tester (SUT). Ainsi le corps du test garde sa lisibilité et sa maintenabilité :

 

   1:  [Test]
   2:  public void GetTousLivres_Retourne_Tous_Livres()
   3:  {
   4:      EntrepotLivresFixture fixture = new EntrepotLivresFixture();
   5:   
   6:      fixture.AjouterPlusieursLivres();
   7:   
   8:      // System Under Test
   9:      EntrepotLivres sut = fixture.Sut;
  10:      List<Livre> resultats = sut.GetTousLivres();
  11:   
  12:      Assert.AreEqual(fixture.Livres, resultats);
  13:  }

 

On remarque que deux conventions de Zero Friction TDD censées reposer l'oeil et le cerveau du lecteur du test sont utilisées : Naming SUT Test Variables, qui préconise de nommer toujours de la même manière la variable de l'objet testé, et Naming Direct Output Variables, qui conseille la même chose pour le résultat qui va être asserté.

Voici l'implémentation du Fixture Object :

 

   1:  internal class EntrepotLivresFixture
   2:  {
   3:      public EntrepotLivresFixture()
   4:      {
   5:          Sut = new EntrepotLivres();
   6:          this.Livres = new List<Livre>();
   7:      }
   8:   
   9:      internal EntrepotLivres Sut { get; private set; }
  10:      internal IList<Livre> Livres { get; private set; }
  11:      internal int Plusieurs { get { return 3; } }
  12:      internal Livre PremierLivre { get { return Livres[0]; } }
  13:      internal Livre DeuxiemeLivre { get { return Livres[1]; } }
  14:   
  15:      internal void AjouterPlusieursLivres()
  16:      {
  17:          for (int i = 0; i < Plusieurs; i++)
  18:          {
  19:              Livre livre = new Mock<Livre>().Object;
  20:              Livres.Add(livre);
  21:              Sut.Ajouter(livre);
  22:          }
  23:      }
  24:  }

 

 

Voilà, ceci n'est qu'un échantillon des différentes stratégies de factorisation de tests imaginables. Il y a probablement des milliers de solutions possibles.

Et vous, quelle est votre régime minceur préféré pour vos tests ?

vendredi 5 mars 2010

Règlement de comptes à Données Corral

Il va y avoir du sport

Derrière le jeu de mots vaseux de ce titre se cache une réalité beaucoup moins fun. Celle de la guerre que se livrent, depuis quelques temps déjà, les partisans d'une approche des bases de données novatrice et iconoclaste et les défenseurs de la tradition des SGBD relationnels.

Si vous suivez l'actualité du monde du développement, vous avez déjà compris de quoi je veux parler. Il ne vous a pas échappé que ces derniers temps, l'hégémonie des systèmes relationnels sur le marché des bases de données a été quelque peu chahutée : en 2009 sont apparues un certain nombre de bases regroupées sous le nom parapluie de "NoSQL". Ces systèmes (CouchDB, Big Table de Google, Project Voldemort...) sont des key-value stores, des document stores ou des bases fondées sur les graphes qui ont en commun de partir d'un constat de lourdeur du relationnel, du langage SQL et de ses jointures. Elles se veulent plus simples d'approche, plus scalable pour de gros volumes de données, plus adaptées au web et moins sclérosées par les schémas de données.


La rébellion gagne des voix

Il est intéressant de voir qu'en ce début d'année, il y a un regain de tension dans le débat qui oppose les deux visions et que celui-ci s'est invité chez les développeurs .NET. Plus particulièrement, deux acteurs importants de la communauté et qui n'ont pas a priori d'intérêt particulier à soutenir les bases NoSQL, ont pris position pour les défendre.

Le premier est Ayende Rahien, contributeur du projet NHibernate et créateur entre autres du framework d'isolation RhinoMocks. Dans un article nommé Slaying Relational Dragons, il remet en cause l'hégémonie des SGBDR et cite une session de support client à l'issue de laquelle il a recommandé de ne pas utiliser de base relationnelle. S'en suit un exemple de type d'application pour laquelle selon lui, il est tout à fait justifié d'opter pour un document store du style CouchDB. Plus étonnant, Ayende a également démarré un projet de base document, Rhino DivanDB, alors même qu'une grande partie de son travail des dernières années a été dévoué indirectement aux bases relationnelles par le biais d'NHibernate.

Une autre chose a piqué ma curiosité dans le billet d'Ayende : pour décrire les grappes de données pêchées dans une base NoSQL, il utilise le terme d'Agrégats. Oui, c'est à peu près la même notion d'Agrégat que dans Domain Driven Design. Visuellement, cela peut donner ceci (2 racines d'agrégat Book en l'occurrence) :

Agrégats

Si on cherche un peu, on s'aperçoit aussi que des frameworks DDD comme Jdon prévoient d'entrée l'utilisation d'une base NoSQL pour la persistance. Y aurait-il une synergie entre DDD et NoSQL dans la forme sous laquelle les entités sont appréhendées ? Intéressant, à creuser en tout cas.

Notre deuxième homme est Greg Young, très impliqué dans DDD justement, et qui a popularisé l'approche Command-Query Separation. Greg a comparé dans un billet récent l'utilisation d'un ORM au fait d'embrasser sa soeur (expression américaine désignant une action dénuée d'intérêt)... Pour lui, nous devrions plus souvent nous arrêter et nous demander si le choix d'un ORM couplé à un modèle de données relationnel pour notre projet, est bien justifié. Plutôt que de faire de l'ORM + SGBDR le choix par défaut, pourquoi ne pas envisager une base objet ou une base document à la place ? Dans certains cas, c'est beaucoup plus adapté au contexte et ça évite les problèmes de décalage d'impédance entre l'application objet et le modèle de données.


L'Empire contre-attaque

Bien sûr, les défenseurs des SGBD relationnels ont tôt fait de réagir. Un des plus virulents dans la contre-offensive a certainement été Frans Bouma, curieusement acteur de la scène ORM lui aussi (avec LLBLGen). Dans des commentaires et sur son blog, il avance trois arguments principaux pour contrer les enthousiastes du NoSQL :

  • Les cas évoqués par Ayende sont des anti-pattern, on essaie de créer un modèle de données qui est directement calqué sur la mise en forme d'un écran de l'application, ce qui est une mauvaise pratique.
  • Un modèle de données a pour vocation de représenter la réalité, et pas juste de refléter des entités utilisées dans une application (on retrouve un peu ici l'approche bottom-up vs l'approche top-down). Pourquoi ? Parce que dans un contexte d'entreprise, de multiples logiciels accèdent aux mêmes données et c'est de plus en plus vrai au fil du temps. Il faut donc un modèle qui représente parfaitement le métier et en est le garant quelle que soit l'application qui y puisera.
  • Les bases relationnelles existent depuis plusieurs dizaines d'années, elles sont fondées sur une théorie solide et ont fait l'objet d'innombrables recherches, ce qui en fait les outils incontournables et aboutis qu'on connait aujourd'hui. Toute concurrence est donc pour l'instant anecdotique.


Verdict

Conceptuellement, on voit bien le clivage entre les bases de type document store qui recèlent le strict nécessaire permettant à une application de fonctionner (le tout taillé sur mesure pour elle seule : une sorte de YAGNI de la donnée), et un modèle relationnel qui essaie de capturer la réalité de manière parfaite pour disposer d'une clé qui déverrouillera tous les situations à venir.

Sans prendre parti pour un camp ou l'autre, j'ai peur qu'à l'heure actuelle les bases NoSQL manquent de maturité face aux mastodontes relationnels. Mais elles restent une alternative à explorer et à mon avis, elles n'ont pas dit leur dernier mot. La guerre est loin d'être finie.

dimanche 6 décembre 2009

DDD Vite Fait, notions avancées

DDD Vite Fait Partie 2

Comme promis, voici la deuxième partie de la traduction de DDD Quickly : DDD Vite Fait, partie 2, notions avancées.

On y retrouve des techniques pour mettre en oeuvre DDD sur de gros projets impliquant plusieurs équipes, mais aussi pour refactorer un domaine et gérer sa croissance, notamment à travers la distillation et l'identification d'un Coeur de Domaine.

Bonne lecture ;)

(Mise à jour) : et voici le fichier complet avec les deux parties.

jeudi 22 octobre 2009

Impressions sur Visual Studio 2010

La béta 2 de Visual Studio 2010 étant sortie il y a peu, il était grand temps pour moi de voir ce que cette nouvelle mouture de l'IDE de Microsoft avait sous le capot.

Un tour sur la page de download de la béta, et après un processus d'installation classique mais efficace, la nouvelle interface s'offre à nos yeux.

Visual Studio 2010

Bon, à part une couleur bleu horizon du plus bel effet, on ne peut pas dire que la page de démarrage soit une révolution par rapport à Visual Studio 2008. Nouveau/Ouvrir Projet, Projets récents, quelques nouveaux liens comme la personnalisation de la page d'accueil, les habitués ne seront pas dépaysés.

Lorsqu'on ouvre un solution et qu'on commence à jouer avec la bête, petite déception : l'interface ne semble pas très réactive. Même si c'est pardonnable pour une béta, on constate à l'usage beaucoup de lenteurs et même quelques freezes momentanés (machine sous Windows Vista, Core 2 Duo 2 GHz, 4 Go de RAM). Peut-être la faute à l'IHM basée sur du WPF.

Mais voyons maintenant les principales nouveautés autour de ce qui nous intéresse le plus : la manipulation du code.


Navigation

Quick Search

Les développeurs de Visual 2010 avaient annoncé une navigabilité dans le code grandement améliorée, et c'est certainement un des domaines où le plus de progrès ont été faits. Voici les avancées les plus marquantes :

  • Quick Search : activée par un Ctrl-virgule, cette boite de dialogue permet une recherche intuitive avec suggestion de résultat lorsqu'on tape les premières lettres d'une classe ou les majuscules qui composent son nom. En fait, un passage incontournable pour naviguer entre vos fichiers/classes, tellement il est pratique.
  • Surlignage de références : lorsque le curseur se trouve sur une variable, méthode ou type, après quelques instants celle-ci est discrètement surlignée ainsi que toutes ses occurrences dans le fichier actif. Une aide visuelle non négligeable qui évite de chercher les références manuellement.
  • Hiérarchie des appels : cette nouvelle fonctionnalité affiche un arbre contenant les appels à la méthode/property sélectionnée, les appels au code qui l'appellent, et ainsi de suite. Cela évite de faire des "find usages" successifs lorsqu'on veut remonter une pile d'appels dans le code.


Refactoring

Smart tag

Au système d'affichage d'erreurs à la volée déjà existant, les développeurs de Visual Studio 2010 ont ajouté, exactement à la manière d'un ReSharper, des petites boîtes de suggestion (les SmartTags) qui lorsqu'elles sont déroulées proposent un menu avec diverses actions de refactoring et de génération de code.

Si certaines sont utiles en toute occasion (implémentation/héritage automatique, suggestion d'ajout de références...), d'autres sont clairement orientées TDD comme la génération de stubs de classe ou de méthodes qui n'existent pas encore.
Le raccourci pour les SmartTags est Ctrl-point-virgule.


Un tueur de ReSharper ? Pas vraiment

VS2010-ReSharper

Au vu de ces éléments, on pourrait penser que Microsoft a encore fait ce qu'il sait le mieux faire, c'est à dire copier les innovations de plus petits acteurs du marché pour les intégrer et les généraliser dans ses mastodontes. Le petit poucet innovant étant dans le cas présent JetBrains avec son ReSharper, plug-in tellement utile à la vie du développeur (il comportait déjà bon nombre des fonctionnalités de VS 2010 citées ci-dessus dès ses premières versions) qu'on avait du mal à s'en passer.
Le problème, c'est que Microsoft a copié cette recette à succès, mais en oubliant quelques ingrédients. Il manque à Visual 2010 tout un tas de petites fonctions de ReSharper qui me font dire que ce dernier sera toujours indispensable :

  • Pas de fermeture automatique des accolades
  • Pas d'autocomplétion des noms de variables avec un nom par défaut
  • Pas de go to Inheritor / base
  • Pas de surlignage des erreurs à la volée pour certains types d'erreurs (ex : non implémentation d'une interface)
  • SmartTags mal placés : par exemple sur le nom de la classe mère après les deux points d'un héritage, mais pas sur le nom de la classe fille donc peu visible
  • etc.

Au final, après quelques heures de manipulation de Visual Studio 2010, mon impression est qu'il n'est toujours pas aussi facile à piloter qu'un 2008 + ReSharper. Il manque cette réactivité, cette intuitivité qui font que l'IDE semble répondre au doigt et à l'oeil du développeur et lui apporte une productivité maximum.

Si l'on ajoute que JetBrains met la barre encore plus haut pour le futur ReSharper 5, je pense que le petit plug-in a encore de beaux jours devant lui...

mercredi 14 octobre 2009

DDD Vite Fait, les fondamentaux de Domain Driven Design

DDD Vite Fait

Dans mon précédent billet, j'évoquais l'importance à mes yeux de l'approche collaborative du développement logiciel qu'apporte Domain Driven Design et je vous parlais d'un travail en cours sur DDD.

Et bien le voici : DDD Vite Fait, première partie - les fondamentaux de DDD.

Ce livre (en version originale DDD Quickly, par A. Avram et F. Marinescu) présente tous les concepts et patterns qui forment la base de Domain Driven Design. Le texte est voulu comme une courte introduction - 54 pages tout de même ! - avec comme but avoué l'hégémonie planétaire de DDD de diffuser la vision initiée par l'auteur Eric Evans et faire (re)découvrir Domain Driven Design, approche de développement logiciel parfois mal connue en France.

Vous pouvez télécharger le PDF de la version française ici ou sur le site DDDFrance.

Un grand merci à Floyd Marinescu pour m'avoir autorisé à traduire son ouvrage, et rendez-vous dans quelques semaines pour découvrir la deuxième partie : DDD, notions avancées...

jeudi 1 octobre 2009

Smells DDD, quand votre domaine sent des pieds

Smell

En ce moment, je me replonge dans Domain Driven Design, l'approche du développement logiciel orientée domaine initiée par Eric Evans. Il m'est subitement apparu une chose que je ne m'étais jamais représentée auparavant, certainement parce que j'avais abordé le sujet avant tout par son aspect technique : le plus important à mon avis dans DDD, ce ne sont pas les patterns, c'est la philosophie de la modélisation collaborative du domaine.

Comprenez-moi bien, j'estime que les design patterns évoqués dans DDD sont brillants, ce sont des guides précieux pour développer un code du domaine clair, maintenable et faiblement couplé. Mais pour moi, l'essence de DDD ne réside pas là. Elle réside dans la collaboration étroite entre experts métiers, concepteurs et développeurs pour construire un modèle du domaine qui reflète la réalité et permet de résoudre de vraies problématiques métier. Elle réside dans la constitution d'un langage commun qui, en plus de faciliter la compréhension du métier par les personnes techniques, instaure dialogue et confiance au sein de l'équipe projet. Car je pense que c'est cela qui, au final, permet de faire du bon logiciel.

Je me suis sans doute heurté à ce constat car ce n'est pas du tout le cas dans l'organisation dans laquelle je travaille actuellement. Ca serait même plutôt le contraire. La forme de communication privilégiée est le fichier Word de besoin, de recette ou le ticket d'outil de gestion de bugs. Les conversations téléphoniques sont rares et ont souvent lieu en famille, entre développeurs ou entre experts métier. Sans parler de vraies rencontres entre développement et métier, inexistantes. Une méfiance d'origine floue entre les deux camps a installé une guéguerre perpétuelle MOE/MOA, les uns accusant les autres d'en demander toujours trop et de changer sans cesse de besoin ; les seconds soupçonnant les premiers d'être des tire-au-flanc. Du coup, l'idée de base chez la MOE est de se conformer à la virgule près aux documents, tels des actes notariaux, et chez la MOA d'en fourrer le plus possible dans lesdits documents. Bien sûr, c'est la connaissance du domaine qui en pâtit, tant l'incompréhension est grande. Il est même surprenant de voir que des développeurs avec plusieurs années dans l'organisation n'ont qu'une vision parcellaire et approximative du métier. Au final, le résultat se ressent dans la qualité du logiciel.

D'ailleurs - et ça ne s'applique pas seulement à mon exemple - de tels problèmes refont souvent surface sous la forme de code smells qu'on pourrait appeler Domain smells. Je me suis amusé à en recenser quelques-uns :

  • Domaine fantôme : ça peut paraître abérrent, mais il n'y a pas de couche domaine dans le projet dont je parle, en partie à cause de la technologie employée. Le métier est fondu dans la masse du reste du code. Il va sans dire qu'une couche domaine séparée est indispensable, pour toutes les raisons de découplage, de lisibilité... qu'on peut imaginer.
  • Domaine râpé : On constate aussi parfois que des bouts de domaine sont saupoudrés dans des couches différentes, le plus dur étant de ne pas être tenté d'en mettre dans la couche présentation. Même si ça peut être justifié, il vaut mieux pour la maintenabilité de l'application conserver au même endroit tous les éléments liés au domaine.
  • Domaine indéchiffrable : un des principes de DDD est que le code du domaine doit être clair et compréhensible, et donner tout de suite une vision d'ensemble à celui qui y jette un oeil. Malheureusement ce n'est pas toujours le cas, souvent à cause d'une représentation brouillonne du domaine dans le code dûe à un manque de dialogue avec les experts métier. De plus il est facile de se perdre en créant toujours plus de helpers, de services et autres gestionnaires qui embrouillent la compréhension du domaine.
  • Abus de notion : qui n'a jamais été confronté au mot-valise du domaine, à la notion métier un peu floue qui se retrouve toutes les trois lignes de code dans chaque module de l'application ? Comme si un développeur avait trouvé que le mot sonnait bien et décidé de l'utiliser aussi fréquemment que possible. C'est souvent le signe d'une modélisation pas assez en profondeur, d'un concept du domaine qui recouvre trop d'aspects et devrait sans doute être subdivisé.
  • Frères jumeaux : parfois c'est l'inverse, deux mots différents sont employés à travers l'application pour recouvrir la même notion métier. Cela peut mener à penser qu'il s'agit de deux choses différentes, ce qui est source de confusion. Convenir d'un seul terme précis et bien identifié permettra d'éviter les malentendus.
  • Déphasage de concept : ce smell se fait sentir lorsqu'on interroge un expert métier sur une notion présente dans le code du domaine, et qu'on s'aperçoit qu'elle est en total décalage avec la réalité, même si l'application semble se comporter correctement pour l'utilisateur. Il peut s'agir d'une mauvaise interprétation ou d'une supposition hasardeuse de la part d'un développeur. En tout cas, le code est à reprendre et la communication est à revoir.

Je vois d'ici certains dire "C'est bien beau, mais les experts métier ne sont pas souvent disponibles et une communication écrite restreinte est mieux que pas de communication du tout." Bien sûr, mais après tout, n'est-ce pas dans l'intérêt de la personne métier d'avoir une collaboration aussi efficace que possible avec le développeur qui réalisera l'application pour lui ? N'est-ce pas primordial de s'assurer qu'il est au diapason de la réalité du domaine ?

Même si l'expert métier ne peut pas se rendre souvent disponible, il devrait l'être autant que faire se peut. Une solution au problème pourrait être de timeboxer les scéances de conception collaborative. Parfois, un dialogue d'une demi-heure bien cadré et sans perturbation peut être plus efficace qu'une réunion décousue de deux heures, qui aura pourtant demandé plus de disponibilité à l'expert du domaine...

samedi 12 septembre 2009

Retour sur .NET RIA Services

Il y a peu, nous avons décidé au sein de notre équipe de faire migrer un projet en cours de développement de Silverlight 2 vers Silverlight 3, ce dernier étant sorti en release officielle au courant de l'été. Contrairement à ce que nous craignions, l'opération s'est bien passée, notamment grâce à cette petite checklist bien utile. Une fois les tools Silverlight 3 installés, l'assistant de conversion de Visual Studio s'occupe d'à peu près tout, sauf quelques mises à jour à faire si vous utilisez des services WCF.

Silverlight

Mais une autre question s'est posée à nous : fallait-il transformer nos services WCF Silverlight 2 (assez lourds à manipuler et à mettre à jour) en RIA Services ? En effet ce nouveau framework applicatif paraissait contenir plein de bonnes surprises pour nous faciliter la vie. Utilisant pour prendre la décision une méthode un peu débile mais qui marche, à savoir un tableau avec une colonne + et une colonne -, voici ce que nous avons obtenu :

Du côté des avantages :

+ Un découpage clair des couches et un système de DomainContext qui peut se marier avec du MVVM, du MVC ou même du MVP (solution que nous avions retenue).

+ Les classes clientes des services côté Silverlight sont automatiquement générées lors du build des services, pas de référence de service à ajouter ou mettre à jour contrairement à du WCF classique. Lors du passage sur un serveur d'intégration continue ou de recette, on s'ôte aussi un gros poids puisque la mise à jour des références vers le nouvel emplacement des services n'a pas lieu d'être.

+ Tout un tas de facilités introduites aussi bien au niveau de l'interrogation des services que de la mise à jour des entités.

+ Exceptions de services WCF mieux gérées qu'avec Silverlight 2 (le retour d'exceptions du service vers le client Silverlight n'était tout simplement pas géré) et Silverlight 3 (où il faut surcharger un comportement pour générer le bon code d'erreur à renvoyer au client).

+ Meilleure prise en charge des rôles et droits utilisateur...

Les inconvénients :

- RIA Services est encore en CTP, pour une sortie pas prévue avant 2010 (!)

- Il faut une petite bidouille pour installer RIA Services sur un Visual Studio français.

- Une testabilité ... à tester. Le mocking des DomainServices n'a pas l'air évident même si Nikhil Kothari a proposé une solution sur son blog.

- Mauvaise intégration avec ReSharper : il faut inclure les classes clientes générées par RIA Services dans le projet Visual Studio sinon ReSharper ne retrouve pas ses petits.

Tout compte fait, ça a beau ne pas être une version finale, les avantages et les gains de temps induits sont tels qu'on ne voit pas comment trouver le courage de s'en passer ! Reste le problème de la stabilité d'une CTP et de la nécessité de repenser les services, qui peut, on le comprend, en rebuter certains. Toutefois, dans un projet très peu critique comme le nôtre, le risque semble en valoir la chandelle.

page 2 de 2 -