web-dev-qa-db-fra.com

Que veulent dire les programmeurs quand ils disent "Code contre une interface, pas un objet"?

J'ai commencé la quête très longue et ardue pour apprendre et appliquer TDD à mon flux de travail. J'ai l'impression que TDD s'intègre très bien aux principes de l'IoC.

Après avoir parcouru certaines des questions marquées TDD ici dans SO, j'ai lu que c'est une bonne idée de programmer contre des interfaces, pas des objets.

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 pour les autres personnes désireuses d'apprendre) de saisir les concepts.

Merci beaucoup.

78
delete

Considérer:

class MyClass
{
    //Implementation
    public void Foo() {}
}

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

Étant donné que MyMethod accepte uniquement un MyClass, si vous souhaitez remplacer MyClass par un objet factice afin d'effectuer un test unitaire, 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();
    }
}

Vous pouvez maintenant tester MyMethod, car il utilise uniquement une interface, pas une implémentation concrète particulière. Ensuite, vous pouvez implémenter cette interface pour créer tout type de maquette ou de faux que vous souhaitez à des fins de test. Il existe même des bibliothèques comme Rhino.Mocks.MockRepository.StrictMock<T>() de Rhino Mocks, qui prennent n'importe quelle interface et vous construisent un objet factice à la volée.

82
Billy ONeal

C'est une question d'intimité. Si vous codez pour une implémentation (un objet réalisé), vous êtes dans une relation assez intime avec cet "autre" code, en tant que consommateur de celui-ci. Cela signifie que vous devez savoir comment le construire (c'est-à-dire ses dépendances, éventuellement en tant que paramètres de constructeur, éventuellement en tant que setters), quand en disposer, et vous ne pouvez probablement pas faire grand-chose sans lui.

Une interface devant l'objet réalisé vous permet de faire quelques choses -

  1. D'une part, vous pouvez/devez tirer parti d'une usine pour construire des instances de l'objet. IOC le font très bien pour vous, ou vous pouvez créer les vôtres. Avec des tâches de construction en dehors de votre responsabilité, votre code peut simplement supposer qu'il obtient ce dont il a besoin. De l'autre côté de le mur d'usine, vous pouvez soit construire des instances réelles, soit des instances fictives de la classe. En production, vous utiliserez bien sûr, mais pour les tests, vous voudrez peut-être créer des instances tronquées ou mockées dynamiquement pour tester divers états du système sans avoir à exécuter le système.
  2. Vous n'avez pas besoin de savoir où se trouve l'objet. Ceci est utile dans les systèmes distribués où l'objet auquel vous souhaitez parler peut ou non être local à votre processus ou même à votre système. Si vous avez déjà programmé Java RMI ou ancien EJB de skool, vous connaissez la routine de "parler à l'interface" qui cachait un proxy qui effectuait les tâches de mise en réseau et de triage à distance que votre client n'avait pas WCF a une philosophie similaire de "parler à l'interface" et de laisser le système déterminer comment communiquer avec l'objet/service cible.

** MISE À JOUR ** Il y avait une demande pour un exemple d'un conteneur IOC (Factory). Il y en a beaucoup pour à peu près toutes les plates-formes, mais à leur base, ils fonctionnent 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'ils implémentent (par exemple: enregistrez MyServiceImpl pour l'interface de service). Au cours de ce processus d'enregistrement, il existe généralement une stratégie comportementale que vous pouvez fournir, 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 objets pour vous, il injecte toutes les dépendances dans ces objets dans le cadre du processus de création (c'est-à-dire, si votre objet dépend d'une autre interface, une implémentation de cette interface est à son tour fournie et ainsi de suite).

Pseudo-codishly, cela 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<Service>();

//Blissfully unaware of the implementation, call the service method.
myService.DoGoodWork();
18
hoserdude

Lors de la programmation sur une interface, vous écrirez du code qui utilise une instance d'une interface, pas un type concret. Par exemple, vous pouvez utiliser le modèle suivant, qui incorpore l'injection de constructeur. L'injection de constructeur et d'autres parties de l'inversion de contrôle ne sont pas nécessaires pour pouvoir programmer contre des interfaces, mais puisque vous venez du point de vue TDD et IoC, je l'ai câblé de cette façon pour vous donner un contexte que vous espérez familier avec.

public class PersonService
{
    private readonly IPersonRepository repository;

    public PersonService(IPersonRepository repository)
    {
        this.repository = repository;
    }

    public IList<Person> PeopleOverEighteen
    {
        get
        {
            return (from e in repository.Entities where e.Age > 18 select e).ToList();
        }
    }
}

L'objet de 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 changer l'utilisation.

Par exemple, on supposerait qu'au moment de l'exécution, le conteneur IoC injectera un référentiel qui est câblé pour atteindre la base de données. Pendant le temps de test, vous pouvez passer dans un référentiel maquette ou stub pour exercer votre méthode PeopleOverEighteen.

9
Michael Shimmins

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 en utilisant 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 informé d'un e-mail et votre IoC créera donc une classe concrète EmailMessage. Un autre veut des SMS et vous créez une instance de SMSMessage.

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

3
Lorenzo

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 de toutes les dépendances que vous souhaitez tester séparément ou 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 qui vous permettent d'accéder aux valeurs de configuration. Normalement, vous fourniriez une implémentation qui lit à partir du fichier de configuration, mais pour les tests, vous pouvez en utiliser une qui renvoie simplement des valeurs de test ou lève des exceptions ou autre chose.

Considérez également votre couche d'accès aux données. Le fait d'avoir votre logique métier étroitement couplée à une implémentation d'accès aux données particulière rend difficile le test sans avoir une base de données complète à portée de main avec les données dont vous avez besoin. Si votre accès aux données est caché derrière les 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 fins qui testent réellement des unités individuelles de votre code.

2
Andrew Kennan

Testez votre code comme quelqu'un qui l'utiliserait après avoir lu la documentation. Ne testez rien sur la base 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 instructions, la modification de la mise en œuvre ne devrait pas poser de problème.

D'après mon expérience, il est également recommandé de tester chaque "couche" de votre application. Vous aurez des unités atomiques, qui en soi n'ont pas de dépendances et vous aurez des unités qui dépendent d'autres unités jusqu'à ce que vous arriviez finalement à l'application qui est en soi une unité.

Vous devez tester chaque couche, ne vous fiez pas au fait qu'en testant l'unité A, vous testez également l'unité B dont l'unité A dépend (la règle s'applique également à l'héritage.) Cela aussi doit être traité comme un détail d'implémentation, même mais vous pourriez vous sentir comme si vous vous répétiez.

Gardez à l'esprit qu'une fois les tests écrits, il est peu probable que le code qu'ils testent changera presque définitivement.

Dans la pratique, il y a aussi le problème de IO et du monde extérieur, donc vous voulez utiliser des interfaces pour pouvoir créer des mocks si nécessaire.

Dans les langages plus dynamiques, ce n'est pas vraiment un problème, ici vous pouvez utiliser le typage canard, l'héritage multiple et les mixins pour composer des cas de test. Si vous commencez à ne pas aimer l'héritage en général, vous le faites probablement bien.

2
DasIch

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

Le codage par rapport à une interface signifie que dans votre test, vous pouvez utiliser un objet factice au lieu de l'objet réel. En utilisant un bon cadre de simulation, vous pouvez faire dans votre objet de simulation tout ce que vous voulez.

1
BЈовић