Das Verspotten führt die Handhabung im Produktionscode ein

15

Angenommen, eine IReader-Schnittstelle, eine Implementierung der IReader-Schnittstelle ReaderImplementation und eine Klasse ReaderConsumer, die Daten vom Reader verwendet und verarbeitet.

public interface IReader
{
     object Read()
}

Implementierung

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

Verbraucher:

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    
    }
}

Zum Testen von ReaderConsumer und der Verarbeitung verwende ich einen IReader-Mock. So wird ReaderConsumer:

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
        }
    }
}

In dieser Lösung führt Mocking einen if-Satz für den Produktionscode ein, da nur der Mocking-Konstruktor Instanzen der Schnittstelle bereitstellt.

Während des Schreibens wird mir klar, dass der try-finally-Block in keiner Beziehung steht, da er dazu dient, den Benutzer beim Ändern des Speicherorts während der Laufzeit der Anwendung zu unterstützen.

Insgesamt riecht es, wie könnte es besser gehandhabt werden?

kristian mo
quelle
16
In der Regel ist dies kein Problem, da der Konstruktor mit der injizierten Abhängigkeit der einzige Konstruktor ist. Kommt es nicht in Frage, ReaderConsumerunabhängig zu machen ReaderImplementation?
Chris Wohlert
Derzeit ist es schwierig, die Abhängigkeit zu entfernen. Wenn ich es etwas genauer betrachte, habe ich ein tieferes Problem als nur die Abhängigkeit von ReaderImplemenatation. Da ReaderConsumer beim Start aus einer Fabrik erstellt wird, über die gesamte Lebensdauer der Anwendung bestehen bleibt und Änderungen von den Benutzern akzeptiert, ist eine zusätzliche Massage erforderlich. Möglicherweise ist die Konfiguration / Benutzereingabe als Objekt vorhanden, und ReaderConsumer und ReaderImplementation können stattdessen im laufenden Betrieb erstellt werden. Beide gegebenen Antworten lösen den allgemeineren Fall ziemlich gut.
Kristian Mo
3
Ja . Dies ist der Punkt , von TDD: mit zu Schreibtests zunächst impliziert ein entkoppelte Design (sonst sind Sie nicht in der Lage zu schreiben Einheit Tests ...). Dies hilft, den Code wartbarer und erweiterbarer zu machen.
Bakuriu
Eine gute Möglichkeit, Gerüche zu erkennen, die mit Dependency Injection behoben werden können, ist die Suche nach dem Schlüsselwort 'new'. Erneuern Sie nicht Ihre Abhängigkeiten. Spritzen Sie sie stattdessen.
Eternal21

Antworten:

67

Verschieben Sie diese Zeile, anstatt den Reader von Ihrer Methode aus zu initialisieren

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

In den Standardkonstruktor ohne Parameter.

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

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

Es gibt keinen "Scheinkonstruktor". Wenn Ihre Klasse über eine Abhängigkeit verfügt, die für die Arbeit erforderlich ist , sollte dem Konstruktor entweder dieses Objekt bereitgestellt oder es erstellt werden.

Quietscheentchen
quelle
3
score ++ ... abgesehen von einem DI-Standpunkt ist dieser Standardkonstruktor definitiv ein Codegeruch.
Mathieu Guindon
3
@ Mat'sMug nichts falsch mit einer Standardimplementierung. Die fehlende Verkettung ist der Geruch hier. =;) -
RubberDuck
Oh ja, wenn Sie das Buch nicht haben, scheint dieser Artikel das Anti-Muster der Bastard-Injektion ziemlich gut zu beschreiben (wird aber nur überflogen).
Mathieu Guindon
3
@ Mat'sMug: Du interpretierst DI dogmatisch. Wenn Sie den Standardkonstruktor nicht injizieren müssen, tun Sie dies nicht.
Robert Harvey
3
In dem verlinkten Buch @ Mat'sMug geht es auch um "akzeptable Standardeinstellungen", da Abhängigkeiten in Ordnung sind (obwohl es sich um die Eigenschaftsinjektion handelt). Sagen wir es so: Der Zwei-Konstruktor-Ansatz kostet mehr in Bezug auf Einfachheit und Wartbarkeit als der Ein-Konstruktor-Ansatz, und da die Frage aus Gründen der Klarheit zu stark vereinfacht ist, sind die Kosten nicht gerechtfertigt, aber in einigen Fällen .
Carl Leth
54

