Когда использовать дженерики в дизайне интерфейса

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может также быть расширен в будущем, но не так, как я могу предвидеть, за пределами моей реализации по умолчанию.

Теперь я пытаюсь сделать эту библиотеку максимально надежной; это все еще находится на ранних стадиях (пре-пре-альфа), так что это может быть актом чрезмерной инженерии (YAGNI). Это подходящее место для использования дженериков?

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

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

Zymus
источник
1
Что-то вроде того, что вы раскрываете детали реализации в любом случае. Почему звонящий должен работать со стеками и знать / заботиться о них, а не о простых количествах?
Цао
Это то, о чем я не думал. Это дает хорошее представление об этой конкретной проблеме. Я обязательно внесу изменения в мой код.
Зимус

Ответы:

9

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

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

Любая ситуация, когда вы хотите улучшить безопасность типов в обобщенном виде, является хорошим кандидатом на генерики. A 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общий способ, как вы предлагаете. Самое глубокое преимущество дженериков в том, что вам почти никогда не нужно ничего разыгрывать, а предложение кода, который вынуждает пользователей разыгрывать, является ИМХО уголовным преступлением.

OldCurmudgeon
источник
2

Ух ты ... У меня совершенно другой взгляд на это.

Я могу предвидеть некоторые сторонние потребности

Это ошибка. Вы не должны писать код «на будущее».

Также интерфейсы написаны сверху вниз. Вы не смотрите на класс и не говорите: «Эй, этот класс может полностью реализовать интерфейс, и тогда я смогу использовать этот интерфейс где-нибудь еще». Это неправильный подход, потому что только класс, который использует интерфейс, действительно знает, каким должен быть интерфейс. Вместо этого вы должны подумать: «В этом месте моему классу нужна зависимость. Позвольте мне определить интерфейс для этой зависимости. Когда я закончу писать этот класс, я напишу реализацию этой зависимости». Будет ли кто-то когда-либо повторно использовать ваш интерфейс и заменять вашу реализацию по умолчанию своей собственной, совершенно не имеет значения. Они могут никогда не делать. Это не значит, что интерфейс был бесполезен. Это заставляет ваш код следовать принципу Inversion of Control, что делает ваш код очень расширяемым во многих местах "

anton1980
источник
0

Я думаю, что использование дженериков в вашей ситуации слишком сложно.

Если вы выбираете непатентованную версию интерфейсов, дизайн указывает, что

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

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

Акио Такахаши
источник
0

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

  • будут ли они просто перезаписывать методы, и поэтому экземпляры их подклассов будут использоваться так же, как базовые классы, просто вести себя по- другому?
  • или они будут добавлять членов и использовать подклассы по-другому?

В первом случае нет необходимости в дженериках, во втором, вероятно, есть.

Майкл Боргвардт
источник
0

Возможно, у вас может быть иерархия интерфейса, где Foo - это один интерфейс, а Bar - другой. Всякий раз, когда тип должен быть и тем и другим, это FooAndBar.

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

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

Также как примечание: интерфейсы о том, какие возможные действия могут быть выполнены. На самом деле они должны быть глаголами, а не существительными.

Акаш Патель
источник
Это может быть альтернативой использованию дженериков, но первоначальный вопрос касался того, когда следует использовать дженерики вместо того, что вы предлагали. Можете ли вы улучшить свой ответ, чтобы предположить, что генерики не нужны никогда, но использование нескольких интерфейсов - это решение для всех?
Джей Элстон