Когда я должен вернуть интерфейс, а когда конкретный класс?

StackOverflow https://stackoverflow.com/questions/932452

  •  06-09-2019
  •  | 
  •  

Вопрос

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

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

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

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

Другие советы

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

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

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

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

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

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

В двух словах:код абстрактный, но явно :)

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

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

-Ойсин

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для работы с интерфейсами вы должны создать их экземпляры следующим образом:

IParser parser = new Parser();

Теперь IParser будет вашим интерфейсом, а Parser — вашей реализацией.Теперь, когда вы работаете с объектом парсера сверху, вы будете работать с интерфейсом (IParser), который, в свою очередь, будет работать с вашей реализацией (Parser).

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

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

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

Вы обнаружите (или обнаружили), что возвращаемые интерфейсы проникают в ваш код.напримервы возвращаете интерфейс из метода A, и вы иметь чтобы затем передать интерфейс методу B.

Вы занимаетесь программированием по контракту, хотя и в ограниченных масштабах.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top