82 votes

Qu'est-ce que les programmeurs veulent dire quand ils disent : "Codez contre une interface, pas contre un objet" ?

J'ai commencé la très longue et ardue quête pour apprendre et aplicar TDD à mon flux de travail. J'ai l'impression que le TDD s'accorde très bien avec les principes IoC.

Après avoir parcouru certaines des questions liées au TDD ici dans SO, j'ai lu que c'est une bonne idée de programmer par rapport aux interfaces, et non aux objets.

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

Merci beaucoup.

86voto

Billy ONeal Points 50631

Pensez-y :

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

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

Parce que MyMethod n'accepte qu'un MyClass si vous voulez remplacer MyClass avec un objet fantaisie afin de faire des tests unitaires, vous ne pouvez pas. Le mieux est d'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, et non une implémentation concrète particulière. Vous pouvez prendre cette interface et en hériter pour créer n'importe quel type de simulateur ou de faux que vous voulez, ou vous pouvez utiliser n'importe laquelle des bibliothèques de simulateurs préfabriquées, telles que Rhino.Mocks.MockRepository.StrictMock<T>() qui peut prendre l'interface et vous construire un objet fantaisie à la volée.

19voto

hoserdude Points 629

C'est une question d'intimité. Si vous codez vers 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 quelles sont les dépendances qu'il a, é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/devriez utiliser une fabrique pour construire des instances de l'objet. Les conteneurs IOC le font très bien pour vous, ou vous pouvez créer le vôtre. 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 de la fabrique, vous pouvez construire soit des instances réelles, soit des instances factices de la classe. En production, vous utiliserez bien sûr des instances réelles, mais pour les tests, vous pouvez créer des instances simulées ou dynamiquement simulées pour tester différents états du système sans avoir à exécuter le système.
  2. Il n'est pas nécessaire de savoir où se trouve l'objet. Ceci est utile dans les systèmes distribués où l'objet auquel vous voulez parler peut ou non être local à votre processus ou même à votre système. Si vous avez déjà programmé Java RMI ou EJB old skool, vous connaissez la routine de "parler à l'interface" qui cachait un proxy qui a fait le réseau distant et les devoirs de marshalling que votre client n'a pas dû se soucier. WCF a une philosophie similaire de "parler à l'interface" et laisser le système déterminer comment communiquer avec l'objet/service cible.

** MISE À JOUR ** Il y a eu une demande pour un exemple d'un conteneur IOC (Factory). Il en existe de nombreux pour pratiquement toutes les plates-formes, mais ils fonctionnent essentiellement de la manière suivante :

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

  2. Vous "enregistrez" les implémentations que vous souhaitez que le conteneur crée pour vous en tant que fabrique pour les interfaces qu'elles mettent en œuvre (par exemple, enregistrer MyServiceImpl pour l'interface Service). Au cours de ce processus d'enregistrement, il existe généralement une politique comportementale que vous pouvez définir, par exemple si une nouvelle instance est créée à chaque fois ou si une seule instance 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 que si votre objet dépend d'une autre interface, une implémentation de cette interface est fournie à son tour, et ainsi de suite).

De manière pseudo-codifiée, 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();

9voto

Michael Shimmins Points 12740

Lorsque vous programmez contre une interface, vous écrivez 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 de contrôle ne sont pas nécessaires pour pouvoir programmer contre les interfaces, cependant, puisque vous venez de la perspective TDD et IoC, je l'ai mis en place de cette façon pour vous donner un contexte avec lequel vous êtes, je l'espère, familier.

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 référentiel est transmis et est un type d'interface. L'avantage de passer dans une interface est la possibilité de "changer" l'implémentation concrète sans changer 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 base de données. Pendant le temps de test, vous pouvez passer dans un référentiel fantaisie ou stub pour exercer votre PeopleOverEighteen méthode.

3voto

Lorenzo Points 12167

Ça veut dire penser générique. Pas spécifique.

Supposons que vous ayez une application qui notifie l'utilisateur en lui envoyant 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 notifié par email et donc votre IoC créera une classe concrète EmailMessage. Un autre veut un 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.

2voto

Andrew Kennan Points 8221

Le grand avantage de la programmation contre les interfaces lors des tests unitaires est qu'elle permet d'isoler un morceau de code de toutes les dépendances que vous voulez tester séparément ou simuler pendant les tests.

Un exemple que j'ai déjà mentionné quelque part ici 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 devriez fournir une implémentation qui lit le fichier de configuration, mais pour les tests, vous pouvez en utiliser une qui renvoie simplement des valeurs de test ou qui lève des exceptions ou autre.

Considérez également votre couche d'accès aux données. Si votre logique métier est étroitement liée à une implémentation particulière de l'accès aux données, il est difficile de la tester sans disposer d'une base de données complète contenant les données dont vous avez besoin. Si votre accès aux données est caché derrière 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 fins qui testent réellement les unités individuelles de votre code.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X