Насмешка вводит обработку в производственный код

15

Предполагая интерфейс IReader, реализацию интерфейса IReader ReaderImplementation и класс ReaderConsumer, который потребляет и обрабатывает данные из считывателя.

public interface IReader
{
     object Read()
}

Реализация

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

Потребитель:

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

Для тестирования ReaderConsumer и обработки я использую макет IReader. Так 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
        }
    }
}

В этом решении mocking вводит предложение if для производственного кода, поскольку только конструктор mocking предоставляет экземпляры интерфейса.

Во время написания этого я понимаю, что блок try-finally несколько не связан, так как он предназначен для того, чтобы обрабатывать изменение пользователем местоположения во время выполнения приложения.

В целом он чувствует себя вонючим, как с ним можно справиться лучше?

Кристиан Мо
источник
16
Как правило, это не проблема, потому что конструктор с внедренной зависимостью будет единственным конструктором. Можно ли говорить о ReaderConsumerнезависимости ReaderImplementation?
Крис Волерт
В настоящее время было бы трудно удалить зависимость. Глядя на это немного больше, я имею более глубокую проблему, чем просто зависимость от ReaderImplemenatation. Поскольку ReaderConsumer создается во время запуска с фабрики, сохраняется в течение всего жизненного цикла приложения и принимает изменения от пользователей, что требует некоторого дополнительного массажа. Вероятно, конфигурация / пользовательский ввод могут существовать в виде объекта, и тогда ReaderConsumer и ReaderImplementation могут быть созданы на лету. Оба приведенных ответа довольно хорошо решают более общий случай.
Кристиан Мо
3
Да . В этом и заключается смысл TDD: необходимость сначала писать тесты подразумевает более разрозненный дизайн (иначе вы не сможете писать модульные тесты ...). Это помогает сделать код более понятным и расширяемым.
Бакуриу
Хороший способ обнаружить запахи, которые можно устранить с помощью Dependency Injection, - это поиск по ключевому слову «new». Не обновляйте свои зависимости. Вместо этого введите их.
Eternal21

Ответы:

67

Вместо инициализации читателя из вашего метода, переместите эту строку

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

В конструктор по умолчанию без параметров.

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

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

Не существует такого понятия, как «фиктивный конструктор», если у вашего класса есть зависимость, которая требуется ему для работы, тогда конструктору следует либо предоставить эту вещь, либо создать ее.

Резиновая утка
источник
3
оценка ++ ... кроме как с точки зрения DI, этот конструктор по умолчанию определенно является запахом кода.
Матье
3
@ Mat'sMug ничего плохого в реализации по умолчанию. Отсутствие цепочки ctor - вот запах. =;) -
RubberDuck
О, да, есть - если у вас нет книги, эта статья, похоже, довольно хорошо описывает анти-паттерн впрыскивания ублюдка (хотя и пролистал).
Матье
3
@ Mat'sMug: Вы интерпретируете DI догматично. Если вам не нужно вводить конструктор по умолчанию, то вам не нужно.
Роберт Харви
3
Связанная книга @ Mat'sMug также говорит о том, что «приемлемые значения по умолчанию» для зависимостей просто хороши (хотя это относится к внедрению свойств). Скажем так: подход с двумя конструкторами стоит дороже с точки зрения простоты и ремонтопригодности, чем подход с одним конструктором, и с вопросом, который слишком упрощен для ясности, стоимость не оправдана, но она может быть в некоторых случаях ,
Карл Лет
54

Вам нужен только один конструктор:

public class ReaderConsumer()
{
    private IReader reader = null

    public string location

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

в вашем производственном коде:

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

в вашем тесте:

var rc = new ReaderConsumer(new MockImplementation(0));
Ewan
источник
13

Посмотрите на внедрение зависимостей и инверсию контроля

И Ewan, и RubberDuck имеют отличные ответы. Но я хотел бы упомянуть еще одну область, в которую нужно обратить внимание, это внедрение зависимостей (DI) и инверсия контроля (IoC). Оба этих подхода переносят проблему, с которой вы сталкиваетесь, в фреймворк / библиотеку, так что вам не нужно беспокоиться об этом.

Ваш пример прост и быстро обходится без него, но вы неизбежно будете опираться на него, и в итоге вы получите либо тонны конструкторов, либо процедуры инициализации, которые выглядят так:

var foo = new Foo (new Bar (новый Baz (), новый Quz ()), новый Foo2 ());

С DI / IoC вы используете библиотеку, которая позволяет вам прописать правила для сопоставления интерфейсов с реализациями, а затем вы просто говорите «Дайте мне одурачивание», и это работает, как соединить все это.

Есть много очень дружественных IoC-контейнеров (как их называют), и я собираюсь порекомендовать один из них, но, пожалуйста, изучите, так как есть очень много хороших вариантов.

Простой для начала это:

http://www.ninject.org/

Вот список для изучения:

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

Реджинальд Блю
источник
7
Ответы как Юана, так и RubberDuck демонстрируют DI. DI / IoC - это не инструмент или инфраструктура, а архитектура и структурирование кода таким образом, что управление инвертируется и вводятся зависимости. Книга Марка Сибанна (Mark Seemann) проделывает большую (удивительную!) Работу по объяснению того, как DI / IoC полностью достижим без инфраструктуры IoC, и почему и когда платформа IoC становится хорошей идеей (подсказка: когда у вас есть зависимости зависимостей от зависимостей .. .) =)
Матье
Также ... +1, потому что Ninject потрясающий, и после перечитывания ваш ответ кажется нормальным. Первый абзац читается немного как ответы Юана и RubberDuck, но не о DI, что и послужило причиной моего первого комментария.
Матье
1
@ Mat'sMug Определенно получите свою точку зрения. Я пытался убедить ФП изучить DI / IoC, который фактически использовались другими авторами (Ewan's - DI Бедного Человека), но не упомянул. Конечно, преимущество использования фреймворка состоит в том, что он погрузит пользователя в мир DI множеством конкретных примеров, которые, надеюсь, помогут им понять, что они делают ... хотя ... не всегда. :-) И, конечно же, мне понравилась книга Марка Симанна. Это один из моих любимых.
Реджинальд Блю
Спасибо за совет по поводу DI-фреймворка, похоже, это способ правильно
Кристиан Мо