Когда я должен вернуть интерфейс и когда конкретный класс? - PullRequest
21 голосов
/ 31 мая 2009

при программировании на Java я практически всегда, просто по привычке, пишу что-то вроде этого:

public List<String> foo() {
    return new ArrayList<String>();
}

Большую часть времени даже не думая об этом. Теперь возникает вопрос: должен ли я всегда указывать интерфейс в качестве возвращаемого типа? Или целесообразно использовать фактическую реализацию интерфейса, и если да, то при каких обстоятельствах?

Очевидно, что использование интерфейса имеет много преимуществ (вот почему он есть). В большинстве случаев не имеет значения, какая конкретная реализация используется библиотечной функцией. Но, возможно, есть случаи, когда это имеет значение. Например, если я знаю, что в первую очередь я получу доступ к данным в списке случайным образом, то LinkedList будет плохим. Но если моя библиотечная функция возвращает только интерфейс, я просто не знаю. Чтобы быть в безопасности, мне может даже понадобиться скопировать список явно в ArrayList:

List bar = foo();
List myList = bar instanceof LinkedList ? new ArrayList(bar) : bar;

но это только кажется ужасным, и мои коллеги, вероятно, линчуют меня в столовой. И это справедливо.

Что вы, ребята, думаете? Каковы ваши рекомендации, когда вы стремитесь к абстрактному решению и когда вы раскрываете детали своей реализации для потенциального повышения производительности?

Ответы [ 12 ]

30 голосов
/ 31 мая 2009

Вернуть соответствующий интерфейс, чтобы скрыть детали реализации. Ваши клиенты должны заботиться только о том, что предлагает ваш объект, а не о том, как вы его реализовали. Если вы начнете с закрытого ArrayList, а затем решите, что что-то более подходящее (например, LinkedLisk, список пропусков и т. Д.) Более уместно, вы можете изменить реализацию, не затрагивая клиентов, если вы вернете интерфейс. В тот момент, когда вы возвращаете конкретный тип, возможность теряется.

9 голосов
/ 31 мая 2009

Например, если я знаю, что я буду прежде всего получить доступ к данным в списке случайно LinkedList будет плохим. Но если моя функция библиотеки только возвращает интерфейс, я просто не знать. Чтобы быть на безопасной стороне, я мог бы даже нужно скопировать список явно в ArrayList.

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

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

Тем не менее, ваш пример пахнет преждевременной оптимизацией.

Если метод является или может быть критическим, он может упомянуть подробности реализации в документации.

7 голосов
/ 31 мая 2009

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

В этом контексте я бы ответил и вернул бы только то, что имеет значение . Имеет ли смысл возвращаемое значение быть конкретным классом? Ака в вашем примере, спросите себя: будет ли кто-нибудь использовать метод, специфичный для LinkedList, для возвращаемого значения foo?

  • Если нет, просто используйте интерфейс более высокого уровня. Он гораздо более гибкий и позволяет менять бэкэнд
  • Если да, спросите себя: не могу ли я выполнить рефакторинг своего кода, чтобы вернуть интерфейс более высокого уровня? :)

Чем более абстрактен ваш код, тем меньше изменений вы должны будете вносить при смене бэкенда. Это так просто.

Если, с другой стороны, вы в конечном итоге приведете возвращаемые значения к конкретному классу, то это сильный признак того, что вы, вероятно, должны возвращать вместо этого конкретного класса. Ваши пользователи / партнеры по команде не должны знать о более или менее неявных контрактах: если вам нужно использовать конкретные методы, просто верните конкретный класс для ясности.

В двух словах: код аннотация , но явно :)

7 голосов
/ 31 мая 2009

Не имея возможности обосновать это множеством цитат из CS (я самоучка), я всегда руководствовался мантрой «Прими наименее производное, верни наиболее производное», когда проектировал классы, и он выдержал мне хорошо за эти годы.

Я предполагаю, что с точки зрения интерфейса по сравнению с конкретным возвратом это означает, что если вы пытаетесь уменьшить зависимости и / или отделить, возвращение интерфейса, как правило, более полезно. Однако, если конкретный класс реализует больше , чем этот интерфейс, для вызывающих методов вашего метода обычно более полезно вернуть конкретный класс (т. Е. «Наиболее производный»), чем произвольно ограничивать их подмножество функциональности этого возвращаемого объекта - если только вам не нужно , чтобы ограничить их. Опять же, вы также можете просто увеличить охват интерфейса. Подобные ненужные ограничения я сравниваю с бездумной герметизацией классов; никогда не знаешь. Просто чтобы немного рассказать о первой части этой мантры (для других читателей), принятие наименьшего производного также дает максимальную гибкость для вызывающих вас методов.

