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?


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ć.


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));

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


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.