Програмування для подальшого використання інтерфейсів


42

У мене поруч сидить колега, який розробив такий інтерфейс:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

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

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

Тепер, моє запитання, чи є джерела, які займаються такою темою, як "шановані" гуру кодування, до яких ми можемо зв'язати його?


29
"Прогнозувати дуже важко, особливо щодо майбутнього".
Йорг W Міттаг

10
Частина проблеми полягає в тому, що це не найкращий інтерфейс для початку. Отримання Foos та їх фільтрація - це дві окремі проблеми. Цей інтерфейс змушує вас фільтрувати список певним чином; набагато більш загальний підхід полягає у виконанні функції, яка вирішує, чи слід включати foo. Однак це елегантно, лише якщо у вас є доступ до Java 8.
Doval

5
Протистояти - точкове. Просто зробіть цей метод, який приймає призначений для користувача тип, який містить лише те, що вам потрібно, і в кінцевому підсумку ви можете додати endпараметр до цього об’єкта і навіть за замовчуванням його, щоб не порушити код
Rémi

3
У методах Java 8 за замовчуванням немає підстав додавати зайві аргументи. Метод може бути доданий пізніше з реалізацією за замовчуванням , що прості виклики Певна с, скажімо, з null. Реалізація занять може потім переоцінювати за потребою
Борис Павук

1
@Doval Я не знаю про Java, але в .NET іноді ви побачите речі, як уникнути викриття химерності IQueryable(може приймати лише певні вирази) для коду поза DAL
Ben Aaronson,

Відповіді:


62

Запропонуйте йому дізнатися про YAGNI . Тут може бути особливо цікавою обгрунтована частина сторінки Вікіпедії:

На думку тих, хто виступає за підхід YAGNI, спокуса написати код, який наразі не є необхідним, але може бути в майбутньому, має такі недоліки:

  • Витрачений час береться на додавання, тестування або вдосконалення необхідної функціональності.
  • Нові функції повинні бути налагоджені, задокументовані та підтримані.
  • Будь-яка нова функція накладає обмеження щодо того, що можна зробити в майбутньому, тому непотрібна функція може перешкоджати додаванню необхідних функцій у майбутньому.
  • Поки функція насправді не потрібна, важко повністю визначити, що вона повинна робити, і перевірити її. Якщо нова функція не визначена належним чином і не перевірена, вона може працювати неправильно, навіть якщо вона в кінцевому підсумку потрібна.
  • Це призводить до роздуття коду; програмне забезпечення стає більшим і складнішим.
  • Якщо немає специфікацій і якогось контролю за редагуванням, функція може бути невідома програмістам, які могли б нею скористатися.
  • Додавання нової функції може запропонувати інші нові функції. Якщо ці нові функції також будуть реалізовані, це може призвести до ефекту снігової кулі до повзучості.

Інші можливі аргументи:

  • "80% вартості програмного забезпечення йде на обслуговування" . Введення коду вчасно скорочує витрати на технічне обслуговування: потрібно підтримувати менше коду, і можна зосередитися на дійсно потрібному коді.

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

  • Очікується, що вихідний код буде самодокументованим. Фактичний підпис вводить в оману, оскільки читач може подумати, що це endвпливає або на результат, або на виконання методу.

  • Особи, які пишуть конкретні реалізації цього інтерфейсу, можуть не розуміти, що останній аргумент не слід використовувати, що призведе до різних підходів:

    1. Мені це не потрібно end, тому я просто ігнорую його значення,

    2. Мені це не потрібно end, тому я кину виняток, якщо його немає null,

    3. Мені це не потрібно end, але спробую якось використовувати його,

    4. Я напишу багато коду, який може бути використаний пізніше, коли endзнадобиться.

Але пам’ятайте, що ваш колега може мати рацію.

Усі попередні моменти базуються на тому, що рефакторинг простий, тому додавання аргументу пізніше не зажадає великих зусиль. Але це інтерфейс, і як інтерфейс його можуть використовувати декілька команд, що надають інформацію про інші частини вашого продукту. Це означає, що зміна інтерфейсу може бути особливо болючою, і в цьому випадку YAGNI тут не застосовується.

