Que veulent dire les programmeurs quand ils disent «Code contre une interface, pas un object»?

J’ai commencé la très longue et difficile tâche d’apprendre et d’ appliquer TDD à mon stream de travail. J’ai l’impression que TDD correspond parfaitement aux principes de l’IoC.

Après avoir parcouru certaines des questions étiquetées TDD ici dans SO, je lis que c’est une bonne idée de programmer contre des interfaces, pas des objects.

Pouvez-vous fournir des exemples de code simples de ce que c’est, et comment l’appliquer dans des cas d’utilisation réels? Des exemples simples sont essentiels pour moi (et les autres personnes qui veulent apprendre) pour comprendre les concepts.

Merci beaucoup.

Considérer:

 class MyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(MyClass c) { //Code you want to test c.Foo(); } } 

Parce que MyMethod accepte uniquement une MyClass , si vous souhaitez remplacer MyClass par un object simulé afin de tester les unités, vous ne pouvez pas. Mieux vaut utiliser une interface:

 interface IMyClass { void Foo(); } class MyClass : IMyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(IMyClass c) { //Code you want to test c.Foo(); } } 

Maintenant, vous pouvez tester MyMethod , car il utilise uniquement une interface, pas une implémentation particulière. Ensuite, vous pouvez implémenter cette interface pour créer n’importe quel type de simulation ou de faux à des fins de test. Il existe même des bibliothèques comme Rhino.Mocks.MockRepository.SsortingctMock() Rhino Mocks, qui prennent n’importe quelle interface et vous construisent un object simulé à la volée.

Tout est une question d’intimité. Si vous codez pour une implémentation (un object réalisé), vous êtes en relation intime avec cet “autre” code, en tant que consommateur. Cela signifie que vous devez savoir comment le construire (c’est-à-dire, quelles sont ses dépendances, éventuellement en tant que parameters de constructeur, éventuellement en tant que parameters), quand et comment vous en débarrasser.

Une interface devant l’object réalisé vous permet de faire quelques choses –

  1. Pour l’un, vous pouvez / devez exploiter une fabrique pour construire des instances de l’object. Les conteneurs IOC le font très bien pour vous, ou vous pouvez en créer vous-même. Avec des tâches de construction hors de votre responsabilité, votre code peut simplement supposer qu’il obtient ce dont il a besoin. De l’autre côté du mur d’usine, vous pouvez soit construire des instances réelles, soit simuler des instances de la classe. En production, vous utiliseriez du réel bien sûr, mais pour les tests, vous souhaiterez peut-être créer des instances compressées ou simulées dynamicment pour tester différents états du système sans avoir à exécuter le système.
  2. Vous n’avez pas besoin de savoir où se trouve l’object. Ceci est utile dans les systèmes dissortingbués où l’object auquel vous souhaitez parler peut être ou ne pas être local à votre processus ou même à votre système. Si vous avez déjà programmé Java RMI ou un ancien Skool EJB, vous connaissez la routine consistant à “parler à l’interface” qui masquait un proxy qui effectuait les tâches de mise en réseau et de rassemblement à distance dont votre client n’avait pas à s’occuper. WCF a une philosophie similaire de «parler à l’interface» et laisser le système déterminer comment communiquer avec l’object / service cible.

** UPDATE ** Il y avait une demande pour un exemple de conteneur IOC (Factory). Il y en a beaucoup pour quasiment toutes les plateformes, mais elles fonctionnent essentiellement comme ceci:

  1. Vous initialisez le conteneur sur la routine de démarrage de vos applications. Certains frameworks le font via des fichiers de configuration ou du code ou les deux.

  2. Vous “enregistrez” les implémentations que vous souhaitez que le conteneur crée pour vous en tant qu’usine pour les interfaces qu’elles implémentent (par exemple: inscrivez MyServiceImpl pour l’interface de service). Au cours de ce processus d’enregistrement, vous pouvez généralement définir une politique de comportement, par exemple si une nouvelle instance est créée à chaque fois ou si une seule instance (tonne) est utilisée.

  3. Lorsque le conteneur crée des objects pour vous, il injecte toutes les dépendances dans ces objects dans le cadre du processus de création (par exemple, si votre object dépend d’une autre interface, une implémentation de cette interface est à son tour fournie, etc.).

Pseudo-codishly ça pourrait ressembler à ceci:

 IocContainer container = new IocContainer(); //Register my impl for the Service Interface, with a Singleton policy container.RegisterType(Service, ServiceImpl, LifecyclePolicy.SINGLETON); //Use the container as a factory Service myService = container.Resolve(); //Blissfully unaware of the implementation, call the service method. myService.DoGoodWork(); 

Lors de la programmation sur une interface, vous écrirez du code qui utilise une instance d’une interface, et non un type concret. Par exemple, vous pouvez utiliser le modèle suivant, qui intègre l’injection de constructeur. L’injection de constructeur et d’autres parties de l’inversion du contrôle ne sont pas obligées de pouvoir être programmées par rapport aux interfaces. Cependant, étant donné que vous vivez du sharepoint vue TDD et IoC, je l’ai câblé pour vous donner du contexte familier avec.

 public class PersonService { private readonly IPersonRepository repository; public PersonService(IPersonRepository repository) { this.repository = repository; } public IList PeopleOverEighteen { get { return (from e in repository.Entities where e.Age > 18 select e).ToList(); } } } 

L’object du référentiel est transmis et est un type d’interface. L’avantage de passer dans une interface est la possibilité de «permuter» l’implémentation concrète sans modifier l’utilisation.

Par exemple, on peut supposer qu’au moment de l’exécution, le conteneur IoC injectera un référentiel qui sera connecté à la firebase database. Pendant le temps de test, vous pouvez passer un référentiel simulé ou stub pour exercer votre méthode PeopleOverEighteen .

Cela signifie penser générique. Pas spécifique.

Supposons que vous ayez une application qui informe l’utilisateur de lui envoyer un message. Si vous travaillez avec une interface IMessage par exemple

 interface IMessage { public void Send(); } 

vous pouvez personnaliser, par utilisateur, la façon dont ils reçoivent le message. Par exemple, quelqu’un veut être averti par e-mail et votre IoC va créer une classe concrète EmailMessage. Certains autres veulent SMS, et vous créez une instance de SMSMessage.

Dans tous ces cas, le code de notification à l’utilisateur ne sera jamais modifié. Même si vous ajoutez une autre classe concrète.

Le grand avantage de la programmation par rapport aux interfaces lors des tests unitaires est qu’elle vous permet d’isoler un morceau de code des dépendances à tester séparément ou de les simuler pendant le test.

Un exemple que j’ai mentionné ici quelque part est l’utilisation d’une interface pour accéder aux valeurs de configuration. Plutôt que de regarder directement ConfigurationManager, vous pouvez fournir une ou plusieurs interfaces permettant d’accéder aux valeurs de configuration. Normalement, vous devez fournir une implémentation qui lit le fichier de configuration, mais pour le tester, vous pouvez en utiliser une qui renvoie simplement des valeurs de test ou émet des exceptions ou autres.

Considérez également votre couche d’access aux données. Le fait que votre logique métier soit étroitement liée à une implémentation particulière de l’access aux données complique les tests sans avoir toute une firebase database à scope de main avec les données dont vous avez besoin. Si votre access aux données est masqué par des interfaces, vous pouvez fournir uniquement les données dont vous avez besoin pour le test.

L’utilisation d’interfaces augmente la «surface» disponible pour les tests, ce qui permet des tests plus précis qui testent réellement les différentes unités de votre code.

Testez votre code comme quelqu’un qui l’utilisera après avoir lu la documentation. Ne testez rien en fonction de vos connaissances car vous avez écrit ou lu le code. Vous voulez vous assurer que votre code se comporte comme prévu.

Dans le meilleur des cas, vous devriez pouvoir utiliser vos tests comme exemples, les doctests en Python en sont un bon exemple.

Si vous suivez ces directives, la modification de la mise en œuvre ne devrait pas poser problème.

De même, selon mon expérience, il est recommandé de tester chaque “couche” de votre application. Vous aurez des unités atomiques, qui en elles-mêmes n’ont pas de dépendances et vous aurez des unités qui dépendent d’autres unités jusqu’à ce que vous arriviez à l’application qui est en soi une unité.

Vous devriez tester chaque couche, ne vous fiez pas au fait qu’en testant l’unité A, vous testez également l’unité B dont dépend l’unité A (la règle s’applique également à l’inheritance). Cela aussi devrait être traité comme un détail d’implémentation, même même si vous vous sentez comme si vous vous répétez.

Gardez à l’esprit que les tests écrits ne sont pas susceptibles de changer alors que le code qu’ils testent changera presque définitivement.

En pratique, il y a aussi le problème des entrées-sorties et du monde extérieur. Vous voulez donc utiliser des interfaces pour pouvoir créer des simulacres si nécessaire.

Dans les langages plus dynamics, ce n’est pas vraiment un problème, ici vous pouvez utiliser le typage de canard, l’inheritance multiple et les mixins pour composer des cas de test. Si vous n’aimez pas l’inheritance en général, vous le faites probablement bien.

Ce screencast explique le développement agile et TDD dans la pratique pour c #.

En codant sur une interface, cela signifie que dans votre test, vous pouvez utiliser un object fictif au lieu de l’object réel. En utilisant un bon framework, vous pouvez faire ce que vous voulez avec votre object.