Балансировка внедрения зависимостей с публичным дизайном API

13

Я размышлял, как сбалансировать тестируемый дизайн, используя внедрение зависимостей, с предоставлением простого фиксированного общедоступного API. Моя дилемма заключается в следующем: люди хотели бы сделать что-то подобное var server = new Server(){ ... }и не должны беспокоиться о создании множества зависимостей и графа зависимостей, которые Server(,,,,,,)могут иметь. При разработке я не слишком беспокоюсь, поскольку для этого я использую инфраструктуру IoC / DI (я не использую аспекты управления жизненным циклом любого контейнера, что еще более усложнит ситуацию).

Теперь зависимости вряд ли будут повторно реализованы. Компонентация в этом случае почти исключительно для тестируемости (и достойного дизайна!), А не для создания швов для расширения и т. Д. Люди будут 99,999% времени желать использовать конфигурацию по умолчанию. Так. Я мог бы жестко закодировать зависимости. Не хочу этого делать, мы теряем наше тестирование! Я мог бы предоставить конструктор по умолчанию с жестко запрограммированными зависимостями и конструктор, который принимает зависимости. Это ... грязно и, вероятно, сбивает с толку, но жизнеспособно. Я мог бы сделать конструктор получения зависимостей внутренним и сделать мои модульные тесты дружественной сборкой (предполагая, что C #), что приводит в порядок публичный API, но оставляет неприятную скрытую ловушку для обслуживания. Наличие в моей книге двух конструкторов, которые неявно связаны, а не явно, были бы плохим дизайном вообще.

На данный момент это самое меньшее зло, о котором я могу думать. Мнения? Мудрость?

Kolektiv
источник

Ответы:

11

Внедрение зависимостей является мощной моделью при правильном использовании, но слишком часто ее практики становятся зависимыми от какой-то внешней структуры. Получившийся API довольно болезнен для тех из нас, кто не хочет свободно связывать наше приложение с помощью клейкой ленты XML. Не забывайте про простые старые объекты (POO).

Сначала подумайте, как вы ожидаете, что кто-то будет использовать ваш API - без участия фреймворка.

  • Как вы создаете экземпляр Сервера?
  • Как вы расширяете сервер?
  • Что вы хотите выставить во внешнем API?
  • Что вы хотите скрыть во внешнем API?

Мне нравится идея предоставления реализаций по умолчанию для ваших компонентов и тот факт, что у вас есть эти компоненты. Следующий подход является вполне допустимой и достойной практикой ООП:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Пока вы документируете, какие реализации по умолчанию для компонентов находятся в документации API, и предоставляете один конструктор, который выполняет всю настройку, у вас все будет в порядке. Каскадные конструкторы не являются хрупкими, если код установки происходит в одном главном конструкторе.

По сути, все общедоступные конструкторы будут иметь различные комбинации компонентов, которые вы хотите представить. Внутри они заполняют значения по умолчанию и переходят к закрытому конструктору, который завершает настройку. По сути, это дает вам СУХОЙ и довольно легко проверить.

Если вам нужно предоставить friendдоступ к своим тестам, чтобы настроить объект Server для его изоляции для тестирования, сделайте это. Он не будет частью общедоступного API, с которым вам нужно быть осторожным.

Просто будьте добры к своим пользователям API и не требуйте инфраструктуры IoC / DI для использования вашей библиотеки. Чтобы почувствовать боль, которую вы причиняете, убедитесь, что ваши модульные тесты также не основаны на IoC / DI. (Это личная любимая мозоль, но как только вы представите фреймворк, он больше не будет модульным тестированием - он станет интеграционным тестированием).

Берин Лорич
источник
Я согласен, и я, конечно, не фанат супа конфигурации IoC - конфигурация XML не то, что я использую вообще, когда дело касается IoC. Безусловно, требование использования IoC - это именно то, чего я избегал - такое предположение, которое никогда не может сделать дизайнер общедоступного API. Спасибо за комментарий, это то, о чем я думал, и обнадеживает время от времени проверять работоспособность!
Колектив
-1 Этот ответ в основном говорит «не использовать внедрение зависимостей» и содержит неточные предположения. В вашем примере, если HttpListenerконструктор изменится, Serverкласс также должен измениться. Они связаны. Лучшее решение - это то, что @Winston Ewert говорит ниже, - предоставить класс по умолчанию с заранее заданными зависимостями вне API библиотеки. Тогда программист не обязан устанавливать все зависимости, поскольку вы принимаете решение за него, но у него все еще есть возможность изменить их позже. Это большая проблема, когда у вас есть сторонние зависимости.
М. Дадли
FWIW предоставленный пример называется «инъекция ублюдка». stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
М. Дадли
1
@MichaelDudley, вы читали вопрос ОП? Все «предположения» основывались на предоставленной ФП информации. Какое-то время «инъекция ублюдков», как вы это называли, была предпочтительным форматом внедрения зависимостей - особенно для систем, составы которых не меняются во время выполнения. Сеттеры и геттеры также являются другой формой внедрения зависимостей. Я просто использовал примеры, основанные на том, что предоставил ОП.
Берин Лорич
4

В таких случаях в Java обычно используется конфигурация «по умолчанию», созданная фабрикой, независимо от фактического «правильно» работающего сервера. Итак, ваш пользователь пишет:

var server = DefaultServer.create();

в то время Serverкак конструктор по-прежнему принимает все его зависимости и может использоваться для глубокой настройки.

fdreger
источник
+1, ПСП. Ответственность стоматологического кабинета заключается не в том, чтобы построить стоматологический кабинет. Ответственность сервера не в том, чтобы построить сервер.
Р. Шмитц
2

Как насчет предоставления подкласса, который обеспечивает "публичный API"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

Пользователь может new StandardServer()и быть в пути. Они также могут использовать базовый класс Сервера, если они хотят больше контролировать работу сервера. Это более или менее подход, который я использую. (Я не использую фреймворк, так как еще не видел смысла.)

Это все еще выставляет внутренний API, но я думаю, что вы должны. Вы разделили свои объекты на различные полезные компоненты, которые должны работать независимо. Пока неизвестно, когда третья сторона захочет использовать один из компонентов в изоляции.

Уинстон Эверт
источник
1

Я мог бы сделать конструктор получения зависимостей внутренним и сделать мои модульные тесты дружественной сборкой (предполагая, что C #), что приводит в порядок публичный API, но оставляет неприятную скрытую ловушку для обслуживания.

Это не похоже на "противную скрытую ловушку" для меня. Открытый конструктор должен просто вызывать внутренний конструктор с зависимостями «по умолчанию». Пока ясно, что публичный конструктор не должен быть изменен, все должно быть хорошо.

Anon.
источник
0

Я полностью согласен с вашим мнением. Мы не хотим загрязнять границы использования компонентов только с целью модульного тестирования. В этом вся проблема решения для тестирования на основе DI.

Я бы предложил использовать шаблон InjectableFactory / InjectableInstance . InjectableInstance - это простые многократно используемые служебные классы, которые являются изменяемыми держателями значений . Интерфейс компонента содержит ссылку на этот держатель значения, который инициализирован с реализацией по умолчанию. Интерфейс предоставит одноэлементный метод get (), который делегирует держателю значения. Клиент компонента будет вызывать метод get () вместо new, чтобы получить экземпляр реализации, чтобы избежать прямой зависимости. Во время тестирования мы можем при желании заменить реализацию по умолчанию на макет. Ниже я буду использовать один пример TimeProviderкласс, который является абстракцией Date (), поэтому он позволяет юнит-тестированию внедрять и имитировать разные моменты. Извините, я буду использовать Java здесь, так как я более знаком с Java. C # должен быть похожим.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance довольно легко реализовать, у меня есть эталонная реализация в Java. Для получения подробной информации, пожалуйста, обратитесь к моему сообщению в блоге Dependency Indirection с Injectable Factory

Цзяну Чен
источник
Итак ... вы заменяете внедрение зависимостей на изменяемый синглтон? Звучит ужасно, как бы вы провели независимые тесты? Открываете ли вы новый процесс для каждого теста или вводите их в определенную последовательность? Ява не моя сильная сторона, я что-то не так понял?
nvoigt
В проекте Java maven общий экземпляр не является проблемой, так как механизм junit по умолчанию будет запускать каждый тест в разных загрузчиках классов. Однако я сталкиваюсь с этой проблемой в некоторых IDE, поскольку он может повторно использовать один и тот же загрузчик классов. Чтобы решить эту проблему, класс предоставляет метод сброса, который вызывается для сброса в исходное состояние после каждого теста. На практике это редкая ситуация, когда разные тесты хотят использовать разные макеты. Мы использовали этот шаблон для крупномасштабных проектов в течение многих лет. Все прошло гладко, мы наслаждались читаемым, чистым кодом без ущерба для переносимости и тестируемости.
Цзяну Чен