Відповідь hjk дає вдале рішення: додавання методу до вже використовуваного інтерфейсу не є особливо складним, але в деяких випадках воно також має значну вартість:

  • Деякі рамки не підтримують перевантажень. Наприклад, і якщо я добре пам’ятаю (виправте мене, якщо я помиляюся), WCF .NET не підтримує перевантаження.

  • Якщо в інтерфейсі є багато конкретних реалізацій, додавання методу до інтерфейсу вимагатиме проходження всіх реалізацій та додавання методу також.


4
Я думаю, що також важливо врахувати, наскільки ймовірно ця функція з’явиться в майбутньому. Якщо ви точно знаєте, він буде доданий, але не зараз через будь-яку причину, тоді я б сказав, що це гарний аргумент, який слід пам’ятати, оскільки це не лише функція «на випадок».
Jeroen Vannevel

3
@JeroenVannevel: звичайно, але це дуже спекулятивно. З мого досвіду, більшість функцій, на які я вважав, що, безумовно, будуть реалізовані, були або скасовані, або затримані назавжди. Сюди входять функції, які спочатку були визнані дуже важливими зацікавленими сторонами.
Арсеній Мурценко

26

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

(через деякий час)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

Це все, що вам потрібно, метод перевантаження. Додатковий метод у майбутньому можна запровадити прозоро, не впливаючи на виклики до існуючого методу.


6
За винятком того, що ваша зміна означає, що це більше не інтерфейс, і його неможливо буде використовувати в тих випадках, коли об’єкт походить від об'єкта і реалізує інтерфейс. Тривіальна проблема, якщо всі класи, які реалізують інтерфейс, знаходяться в одному проекті (переслідують помилки компіляції). Якщо це бібліотека, яка використовується в декількох проектах, додавання поля викликає плутанину та роботу для інших людей спорадично протягом наступних х місяців / років.
Kieveli

1
Якщо команда ОП (окрім колеги) дійсно вважає, що endпараметр зараз не потрібний, і він використовує endметод -less впродовж усіх проектів, то оновлення інтерфейсу є найменшою стурбованістю, оскільки це стає необхідністю оновлення заняття з реалізації. Моя відповідь орієнтована конкретно на частину "адаптувати весь код", тому що це здається, що колега думав про необхідність оновлення абонентів оригінального методу протягом усіх проектів, щоб запровадити третій параметр за замовчуванням / манекена .
hjk

18

[Чи є] "шановані" кодуючі гуру, з якими ми можемо пов’язати його [переконати]?

Звернення до влади не є особливо переконливими; краще представити аргумент, що є дійсним незалежно від того, хто це сказав.

Ось скорочення ad absurdum, яке повинно переконати або продемонструвати, що ваш колега застряг у тому, щоб бути "правильним" незалежно від чутливості:


Те, що вам справді потрібно, - це

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

Ви ніколи не знаєте, коли вам доведеться інтернаціоналізувати для Клінгона, тож краще подбайте про це зараз, оскільки для модернізації знадобиться багато роботи, а клінгони не відомі своїм терпінням.


22
You never know when you'll have to internationalize for Klingon. Ось чому ви просто повинні пройти Calendarта дозволити клієнтському коду вирішити, чи хоче він надіслати GregorianCalendarабо а KlingonCalendar(я думаю, деякі вже зробили). Дух. :-p
SJuan76

3
Що про StarDate sdStartі StarDate sdEnd?
Зрештою,

Все це, очевидно, або підкласи або конвертовані в Date!
Darkhogg

Якби це було так просто .
msw

@msw, я не впевнений, що він просив "звернення до влади", коли він просив посилань на "шановні гуру кодування". Як ви кажете, йому потрібен "аргумент, що є дійсним незалежно від того, хто це сказав". Люди типу "гуру", як правило, знають багато таких. Також ви забули HebrewDate startі HebrewDate end.
trysis

12

З точки зору інженерії програмного забезпечення, я вважаю, що правильне вирішення подібних проблем знаходиться у моделі розробника . Це безумовно посилання авторів "гуру" для вашого колеги http://en.wikipedia.org/wiki/Builder_pattern .

У шаблоні конструктора користувач створює об'єкт, який містить параметри. Цей контейнер параметрів буде переданий у метод. Це забезпечить будь-яке розширення та перевантаження параметрів, які знадобляться вашому колезі в майбутньому, роблячи все це дуже стабільним, коли потрібно внести зміни.

