Всегда ли имеет смысл «программировать на интерфейс» в Java?

9

Я видел обсуждение этого вопроса относительно того, как будет реализован класс, реализующий интерфейс. В моем случае я пишу очень маленькую программу на Java, которая использует экземпляр TreeMap, и, по мнению каждого, она должна создаваться следующим образом:

Map<X> map = new TreeMap<X>();

В моей программе я вызываю функцию map.pollFirstEntry(), которая не объявлена ​​в Mapинтерфейсе (и пару других, которые также присутствуют в Mapинтерфейсе). Мне удалось сделать это, приведя к TreeMap<X>везде, где я называю этот метод, как:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

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

РЕДАКТИРОВАТЬ: Я хотел бы отметить, что меня больше интересуют широкие хорошие практики кодирования, а не применение конкретной функции TreeMap. Как уже указывалось в некоторых ответах (и я отметил, что ответили первыми, кто это сделал), следует использовать более высокий возможный уровень абстракции, не теряя при этом функциональности.

jimijazz
источник
1
Используйте TreeMap, когда вам нужны функции. Вероятно, это был выбор дизайна по определенной причине, поэтому он также должен быть частью реализации.
@JordanReiter я должен просто добавить новый вопрос с тем же содержанием, или есть внутренний механизм перекрестной публикации?
jimijazz
2
«Я понимаю преимущества инструкций по инициализации, описанных выше для больших программ». Приведение в другое место не является преимуществом независимо от размера программы
Бен Ааронсон,

Ответы:

23

«Программирование на интерфейсе» не означает «использование максимально абстрактной версии». В этом случае все будут просто использовать Object.

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

Йерун Ванневел
источник
2
TreeMap - это не интерфейс, а класс реализации. Он реализует интерфейсы Map, SortedMap и NavigableMap. Описанный метод является частью интерфейса NavigableMap . Использование TreeMap предотвратит переключение реализации на ConcurrentSkipListMap (например), который является всей точкой кодирования интерфейса, а не реализации.
3
@MichaelT: Я не смотрел на точную абстракцию, которая ему нужна в этом конкретном сценарии, поэтому я использовал TreeMapв качестве примера. «Программа для интерфейса» не должна восприниматься буквально как интерфейс или абстрактный класс - реализация также может рассматриваться как интерфейс.
Йерун Ванневел
1
Хотя открытый интерфейс / методы класса реализации технически являются «интерфейсом», он нарушает концепцию, лежащую в основе LSP, и предотвращает замену другого подкласса, поэтому вы хотите программировать public interfaceвместо «открытых методов реализации». ,
@JeroenVannevel Я согласен, что программирование интерфейса может быть выполнено, когда интерфейс фактически представлен классом. Тем не менее, я не понимаю, какая польза от использования TreeMapбыла бы выше SortedMapилиNavigableMap
toniedzwiedz
16

Если вы все еще хотите использовать интерфейс, вы можете использовать

NavigableMap <X, Y> map = new TreeMap<X, Y>();

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


источник
3

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

Рассмотрим ситуацию, когда оригинальная версия вашего кода использовала HashMapи разоблачила это.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Это означает, что любое изменение getFoo()является критическим изменением API и сделает людей, использующих его, несчастными. Если все, что вы гарантируете, fooэто Карта, вы должны вернуть ее.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Это дает вам гибкость в изменении того, как все работает внутри вашего кода. Вы понимаете, что на fooсамом деле должна быть Карта, которая возвращает вещи в определенном порядке.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

И это ничего не нарушает для остальной части кода.

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

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Это углубляется в принцип подстановки Лискова

Заменимость является принципом объектно-ориентированного программирования. В нем говорится, что в компьютерной программе, если S является подтипом T, объекты типа T могут быть заменены объектами типа S (то есть объекты типа S могут заменять объекты типа T) без изменения какого-либо из желательных свойства этой программы (правильность, выполненная задача и т. д.).

Поскольку NavigableMap является подтипом Map, эта замена может быть выполнена без изменения программы.

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

Вы все еще хотите избежать утечек типов реализации. Например, вы можете захотеть реализовать ConcurrentSkipListMap вместо этого из-за некоторых характеристик производительности или вам просто нравится, java.util.concurrent.ConcurrentSkipListMapа не java.util.TreeMapв операторах импорта или что-то еще.


источник
1

Согласившись с другими ответами, вам следует использовать наиболее общий класс (или интерфейс), который вам действительно нужен, в данном случае TreeMap (или, как кто-то предложил NavigableMap). Но я хотел бы добавить, что это, безусловно, лучше, чем использовать его повсюду, что в любом случае будет гораздо более сильным запахом. См. Https://stackoverflow.com/questions/4167304/why-should-casting-be-avoided по некоторым причинам.

Себастьян ван ден Брук
источник
1

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

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

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

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Конечно, вызывающие могут по-прежнему обрабатывать возвращаемый объект как Mapтаковой, но это выходит за рамки вашего метода:

private Map<String, String> output = processOrderedMap(input);

Делая шаг назад

Общие рекомендации по кодированию для интерфейса (в общем) применимы, потому что обычно именно интерфейс обеспечивает гарантию того, что объект должен быть в состоянии выполнить, то есть контракт . Многие начинающие начинают с того, что HashMap<K, V> map = new HashMap<>()им советуют объявить это как a Map, потому что a HashMapне предлагает ничего больше, чем то, что Mapпредполагается сделать. Исходя из этого, они смогут понять (надеюсь), что их методы должны принимать Mapвместо a HashMap, и это позволяет им реализовать функцию наследования в ООП.

Процитирую всего одну строчку из статьи Википедии о любимом всем принципе, связанном с этой темой:

Это семантическое, а не просто синтаксическое отношение, потому что оно намеревается гарантировать семантическую совместимость типов в иерархии ...

Другими словами, использование Mapобъявления не потому, что оно имеет смысл «синтаксически», а, скорее, вызовы объекта должны заботиться только о его типе Map.

Более чистый код

Я считаю, что это иногда позволяет мне писать более чистый код, особенно когда речь идет о модульном тестировании. Создание HashMapс одной тестовой записью, доступной только для чтения, занимает больше строки (исключая использование инициализации с двойной скобкой), когда я могу легко заменить это на Collections.singletonMap().

ХИК
источник