Коли використовувати дженерики в дизайні інтерфейсу


11

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

В даний час вони визначаються як

Пункт:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

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

Зараз я намагаюся зробити цю бібліотеку якомога міцнішою; це ще на ранніх стадіях (пре-пре-альфа), тому це може бути актом надмірної інженерії (ЯГНІ). Це підходяще місце для використання дженериків?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

І

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

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


1
Ніби не відчуваєш, що ти так чи інакше розкриваєш деталі щодо впровадження. Чому абоненту потрібно працювати і знати / піклуватися про стеки, а не прості кількості?
cHao

Це те, про що я не думав. Це дає певне уявлення про цю конкретну проблему. Я обов’язково внесу зміни в свій код.
Зимус

Відповіді:


9

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

Наприклад, будь-яка структура даних, яка може приймати довільні об'єкти, є хорошим кандидатом на загальний інтерфейс. Приклади: List<T>і Dictionary<K,V>.

Будь-яка ситуація, коли ви хочете покращити безпеку типу узагальненим способом, є хорошим кандидатом для дженериків. А List<String>- це список, який працює лише на рядках.

Будь-яка ситуація, коли ви хочете застосувати SRP узагальненим чином та уникати специфічних методів, є хорошим кандидатом для дженериків. Наприклад, PersonDocument, PlaceDocument і ThingDocument можна замінити на Document<T>.

Шаблон абстрактної фабрики є хорошим випадком використання для загального, тоді як звичайний заводський метод просто створює об'єкти, які успадковуються із загального, конкретного інтерфейсу.


Я не дуже згоден з думкою про те, що загальні інтерфейси повинні поєднуватися з загальними реалізаціями. Оголошення параметру загального типу в інтерфейсі, а потім його вдосконалення чи інстанції в різних реалізаціях є потужною технікою. Особливо часто це стосується Scala. Інтерфейси абстрактних речей; класи їх спеціалізують.
Бенджамін Ходжсон

@BenjaminHodgson: це означає, що ви створюєте реалізацію на загальному інтерфейсі для конкретних типів. Загальний підхід все ще є загальним, хоча дещо менш.
Роберт Харві

Я згоден. Можливо, я неправильно зрозумів твою відповідь - ти, здається, радиш проти такого дизайну.
Бенджамін Ходжсон

@BenjaminHodgson: Чи не буде заводський метод записаний на конкретному інтерфейсі, а не на загальному? (хоча абстрактна фабрика була б загальною)
Роберт Харві

Я думаю, що ми згодні :) Я б, мабуть, написав приклад ОП як щось подібне class StackFactory { Stack<T> Create<T>(T item, int count); }. Я можу написати приклад того, що я мав на увазі під «уточненням параметра типу в підкласі» у відповідь, якщо ви хочете отримати більше роз’яснень, хоча відповідь буде стосуватися лише дотичного до початкового питання.
Бенджамін Ходжсон

8

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

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

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


3

Тепер Itemі ItemStackFactoryя можу абсолютно передбачити деякі третьої сторони , яка має намір продовжити його в майбутньому.

Там прийнято ваше рішення. Якщо ви не надаєте дженерики, їх потрібно буде покращувати для Itemкожного впровадження кожного разу:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

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


2

Нічого собі ... я по-іншому ставлюсь до цього.

Я абсолютно можу передбачити якісь сторонні потреби

Це помилка. Ви не повинні писати код "на майбутнє".

Також інтерфейси записуються зверху вниз. Ви не дивитесь на клас і кажете "Ей, цей клас міг би повністю реалізувати інтерфейс, і тоді я можу використовувати цей інтерфейс ще десь". Це неправильний підхід, оскільки тільки клас, який використовує інтерфейс, справді знає, яким повинен бути інтерфейс. Натомість вам слід думати: "У цьому місці мій клас потребує залежності. Дозвольте мені визначити інтерфейс для цієї залежності. Коли я закінчу писати цей клас, я напишу реалізацію цієї залежності". Чи хтось коли-небудь повторно використовувати ваш інтерфейс і замінить вашу стандартну реалізацію власною, зовсім не має значення. Вони ніколи цього не роблять. Це не означає, що інтерфейс був марним. Це змушує ваш код слідувати принципу "Інверсія контролю", що робить ваш код у багатьох місцях дуже розширюваним "


0

Я думаю, що використовувати дженерики у вашій ситуації занадто складно.

Якщо ви обираєте загальну версію інтерфейсів, дизайн вказує на це

  1. ItemStackContainer <A> містить один тип стека, A. (тобто ItemStackContainer не може містити декілька типів стека.)
  2. ItemStack присвячений конкретному типу предмета. Таким чином, відсутній тип абстракції всіх видів стеків.
  3. Вам потрібно визначити конкретний ItemStackFactory для кожного типу елементів. Це вважається занадто тонким, як заводський зразок.
  4. Написати складніший код, наприклад, ItemStack <? розширює Item>, зменшуючи ремонтопридатність.

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


0

Я б сказав, що це залежить головним чином від цього питання: Якщо комусь потрібно розширити функціонал Itemі ItemStackFactory,

  • вони просто перезаписують методи, і тому екземпляри їх підкласів будуть використовуватись так само, як базові класи, просто поводяться інакше?
  • або вони додадуть учасників і використовуватимуть підкласи по-різному?

У першому випадку в дженериках немає потреби, у другому - мабуть, є.


0

Можливо, ви можете мати ієрархію інтерфейсів, де Foo - це один інтерфейс, Bar - інший. Щоразу, коли тип повинен бути обом, це FooAndBar.

Щоб уникнути переналагодження, робіть тип FooAndBar лише тоді, коли це необхідно, і зберігайте список інтерфейсів, які ніколи нічого не розширять.

Це набагато зручніше для більшості програмістів (найбільше люблять перевірку їх типу або люблять ніколи не займатися статичним введенням будь-якого типу). Дуже мало людей написали власний клас генерики.

Також як зауваження: інтерфейси - це те, які можливі дії можна виконати. Вони насправді повинні бути дієсловами, а не іменниками.


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