Archive for novembre, 2008

Rencontres Alt.net, attention ça grandit !

Julien on nov 24th 2008

Rappelez vous, la première rencontre Alt.net Paris, c'était le 30 avril 2008. Nous étions 5 à avoir fait le déplacement pour le "lancement" Français. Que de chemin parcouru sous l'initiative de Rob depuis !

En effet, après la dernière rencontre hébergée chez Valtech, avec pour sujet Mono, nous nous retrouverons ni plus ni moins chez Microsoft pour la ... 8ème édition! Rendez-vous donc le 2 décembre, au 148 rue de l'université. Pour les inscriptions, il suffit de laisser un message sur notre mailing-list.

Le thème de la soirée est le Domain Driven Design, sujet que Gauthier (qui n'a toujours pas de blog !) et moi essayerons d'introduire par une petite présentation, avant de lancer les débats. De bon moments en perspective !

Venez nombreux !

Filed in Rencontres Alt.net | No responses yet

DDD et internal

Julien on nov 21st 2008

Je poursuis mes réflexions sur l'impact que le domain driven design a sur ma façon de développer... :-)

Jusqu'à très récemment, le nombre de fois ou j'ai utilisé le mot clef internal devait se compter sur les doigts des 2 mains. La faute au fait que je n'ai que rarement développé des API consommés par l'extérieur? Ou peut être tout simplement un gros défaut dans mon style de développement :-)!
Quoiqu'il en soit, j'ai remarqué que j'avais fortement évolué depuis que je m'intéresse au domain driven design. Je vais reprendre l'exemple qui me tient à cœur en ce moment, a savoir le cas d'un panier sur un site de e-commerce !

J'ai donc 3 classes. ShoppingCart, Product et ShoppingCartItem :

  1.  
  2. public class ShoppingCart
  3. {
  4. private int _id;
  5. private readonly IList<ShoppingCartItem> _items;
  6.  
  7. public ShoppingCart()
  8. {
  9. _items = new List<ShoppingCartItem>();
  10. }
  11.  
  12. // + les getters
  13. }
  14.  
  15. public class Product
  16. {
  17. private int _id;
  18. private readonly string _name;
  19.  
  20. public Product(string name)
  21. {
  22. _name = name;
  23. }
  24.  
  25. // + les getters
  26. }
  27.  
  28. public class ShoppingCartItem
  29. {
  30. private readonly Product _product
  31. private readonly ShoppingCart _cart;
  32. private int _quantity;
  33.  
  34. public ShoppingCartItem(ShoppingCart cart, Product product)
  35. {
  36. _cart = cart;
  37. _product = product
  38. }
  39.  
  40. // + les getters
  41. }
  42.  

La modélisation est simple et à priori valide. Si je veux ajouter un Product à mon ShoppingCart, j'écrirai le code suivant :

  1.  
  2. var myCart = new ShoppingCart();
  3. var myProduct = new Product();
  4.  
  5. myCart.Items.Add(new ShoppingCartItem(myCart, myProduct));
  6.  

SSSSSTTTTOOOOOOPPPPPPP !!
Certes, ce code est valide mais je viole plusieurs principes (par exemple la loi de Déméter) et myCart.Items.Add(new ShoppingCartItem(myCart, myProduct)); n'a pas beaucoup de sens au niveau du modèle. En effet, si on se met à la place d'un client d'Amazon, on va ajouter un produit à son panier (un dvd ou autre) et non pas autre chose. Il serait donc beaucoup plus logique d'avoir le code suivant :

  1.  
  2. var myCart = new ShoppingCart();
  3. var myProduct = new Product();
  4.  
  5. myCart.AddProduct(myProduct);
  6.  

