La moquerie introduit la manipulation dans le code de production

15

En supposant une interface IReader, une implémentation de l'interface IReader ReaderImplementation et une classe ReaderConsumer qui consomme et traite les données du lecteur.

public interface IReader
{
     object Read()
}

la mise en oeuvre

public class ReaderImplementation
{
    ...
    public object Read()
    {
        ...
    }
}

Consommateur:

public class ReaderConsumer()
{
    public string location

    // constructor
    public ReaderConsumer()
    {
        ...
    }

    // read some data
    public object ReadData()
    {
        IReader reader = new ReaderImplementation(this.location)
        data = reader.Read()
        ...
        return processedData    
    }
}

Pour tester ReaderConsumer et le traitement, j'utilise une maquette d'IReader. ReaderConsumer devient donc:

public class ReaderConsumer()
{
    private IReader reader = null

    public string location

    // constructor
    public ReaderConsumer()
    {
        ...
    }

    // mock constructor
    public ReaderConsumer(IReader reader)
    {
        this.reader = reader
    }

    // read some data
    public object ReadData()
    {
        try
        {
            if(this.reader == null)
            {
                 this.reader = new ReaderImplementation(this.location)
            }

            data = reader.Read()
            ...
            return processedData    
        }
        finally
        {
            this.reader = null
        }
    }
}

Dans cette solution, le mocking introduit une phrase if pour le code de production car seul le constructeur mocking fournit des instances de l'interface.

Lors de l'écriture, je me rends compte que le bloc try-finally est quelque peu indépendant car il est là pour gérer l'utilisateur qui change l'emplacement pendant l'exécution de l'application.

Dans l'ensemble, il sent mal, comment pourrait-il être mieux géré?

kristian mo
la source
16
En règle générale, ce n'est pas un problème car le constructeur avec la dépendance injectée, serait le seul constructeur. Est-il hors de question de rendre ReaderConsumerindépendant ReaderImplementation?
Chris Wohlert
Actuellement, il serait difficile de supprimer la dépendance. En y regardant un peu plus, j'ai un problème plus profond que la simple dépendance à ReaderImplemenatation. Étant donné que ReaderConsumer est créé lors du démarrage à partir d'une usine, persiste pendant toute la durée de vie de l'application et accepte les modifications des utilisateurs. Cela nécessite un massage supplémentaire. L'entrée de configuration / utilisateur pourrait probablement exister en tant qu'objet, puis ReaderConsumer et ReaderImplementation pourraient être créés à la place. Les deux réponses données résolvent assez bien le cas plus générique.
kristian mo
3
Oui . C'est le sens du TDD: devoir d'abord écrire des tests implique une conception plus découplée (sinon vous ne pourrez pas écrire de tests unitaires ...). Cela permet de rendre le code plus maintenable et également extensible.
Bakuriu
Un bon moyen de détecter les odeurs qui peuvent être résolues avec l'injection de dépendance consiste à rechercher le mot-clé «nouveau». Ne renouvelez pas vos dépendances. Injectez-les à la place.
Eternal21

Réponses:

67

Au lieu d'initialiser le lecteur à partir de votre méthode, déplacez cette ligne

{
    this.reader = new ReaderImplementation(this.location)
}

Dans le constructeur sans paramètre par défaut.

public ReaderConsumer()
{
    this.reader = new ReaderImplementation(this.location)
}

public ReaderConsumer(IReader reader)
{
    this.reader = reader
}

Il n'y a pas de "constructeur factice", si votre classe a une dépendance dont elle a besoin pour fonctionner, alors le constructeur devrait soit lui fournir cette chose, soit la créer.

