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> where);
  5.  
  6. IList Fetch(Expression> 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

2 Responses to “Le pattern Repository, testing et domain driven design…”

  1. Gauthier Segay déc 1st 2008 at 03:11 1

    Ton approche m’a convaincu!

    j’utilise à ce jour une implémentation de base pour les repositories qui sont exposés à mes services.

    Le problème est effectivement d’avoir un ensemble de méthodes qui n’ont parfois aucun sens, un autre problème est d’avoir des signatures de méthode sur cette implémentation dépendant d’un framework particulier (NHibernate…) et de leaker cette dépendance dans la couche de service.

    Je vais donc en profiter pour refactoriser ma solution tant qu’il en est encore temps (40 erreurs de compilation en retirant toutes les méthodes de mon interface IEntityRepository, je devrais m’en sortir…) en renomant IEntityRepository en IEntityPersistenceRepository et en cassant la chaine d’héritage des interfaces spécifiques à des racines d’aggregats (IProductRepository n’héritera plus de IEntityRepository…).

    Le sujet est d’ailleurs abordé sur la liste ddd:

    http://tech.groups.yahoo.com/group/domaindrivendesign/message/9047

    une solution proposée qui ne m’a pas l’air trop mal est d’utiliser des interfaces sans héritage: du genre IProductRepository qui n’hérite pas de IRepository avec une implémentation du genre ProductRepository: IRepository, IProductRepository.

    Entre les deux approches, mon coeur balance :)

  2. Julien déc 2nd 2008 at 09:13 2

    L’approche avec un ProductRepository qui hérite de Repository mais n’implémente pas IRepository est pratique et rapide à mettre en place. Par contre, elle n’apporte pas de solution pour les tests (mais ce n’est peut être pas un objectif pour toi), ou ai-je mal compris?.
    Autre (petit) problème : Si une opération comme save() ou delete() n’est pas présente dans IProductRepository, il est toujours possible de récupérer une instance de IRepository (qui retournera toujours le même ProductRepository) pour effectuer ces opérations. Dans mon expérience, ca peut porter à confusion et pousser les devéloppeurs à demander à l’outils de DI des IRepository plutot que des I*Repository.

Trackback URI | Comments RSS

Leave a reply