Wyśmiewanie wprowadza obsługę kodu produkcyjnego

15

Zakładając interfejs IReader, implementację interfejsu IReader ReaderImplementation oraz klasę ReaderConsumer, która zużywa i przetwarza dane z czytnika.

public interface IReader
{
     object Read()
}

Realizacja

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

Konsument:

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

Do testowania ReaderConsumer i przetwarzania używam makiety IReadera. Czytnik ReaderConsumer staje się:

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

W tym rozwiązaniu kpina wprowadza zdanie produkcyjne do kodu produkcyjnego, ponieważ tylko konstrukt kpiąca dostarcza instancje interfejsu.

Pisząc to, zdaję sobie sprawę, że blok try-last jest w pewnym stopniu niezwiązany, ponieważ ma za zadanie obsłużyć użytkownika zmieniającego lokalizację podczas działania aplikacji.

Ogólnie wydaje się śmierdzący, jak można sobie z tym poradzić lepiej?

Kristian Mo
źródło
16
Zazwyczaj nie stanowi to problemu, ponieważ konstruktor z wprowadzoną zależnością byłby jedynym konstruktorem. Czy nie można być ReaderConsumerniezależnym ReaderImplementation?
Chris Wohlert
Obecnie trudno byłoby usunąć zależność. Patrząc na to trochę bardziej, mam głębszy problem niż tylko zależność od ReaderImplemenatation. Ponieważ ReaderConsumer jest tworzony podczas uruchamiania z fabryki, trwa przez cały okres użytkowania aplikacji i akceptuje zmiany od użytkowników, wymaga to dodatkowego masowania. Prawdopodobnie konfiguracja / dane wejściowe użytkownika mogą istnieć jako obiekt, a następnie ReaderConsumer i ReaderImplementation mogą być tworzone w locie. Obie podane odpowiedzi całkiem dobrze rozwiązują bardziej ogólny przypadek.
kristian mo
3
Tak . O to chodzi w TDD: konieczność napisania testów najpierw implikuje bardziej oddzielony projekt (w przeciwnym razie nie będziesz w stanie pisać testów jednostkowych ...). Pomaga to uczynić kod bardziej łatwym w utrzymaniu i rozszerzalnym.
Bakuriu
Dobrym sposobem na wykrycie zapachów, które można rozwiązać za pomocą Dependency Injection, jest szukanie słowa kluczowego „new”. Nie zmieniaj swoich zależności. Zamiast tego wstrzyknij je.
Eternal21

Odpowiedzi:

67

Zamiast inicjować czytnik z twojej metody, przenieś tę linię

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

Do domyślnego konstruktora bez parametrów.

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

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

Nie ma czegoś takiego jak „próbny konstruktor”, jeśli twoja klasa ma zależność, której wymaga do działania, to konstruktor powinien albo dostarczyć tę rzecz, albo ją stworzyć.

Gumowa kaczuszka
źródło
3
score ++ ... poza tym z punktu widzenia DI, ten domyślny konstruktor jest zdecydowanie zapachem kodu.
Mathieu Guindon
3
@ Mat'sMug nic złego z domyślną implementacją. Brak łańcuchów ctor jest tutaj zapachem. =;) -
RubberDuck
Och, tak, jest - jeśli nie masz książki, ten artykuł wydaje się dość dobrze opisać anty-wzór iniekcji drania (choć po prostu przejrzałem).
Mathieu Guindon
3
@ Mat'sMug: interpretujesz DI dogmatycznie. Jeśli nie musisz wstrzykiwać domyślnego konstruktora, nie musisz.
Robert Harvey
3
Link @ Mat'sMug również mówi o „dopuszczalnych wartościach domyślnych” dla zależności, które są w porządku (chociaż odnosi się to do zastrzyku własności). Ujmijmy to w ten sposób: podejście oparte na dwóch konstruktorach kosztuje więcej pod względem prostoty i łatwości konserwacji niż podejście oparte na jednym konstruktorze, a ponieważ pytanie jest nadmiernie uproszczone dla przejrzystości, koszt nie jest uzasadniony, ale w niektórych przypadkach może być .
Carl Leth
54

Potrzebujesz tylko jednego konstruktora:

public class ReaderConsumer()
{
    private IReader reader = null

    public string location

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

w kodzie produkcyjnym:

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

w teście:

var rc = new ReaderConsumer(new MockImplementation(0));
Ewan
źródło
13

Spójrz na wstrzykiwanie zależności i odwracanie kontroli

Zarówno Ewan, jak i RubberDuck mają doskonałe odpowiedzi. Chciałem jednak wspomnieć o innym obszarze do zbadania, w którym jest wstrzykiwanie zależności (DI) i odwracanie kontroli (IoC). Oba te podejścia przenoszą napotkany problem na platformę / bibliotekę, abyś nie musiał się tym martwić.

Twój przykład jest prosty i można go szybko pominąć, ale nieuchronnie zamierzasz na nim budować i skończysz z mnóstwem konstruktorów lub procedurami inicjalizacji, które wyglądają jak:

var foo = nowy Foo (nowy pasek (nowy Baz (), nowy Quz ()), nowy Foo2 ());

Z DI / IoC korzystasz z biblioteki, która pozwala ci przeliterować zasady dopasowywania interfejsów do implementacji, a następnie po prostu mówisz „Daj mi Foo” i to wyjaśnia, jak to wszystko połączyć.

Istnieje wiele bardzo przyjaznych kontenerów IoC (jak się je nazywa) i zamierzam polecić jeden z nich, ale spójrz, ponieważ jest bardzo wiele dobrych opcji.

Prosty na początek to:

http://www.ninject.org/

Oto lista do odkrycia:

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

Reginald Blue
źródło
7
Zarówno odpowiedzi Ewana, jak i RubberDuck pokazują DI. DI / IoC nie polega na narzędziu ani frameworku, chodzi o architekturę i strukturę kodu w taki sposób, aby odwrócić kontrolę i wstrzyknąć zależności. Książka Mark Seemann wykonuje świetną (niesamowitą!) Pracę, wyjaśniając, w jaki sposób DI / IoC jest całkowicie osiągalny bez frameworka IoC oraz dlaczego i kiedy frameworki IoC stają się dobrym pomysłem (wskazówka: kiedy masz zależności zależności zależności… .) =)
Mathieu Guindon
Także ... +1, ponieważ Ninject jest niesamowity, a po ponownym przeczytaniu twoja odpowiedź wydaje się w porządku. Pierwszy akapit brzmi trochę tak, jakby odpowiedzi Ewana i RubberDuck nie dotyczyły DI, co skłoniło mnie do pierwszego komentarza.
Mathieu Guindon
1
@ Mat'sMug Zdecydowanie zdobądź punkt. Próbowałem zachęcić OP, by zajrzał do DI / IoC, których używali inni plakaty (Ewan to DI biedaka), ale nie wspomniałem. Zaletą korzystania z frameworka jest oczywiście zanurzenie użytkownika w świecie DI z mnóstwem konkretnych przykładów, które, miejmy nadzieję, pomogą mu zrozumieć, co robią ... chociaż ... nie zawsze. :-) I oczywiście uwielbiałem książkę Marka Seemanna. To jeden z moich ulubionych.
Reginald Blue
Dziękuję za wskazówkę dotyczącą frameworka DI, wydaje się, że jest to sposób na poprawne poprawienie tego bałaganu :)
kristian mo