-Oisin

6 голосов
/ 31 мая 2009

В общем случае для общедоступного интерфейса, такого как API, возвращение интерфейса (например, List) поверх конкретной реализации (например, ArrayList) было бы лучше.

Использование ArrayList или LinkedList - это деталь реализации библиотеки, которую следует учитывать для наиболее распространенного варианта использования этой библиотеки. И, конечно же, внутренне наличие private методов, передающих LinkedList s, не обязательно будет плохой вещью, если оно предоставляет средства, облегчающие обработку.

Нет причины, по которой конкретный класс не должен использоваться в реализации, если только нет веских оснований полагать, что какой-то другой класс List будет использоваться позже. Но опять же, изменение деталей реализации не должно быть таким болезненным, если публичная часть хорошо продумана.

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

4 голосов
/ 01 июня 2009

На главный вопрос уже дан ответ, и вы всегда должны использовать интерфейс. Я, однако, просто хотел бы прокомментировать

Очевидно, что использование интерфейса имеет много преимуществ (вот почему он есть). В большинстве случаев не имеет значения, какая конкретная реализация используется библиотечной функцией. Но, возможно, есть случаи, когда это имеет значение. Например, если я знаю, что я буду в первую очередь обращаться к данным в списке случайным образом, LinkedList будет плохим. Но если моя библиотечная функция возвращает только интерфейс, я просто не знаю. Чтобы быть в безопасности, мне может даже понадобиться скопировать список явно в ArrayList.

Если вы возвращаете структуру данных, которая, как вы знаете, имеет низкую производительность произвольного доступа - O (n) и, как правило, много данных - есть другие интерфейсы, которые вы должны указать вместо List, например Iterable, чтобы любой, кто использует библиотека будет полностью осведомлена о том, что доступен только последовательный доступ.

Выбор правильного типа для возврата - это не только интерфейс по сравнению с конкретной реализацией, но и выбор правильного интерфейса.

4 голосов
/ 31 мая 2009

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

Использование интерфейсов таким образом оказалось очень надежным способом написания кода многократного использования.

3 голосов
/ 01 июня 2009

Не имеет большого значения, возвращает ли метод API интерфейс или конкретный класс; несмотря на то, что все здесь говорят, вы почти никогда не меняете класс реализации после написания кода.

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

Когда метод API возвращает ArrayList, я абсолютно не сомневаюсь в этом, но когда ему требуется параметр ArrayList (или, как правило, Vector), я рассматриваю поиск программиста и причинение ему вреда , потому что это означает, что я не могу использовать Arrays.asList(), Collections.singletonList() или Collections.EMPTY_LIST.

2 голосов
/ 12 июля 2014

Извините, что не согласен, но я думаю, что основное правило таково:

  • Для входных аргументов использовать наиболее универсальных .
  • Для выходных значений, наиболее специфических .

Итак, в этом случае вы хотите объявить реализацию как:

public ArrayList<String> foo() {
  return new ArrayList<String>();
}

Обоснование : Случай ввода уже известен и объяснен всем: используйте интерфейс, точку. Тем не менее, выходной регистр может выглядеть нелогичным. Вы хотите вернуть реализацию, потому что вы хотите, чтобы клиент имел наибольшую информацию о том, что получает. В этом случае больше знаний - больше силы .

Пример 1: клиент хочет получить 5-й элемент:

  • return Collection: должен повторяться до 5-го элемента против return List:
  • возврат списка: list.get(4)

Пример 2: клиент хочет удалить 5-й элемент:

  • return List: должен создать новый список без указанного элемента (list.remove() необязательно).
  • return ArrayList: arrayList.remove(4)

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

Итак, опять же, правило можно сформулировать так:

  • Будьте гибки в том, что вы предлагаете.
  • Будьте информативны с тем, что вы доставляете.

Итак, в следующий раз, пожалуйста, верните реализацию.

1 голос
/ 03 июня 2009

Обычно используйте интерфейс во всех случаях, если вам не нужны функциональные возможности конкретного класса. Обратите внимание, что для списков Java добавила класс маркера RandomAccess , главным образом, чтобы отличить общий случай, когда алгоритму может понадобиться узнать, является ли get (i) постоянным временем или нет.

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

...