Чи завжди має сенс "програмувати інтерфейс" на Java?


9

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

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

У своїй програмі я викликаю функцію map.pollFirstEntry(), яка не задекларована в Mapінтерфейсі (та ще пару присутніх в Mapінтерфейсі). Мені вдалося це зробити, перейшовши туди, TreeMap<X>де я називаю такий метод, як:

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

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

EDIT: Я хотів би зазначити, що мене більше цікавить широка хороша практика кодування замість застосування конкретної функції TreeMap. Як уже було вказано на деякі відповіді (і я зазначив, що відповів першим, хто це зробив), слід використовувати більш високий рівень абстракції, не втрачаючи функціональності.


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 - довіряйте мені, коли я бачити своє ім'я у винному svn я хвилююся).

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


1

Погоджуючись з іншими відповідями, що ви повинні використовувати найзагальніший клас (або інтерфейс), з якого вам насправді щось потрібно, в цьому випадку TreeMap (або як хтось запропонував NavigableMap). Але я хотів би додати, що це, безумовно, краще, ніж кидати його скрізь, що було б набагато більшим запахом у будь-якому випадку. Дивіться /programming/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<>()і їм рекомендується заявити про це як про те Map, що HashMapне пропонує нічого іншого, ніж те, що Mapналежить зробити. Виходячи з цього, вони зможуть зрозуміти (сподіваємось), чому їхні методи повинні використовуватись Mapзамість а HashMap, і це дозволяє їм усвідомити особливість успадкування в ООП.

Цитуючи лише один рядок із запису у Вікіпедії улюбленого принципу кожного, пов’язаного з цією темою:

Це семантичне, а не просто синтаксичне відношення, оскільки воно має намір гарантувати семантичну сумісність типів в ієрархії ...

Іншими словами, використання Mapдекларації не тому, що це має сенс «синтаксично», а, скоріше, виклики на об’єкт повинні лише дбати про те, що це тип Map.

Чистіший код

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

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