Canard en caoutchouc
la source
3
score ++ ... sauf d'un point de vue DI, ce constructeur par défaut est définitivement une odeur de code.
Mathieu Guindon
3
@ Mat'sMug rien de mal avec une implémentation par défaut. L'absence de chaînage ctor est l'odeur ici. =;) -
RubberDuck
Oh, oui, il y a - si vous n'avez pas le livre, cet article semble décrire assez bien l'anti-modèle d' injection bâtard (juste survolé cependant).
Mathieu Guindon
3
@ Mat'sMug: Vous interprétez DI de manière dogmatique. Si vous n'avez pas besoin d'injecter le constructeur par défaut, ce n'est pas le cas.
Robert Harvey
3
Le livre lié à Mat'sMug parle également de "valeurs par défaut acceptables" pour les dépendances qui sont très bien (bien qu'il fasse référence à l'injection de propriété). Disons-le de cette façon: l'approche à deux constructeurs coûte plus en termes de simplicité et de maintenabilité que l'approche à un constructeur, et avec la question étant trop simplifiée pour plus de clarté, le coût n'est pas justifié, mais il peut l'être dans certains cas .
Carl Leth
54

Vous n'avez besoin que d'un seul constructeur:

public class ReaderConsumer()
{
    private IReader reader = null

    public string location

    // constructor
    public ReaderConsumer(IReader reader)
    {
        this.reader = reader;
    }

dans votre code de production:

var rc = new ReaderConsumer(new ReaderImplementation(0));

dans votre test:

var rc = new ReaderConsumer(new MockImplementation(0));
Ewan
la source
13

Examiner l'injection de dépendance et l'inversion du contrôle

Ewan et RubberDuck ont ​​tous deux d'excellentes réponses. Mais je voulais mentionner un autre domaine à examiner, à savoir l'injection de dépendance (DI) et l'inversion de contrôle (IoC). Ces deux approches déplacent le problème que vous rencontrez dans un framework / bibliothèque afin que vous n'ayez pas à vous en soucier.

Votre exemple est simple et est rapidement supprimé, mais, inévitablement, vous allez vous en inspirer et vous vous retrouverez avec des tonnes de constructeurs ou des routines d'initialisation qui ressemblent à:

var foo = new Foo (new Bar (new Baz (), new Quz ()), new Foo2 ());

Avec DI / IoC, vous utilisez une bibliothèque qui vous permet d'énoncer les règles pour faire correspondre les interfaces aux implémentations, puis vous dites simplement "Donnez-moi un Foo" et cela fonctionne comment câbler le tout.

Il y a beaucoup de conteneurs IoC très sympathiques (comme on les appelle), et je vais recommander un à regarder, mais, veuillez explorer, car il y a beaucoup de bons choix.

Un simple pour commencer est:

http://www.ninject.org/

Voici une liste à explorer:

http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx

Bleu Reginald
la source
7
Les réponses d'Ewan et de RubberDuck démontrent DI. DI / IoC ne concerne pas un outil ou un framework, il s'agit d'architecturer et de structurer le code de manière à inverser le contrôle et à injecter des dépendances. Le livre de Mark Seemann fait un excellent travail (génial!) Pour expliquer comment DI / IoC est complètement réalisable sans un cadre IoC, et pourquoi et quand un cadre IoC devient une bonne idée (indice: lorsque vous avez des dépendances de dépendances de dépendances de .. .) =)
Mathieu Guindon
Aussi ... +1 parce que Ninject est génial, et après relecture, votre réponse semble correcte. Le premier paragraphe se lit un peu comme si les réponses d'Ewan et de RubberDuck ne concernaient pas DI, ce qui a incité mon premier commentaire.
Mathieu Guindon
1
@ Mat'sMug Obtenez certainement votre point. J'essayais d'encourager le PO à se pencher sur DI / IoC que les autres affiches utilisaient en fait (Ewan's est Poor Man's DI), mais je n'ai pas mentionné. L'avantage, bien sûr, d'utiliser un framework est qu'il plongera l'utilisateur dans le monde de la DI avec de nombreux exemples concrets qui, espérons-le, les guideront pour comprendre ce qu'ils font ... mais ... pas toujours. :-) Et, bien sûr, j'ai adoré le livre de Mark Seemann. C'est l'un de mes favoris.
Reginald Blue
Merci pour l'astuce sur un framework DI, cela semble être le moyen de refactoriser correctement ce gâchis :)
kristian mo