Une implémentation de AddProduct pourrait ressembler à cela :

  1.  
  2. public ShoppingCartItem AddProduct(Product product)
  3. {
  4. ShoppingCartItem item = _items.SingleOrDefault(x => x.Product == product);
  5.  
  6. if(item == null) {
  7. item = new ShoppingCartItem(this, product);
  8.  
  9. _items.Add(item);
  10. } else {
  11. item.Quantity += 1;
  12. }
  13.  
  14. return item;
  15. }
  16.  

Note : la collection Items deviendrait évidemment une ReadOnlyCollection !

Pour revenir au sujet de cet article, quel est le rapport avec le mot clef internal?

Dans ce contexte précis (et c'est évidemment très dépendant de votre domaine), la classe ShoppingCartItem ne devrait jamais être instancié directement en dehors du modèle, cela n'a pas de sens. J'irai même plus loin : seules la classe ShoppingCart et la couche de persistance ne devrait pouvoir l'instancier. A défaut de mieux, on devra donc modifier l'accessibilité du constructeur de ShoppingCartItem à internal afin d'éviter que les consommateurs du domaine n'en fasse mauvais usage.

Filed in Domain Driven Design | 2 responses so far

SpecUnit-net : Une « Fluent Interface » pour les assertions des tests unitaires

Julien on nov 19th 2008

Olivier m'a fait remarqué que j'utilisais une "Fluent Interface" pour mes tests unitaires, au lieu des classiques Assert.* dans mon article précédent. Cela permet de remplacer le test suivant :

  1. [Test]
  2. public void should_create_a_giftboxItem_with_quantity_of_1()
  3. {
  4. Product product = ProductHelpers.CreateProduct();
  5. _gb.AddProduct(product);
  6.  
  7. Assert.AreEqual(1, _gb.Items.Count);
  8. Assert.AreEqual(product, _gb.Items[0].Product);
  9. Assert.AreEqual(1, _gb.Items[0].Quantity);
  10. }

par :

  1. [Test]
  2. public void should_create_a_giftboxItem_with_quantity_of_1()
  3. {
  4. Product product = ProductHelpers.CreateProduct();
  5. _gb.AddProduct(product);
  6.  
  7. _gb.Items.Count.ShouldEqual(1);
  8. _gb.Items[0].Product.ShouldEqual(product);
  9. _gb.Items[0].Quantity.ShouldEqual(1);
  10. }

On aime ou on aime pas, personnellement je trouve que c'est un peu plus lisible.
Pour ceux que ça intéressent, les méthodes d'extensions sont extraites de SpecUnit-net, et spécifiquement de cette classe.

Enjoy!

Filed in Alt.net | 2 responses so far

Le pattern Repository, testing et domain driven design…

Julien on nov 19th 2008

J'ai récemment posé la question suivante sur la mailling list française d'alt.net :

J'aimerai savoir comment vous testez vos repository dans vos projets,
et quels sont les + et les - de vos solutions.

Je sais qu'il y a plusieurs solutions :
1) Tester le repository avec une bdd de dev "classique"
2) Tester le repository avec une base sqlite chargée en mémoire
3) Ajouter une couche d'abstraction supplémentaire entre le repository
et la persistance, puis remplacer la couche persistance lors des tests
par une implémentation Fake à base de hastable ou autre.

La solution 1) n'est pas une option à cause de la vitesse d'execution
des tests.

La solution 2) est très intéressante mais un peu galère à mettre en
place (car il faut recréer le schéma systématiquement, sachant qu'un
schéma pour sql server n'est pas compatible avec sqlite sans
modifications manuelles). Je voulais utiliser migrator.net pour gérer
le problème mais c'est un peu buggé.

La solution 3) me parait la plus simple, mais elle ajoute un niveau
d'abstraction supplémentaire qui n'a à priori raison d'être que pour
les tests

Quel est votre expérience? Que pensez vous de chaque solution? quelles
sont vos best-practices?

Thanks!

J'ai eu des réponses très intéressantes à cette question qui m'ont permis de réfléchir et de trouver la méthode qui me satisfait le plus.

