Збалансування введення залежності із загальнодоступним дизайном API


13

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

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

На даний момент це про найменше зло, про яке я можу придумати. Думки? Мудрість?

Відповіді:


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's конструктор все ще приймає всі його залежності і може бути використаний для глибокої настройки.


+1, SRP. Відповідальність стоматологічного кабінету не полягає у створенні стоматологічного кабінету. Відповідальність сервера не полягає у створенні сервера.
Р. Шмітц

2

Як щодо надання підкласу, який надає "громадський api"

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

Користувач може new StandardServer()і бути на шляху. Вони також можуть використовувати базовий клас сервера, якщо хочуть більшого контролю над тим, як функціонує сервер. Це більш-менш підхід, який я використовую. (Я не використовую рамки, оскільки я ще не бачив сенсу.)

Це все ще розкриває внутрішню апі, але я думаю, що слід. Ви розділили свої об'єкти на різні корисні компоненти, які повинні працювати незалежно. Немає відомостей, коли третя сторона може захотіти використовувати один із компонентів ізольовано.


1

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

Мені це не здається "неприємною прихованою пасткою". Громадський конструктор повинен просто називати внутрішній залежністю "за замовчуванням". Поки зрозуміло, що громадський конструктор не повинен змінюватися, все має бути добре.


0

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

Я б запропонував використовувати шаблон InjectableFactory / InjectableInstance . InjectableInstance - це простий клас корисних програм для багаторазового використання, який є власником змінного значення . Інтерфейс компонента містить посилання на цей власник значення, який ініціалізований з реалізацією за замовчуванням. Інтерфейс забезпечить однотонний метод get (), який делегує власнику значення. Клієнт-компонент викличе метод get () замість нового, щоб отримати екземпляр реалізації, щоб уникнути прямої залежності. Під час тестування ми можемо замінити реалізацію за замовчуванням макетом. Слідом я використаю один приклад 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. Для детальної інформації, будь ласка, зверніться до моєї публікації в блозі Непрямий залежність від фабрики інжекцій


Отже ... ви замінюєте ін'єкцію залежності залежною зміною однотонної? Це звучить жахливо, як би ви провели незалежні тести? Ви відкриваєте новий процес для кожного тесту чи змушуєте їх до певної послідовності? Ява не є моїм сильним костюмом, я щось неправильно зрозумів?
nvoigt

У проекті Java maven спільний екземпляр не є проблемою, оскільки двигун junit за замовчуванням буде виконувати кожен тест у іншому завантажувачі класів. Для вирішення цієї проблеми клас забезпечує метод скидання для виклику для скидання до початкового стану після кожного тесту. На практиці рідкісна ситуація, коли різні тести хочуть використовувати різні макети. Ми застосовуємо цю схему для масштабних проектів протягом багатьох років. Все просто працює гладко, ми насолоджувались читабельним, чистим кодом без жертвоприносимості та перевірки.
Jianwu Chen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.