Вашим прикладом стануть:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}

3
Це хороший загальний підхід, але в даному випадку , здається, просто натисніть цю проблему з IEventGetterдо ISearchFootRequest. Я б замість цього запропонував щось на кшталт public IFooFilterчлена, public bool include(FooType item)який повертає, включити чи ні. Тоді окремі реалізації інтерфейсу можуть вирішити, як вони будуть виконувати цю фільтрацію
Бен Ааронсон,

@Ben Aaronson Я просто редагую код, щоб показати, як створюється об’єкт параметра Request
InformedA

Тут будівельник непотрібний (і, чесно кажучи, загальновикористаний / надмірно рекомендований зразок); не існує складних правил щодо того, як потрібно налаштувати параметри , тому об’єкт параметра цілком чудово, якщо є законне занепокоєння щодо того, що параметри методу є складними або часто змінюються. І я погоджуюся з @BenAaronson, хоча сенс використання об'єкта параметра не в тому, щоб включити непотрібні параметри відразу ж у біт, а зробити їх дуже легко додати пізніше (навіть якщо є кілька реалізацій інтерфейсу).
Aaronaught

7

Вам вона зараз не потрібна, тому не додайте її. Якщо вам це потрібно пізніше, розгорніть інтерфейс:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

+1 за те, що не змінюється існуючий (і, ймовірно, вже перевірений / відправлений) інтерфейс.
Себастьян Годелет

4

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

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

Але, на моєму досвіді, найбільшим наслідком для YAGNI є YDKWFYN: Ви не знаєте, яка форма вам знадобиться (так, я просто склав цю абревіатуру). Навіть якщо потребуючи деяким граничний параметром може бути відносно передбачувано, він може приймати форму межі сторінки, або кількість днів, або логічне уточнення використовувати кінцеву дату з таблиці переваг користувача, або будь-яку кількість речей.

Оскільки у вас поки немає вимоги, ви не можете знати, яким типом має бути цей параметр. У вас часто виникає незручний інтерфейс, який не найкраще відповідає вашим вимогам, або потрібно все-таки змінити його.


2

Для відповіді на це питання недостатньо інформації. Це залежить від того, що getFooListнасправді робить, і як це робить.

Ось очевидний приклад методу, який повинен підтримувати додатковий параметр, використаний чи ні.

void CapitalizeSubstring (String capitalize_me, int start_index);

Реалізація методу, який працює над колекцією, де ви можете вказати початок, але не кінець колекції, часто нерозумно.

Вам справді потрібно подивитися на саму проблему і запитати, чи не є параметр безглуздим у контексті всього інтерфейсу, і чи дійсно, яке навантаження накладає додатковий параметр.


1

Я боюся, що ваш колега може насправді мати дуже вагомий пункт. Хоча його рішення насправді не найкраще.

З запропонованого інтерфейсу видно, що

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

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

Отже, кращим інтерфейсом буде:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

Якщо ви постачаєте інтервал статичним заводським методом:

public static Interval startingAt(Date start) { return new Interval(start, null); }

тоді клієнти навіть не відчують необхідності вказувати час закінчення.

У той же час ваш інтерфейс правильніше передає те, що він робить, оскільки getFooList(String, Date)не повідомляє, що це стосується інтервалу.

Зауважимо, що моя пропозиція випливає з того, що робить даний метод, а не з того, що він повинен або може зробити в майбутньому, і як такий принцип YAGNI (який дійсно дуже дійсний) тут не застосовується.


0

Додавання невикористаного параметра заплутано. Люди можуть назвати цей метод, припускаючи, що ця функція спрацює.

Я б не додав цього. Пізніше додати його за допомогою рефакторингу та механічної фіксації сайтів викликів. Статично набраною мовою це легко зробити. Не потрібно його охоче додавати.

Якщо для цього параметра є причина, ви можете запобігти плутанині. Додайте затвердження у реалізації цього інтерфейсу, щоб примусити цей параметр передавати як значення за замовчуванням. Якщо хтось випадково використовує цей параметр, принаймні, він негайно помітить під час тестування, що ця функція не реалізована. Це знімає ризик потрапляння помилок у виробництво.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.