Pour comprendre mon choix, il faut d'abord revenir sur le pattern Repository...

La façon la plus simple d'implémenter ce pattern est d'utiliser un repository générique dont hériteront des repository typés. On se retrouve par exemple avec cela :

  1. public interface IRepository where T : Entity
  2. {
  3. T Find(long id);
  4. T FindBy(Expression&gt; where);
  5.  
  6. IList Fetch(Expression&gt; where);
  7. IList FetchAll();
  8.  
  9. void Save(T target);
  10. void Delete(T target);
  11. }
  12.  
  13. public interface IProductRepository : IRepository
  14. {
  15. IList FetchTopProducts();
  16. IPaginable Search(string keywords, long categoryId);
  17. }
  18.  

Puis :

  1. public class Repository : IRepository
  2. {
  3. // ...
  4. }
  5.  
  6. public class ProductRepository : Repository, IProductRepository
  7. {
  8. // ...
  9. }

Autrement dit, Repository se charge des fonctions de bases : chercher par id, sauvegarder, supprimer... Et ProductRepository se charge de construire les requêtes spécifiques a l'entité Product. C'est simple, rapide, efficace!

Cependant, cette implémentation à deux inconvénients :

  • Repository est une simple sur-couche à l'ORM que l'on utilise (NHibernate, Linq to Sql, Entity Framework, etc.). Pour tester ses repository, on est donc obliger de taper dans une base de test (ce qui est généralement lent) ou de mocker l'ORM (ce qui rends l'écriture des tests unitaires très fastidieuse).
  • Systématiquement hériter de Repository rends accessible l'ensemble des fonctionnalités de cette même classe. Hors cela peut être problématique du point de vue du domaine si l'on veut par exemple interdire de supprimer une entité! Pourquoi devrait-on offrir la fonction dans l'interface si le comportement est à proscrire?

Une solution plus flexible consiste à injecter Repository dans ProductRepository. Soit :

  1.  
  2. public interface IProductRepository
  3. {
  4. Product Find(long id);
  5. void Save(Product target);
  6.  
  7. IList<Product> FetchTopProducts();
  8. IPaginable<Product> Search(string keywords, long categoryId);
  9. }
  10.  
  11. public class ProductRepository : IProductRepository
  12. {
  13. private readonly IGenericRepository<Product> _genericRepository;
  14.  
  15. public ProductRepository(IGenericRepository<Product> genericRepository)
  16. {
  17. _genericRepository = genericRepository;
  18. }
  19.  
  20. public Product Find(long id)
  21. {
  22. return _genericRepository.Find(id);
  23. }
  24.  
  25. // ...
  26. }
  27.  

On constate alors que IProductRepository n'implémente plus IRepository, tout comme ProductRepository n'hérite plus de Repository.

Cette implémentation règle nos deux problèmes :

  • Je n'expose que les fonctionnalités qui ont du sens par rapport à mon domaine. La qualité de ce dernier s'améliore en conséquence!
  • Je peux injecter dans ProductRepository un InMemoryRepository pour faire mes tests. Je peux donc tester FetchTopProducts() sans faire d'aller retour sur la base et sans mocker mon ORM. Soit par exemple :
  1.  
  2. [Test]
  3. public void should_return_top_products()
  4. {
  5. var memoryRepository = new InMemoryRepository<Product>();
  6. var repository = new ProductRepository(memoryRepository);
  7.  
  8. memoryRepository.Save(new Product("Product1", true));
  9. memoryRepository.Save(new Product("Product2", true));
  10. memoryRepository.Save(new Product("Product3", false));
  11.  
  12. var results = _repository.FetchTopProducts();
  13.  
  14. results.Count.ShouldEqual(2);
  15. results[0].Name.ShouldEqual("Product1");
  16. results[0].IsTopProduct.ShouldBeTrue();
  17. }
  18.  