Sie benötigen nur den einzelnen Konstruktor:

public class ReaderConsumer()
{
    private IReader reader = null

    public string location

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

In Ihrem Produktionscode:

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

in deinem test:

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

Untersuchen Sie Abhängigkeitsinjektion und Inversion der Steuerung

Sowohl Ewan als auch RubberDuck haben hervorragende Antworten. Aber ich wollte noch einen anderen Bereich erwähnen, in dem es um Abhängigkeitsinjektion (DI) und Inversion of Control (IoC) geht. Bei beiden Ansätzen wird das aufgetretene Problem in ein Framework / eine Bibliothek verschoben, sodass Sie sich darüber keine Gedanken machen müssen.

Ihr Beispiel ist einfach und schnell erledigt, aber Sie werden unweigerlich darauf aufbauen, und Sie werden entweder Tonnen von Konstruktoren oder Initialisierungsroutinen haben, die so aussehen:

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

Mit DI / IoC verwenden Sie eine Bibliothek, mit der Sie die Regeln für das Zuordnen von Schnittstellen zu Implementierungen formulieren können. Dann sagen Sie einfach "Give me a Foo" und es wird herausgefunden, wie alles verkabelt wird.

Es gibt viele sehr freundliche IoC-Container (wie sie genannt werden), und ich werde einen empfehlen, den man sich ansehen kann, aber bitte erkundigen Sie sich, da es sehr viele gute Möglichkeiten gibt.

Ein einfacher Einstieg ist:

http://www.ninject.org/

Hier ist eine Liste zum Erkunden:

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

Reginald Blue
quelle
7
Sowohl Ewans als auch RubberDucks Antworten zeigen DI. Bei DI / IoC geht es nicht um ein Tool oder ein Framework, sondern darum, den Code so zu gestalten und zu strukturieren, dass die Steuerung invertiert und Abhängigkeiten eingefügt werden. Mark Seemanns Buch leistet einen großartigen (großartigen!) Job darin zu erklären, wie DI / IoC ohne ein IoC-Framework vollständig erreichbar ist und warum und wann ein IoC-Framework eine gute Idee wird (Hinweis: Wenn Abhängigkeiten von Abhängigkeiten von Abhängigkeiten von .. .) =)
Mathieu Guindon
Außerdem ... +1, weil Ninject fantastisch ist und beim erneuten Lesen Ihre Antwort in Ordnung erscheint. Der erste Absatz liest sich ein bisschen wie Ewans und RubberDucks Antworten handeln jedoch nicht von DI, was meinen ersten Kommentar veranlasste.
Mathieu Guindon
1
@ Mat'sMug Holen Sie sich auf jeden Fall Ihren Standpunkt. Ich habe versucht, das OP zu ermutigen, sich mit DI / IoC zu befassen, das die anderen Poster tatsächlich verwendeten (Ewan's is Poor Man's DI), habe es aber nicht erwähnt. Der Vorteil der Verwendung eines Frameworks besteht natürlich darin, dass der Benutzer mit vielen konkreten Beispielen in die Welt von DI eintauchen kann, die ihn hoffentlich dazu führen, zu verstehen, was er tut ... obwohl ... nicht immer. :-) Und natürlich habe ich Mark Seemanns Buch geliebt. Es ist einer meiner Favoriten.
Reginald Blue
Vielen Dank für den Tipp zu einem DI-Framework, es scheint der Weg zu sein, dieses Chaos richtig
umzugestalten