Filed in Domain Driven Design | 2 responses so far

Asp.net MVC : SubControllers

Julien on nov 18th 2008

Grâce à MVC Contrib, un librairie open source couvrant certains aspects absents d'Asp.net MVC, il est possible d'utiliser des "SubControllers" afin de réutiliser des unités logiques au sein de plusieurs actions en minimisant la duplication de code. Matt Hinze, à l'origine du projet en explique le fonctionnement ici.

Pour récupérer les instances des sous contrôleurs, Matt utilise l'injection de paramètre au niveau des actions. Cette méthode à le mérite d'être simple et efficace mais je trouve qu'elle nuit aussi à la lisibilité du code, dans la mesure ou ce paramètre n'est pas réutilisé dans le corps de l'action. J'ai donc légèrement modifié la syntaxe en introduisant un attribut :

  1. [SubController(typeof(ProductSearchSubController), SubControllerKeys.ProductSearch)]
  2. public ActionResult Index()
  3. {
  4. return View();
  5. }

L'attribut prends 2 paramètres, le type du sous contrôleur à instancier et une chaine de caractère qui définit la clef du dictionnaire ViewData dans lequel on stockera l'instance à invoquer. J'utilise ici une constance, SubControllerKeys.ProductSearch mais on pourrait tout à fait la remplacer par "ProductSearch". L'invocation coté vue ressemblera alors à cela :

  1. &lt;% Html.ViewData.Get("ProductSearch").Invoke(); %&gt;

Voici le code pour cet attribut :

  1. [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
  2. public class SubControllerAttribute : ActionFilterAttribute
  3. {
  4. private Type _subControllerType;
  5. private string _viewDataKey;
  6.  
  7. public Type SubControllerType
  8. {
  9. get
  10. {
  11. return _subControllerType;
  12. }
  13. set
  14. {
  15. Guard.AgainstNull(value, "value can't be null");
  16. Require.That(typeof(ISubController).IsAssignableFrom(value), String.Format("The type '{0}' does not inherit from ISubController.", value.FullName));
  17.  
  18. _subControllerType = value;
  19. }
  20. }
  21.  
  22. public string ViewDataKey
  23. {
  24. get { return _viewDataKey; }
  25. set { _viewDataKey = value; }
  26. }
  27.  
  28. public SubControllerAttribute(Type subControllerType, string viewDataKey)
  29. {
  30. _subControllerType = subControllerType;
  31. _viewDataKey = viewDataKey;
  32. }
  33.  
  34. public override void OnActionExecuting(ActionExecutingContext filterContext)
  35. {
  36. Guard.AgainstNullOrEmpty(_viewDataKey, "View data key is not set");
  37. Guard.AgainstNull(_subControllerType, "Sub controller type is not set");
  38.  
  39. ISubController subController = (ISubController)ObjectFactory.GetInstance(_subControllerType);
  40. if (subController == null) {
  41. throw new InvalidOperationException(_subControllerType + " is not registered with StructureMap");
  42. }
  43.  
  44. filterContext.Controller.ViewData.Add(_viewDataKey, subController.GetResult(filterContext.Controller));
  45.  
  46. base.OnActionExecuting(filterContext);
  47. }
  48. }
  49.  

Deux remarques (qui expliquent pourquoi je n'ai pas soumis de patch pour MVC Contrib) :

  • J'utilise structure map pour construire mon instance du sous contrôleur (ligne 39). Idéalement il faudrait utilise une interface pour pouvoir plugger tout les outils d'injection de dépendances)
  • J'utilise une mini libraire maison pour tester les pré-conditions (Guard.* et Require.*). Il faudra donc la remplacer par des if ou une librairie équivalente.

P.S : Je viens de me rendre compte que je ne pouvais pas coller du code contenant des commentaires en xml. Quelqu'un peut me conseiller un plugin wordpress qui gère cela?

Filed in Asp.net MVC | 3 responses so far