Хороша чи погана практика маскувати колекції Java значущими іменами класів?


46

Останнім часом я звик "маскувати" колекції Java під назви класів, зручних для людини. Деякі прості приклади:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

Або:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Колега вказував мені, що це погана практика, і вводить відставання / затримку, а також є анти-схемою OO. Я можу зрозуміти, що це дуже низька ступінь продуктивності, але не можу уявити, що це зовсім важливо. Тож я запитую: це добре чи погано робити, і чому?


11
Це набагато простіше, ніж це. Це погана практика, тому що я думаю, що ви поширюєте реалізацію цих базових колекцій JDK Java. На Java ви можете розширити лише один клас, тому вам доведеться більше думати і проектувати, коли у вас є розширення. На Java використовуйте розширення помірковано.
Поінформовано

ChangeListкомпіляція буде порушена extends, тому що Список - це інтерфейс implements. @randomA те, що ви уявляєте, пропускає суть через цю помилку
gnat

@gnat Бракує точки, я припускаю , що він був розширює implementationІЕ HashMapабо TreeMapі те , що він там була помилка.
Поінформовано

4
Це практика БАД. BAD BAD BAD. Не робіть цього. Усі знають, що таке карта <String, Widget>. Але WidgetCache? Тепер мені потрібно відкрити WidgetCache.java, мені потрібно пам’ятати, що WidgetCache - це лише карта. Я повинен перевіряти кожен раз, коли з'являється нова версія, що ви не додали щось нове у WidgetCache. Боже ні, ніколи цього не роби.
Майлз Рут

"Якщо б ви побачили, як у коді передається ArrayList <ArrayList <? >>, ви б тікали, кричачи ...?" Ні, мені досить комфортно вкладені загальні колекції. І ти повинен бути теж.
Кевін Крумвієде

Відповіді:


75

Затримка / затримка? Я подзвоню БС з цього приводу. З цієї практики має бути рівно нульові накладні витрати. ( Редагувати: У коментарях було зазначено, що це може насправді гальмувати оптимізацію, яку виконує HotSpot VM. Я не знаю достатньо про реалізацію VM, щоб підтвердити або спростувати це. Я базував свій коментар на C ++ реалізація віртуальних функцій.)

Є якийсь код накладних витрат. Ви повинні створити всі конструктори з базового класу, який потрібно, пересилаючи їх параметри.

Я також не розглядаю це як анти-візерунок. Однак я вважаю це пропущеною можливістю. Замість того, щоб створити клас, який отримує базовий клас просто заради перейменування, як ви замість цього створити клас, який містить колекцію та пропонує покращений інтерфейс, що стосується конкретного випадку? Чи повинен ваш кеш-віджет дійсно пропонувати повний інтерфейс карти? Або він повинен запропонувати спеціалізований інтерфейс?

Крім того, у випадку з колекціями шаблон просто не працює разом із загальним правилом використання інтерфейсів, а не реалізацій - тобто в простому коді колекції ви створили б HashMap<String, Widget>і потім призначили його змінній типу Map<String, Widget>. Ваш WidgetCacheне може поширюватися Map<String, Widget>, тому що це інтерфейс. Це не може бути інтерфейс, що розширює базовий інтерфейс, тому HashMap<String, Widget>що не реалізує цей інтерфейс, а також жодна інша стандартна колекція. І хоча ви можете зробити його класом, який розширюється HashMap<String, Widget>, тоді вам доведеться оголосити змінні як WidgetCacheабо Map<String, Widget>, і перший втрачає вам гнучкість замінити іншу колекцію (можливо, якась лінива колекція завантаження ORM), а другий тип перемагає крапку мати клас.

Деякі з цих контрапунктів стосуються і мого запропонованого спеціалізованого класу.

Це все, що слід врахувати. Це може бути, а може і не бути правильним вибором. В будь-якому випадку запропоновані вашими колегами аргументи не вірні. Якщо він вважає, що це анти-модель, він повинен її назвати.


1
Зауважте, що, хоча це цілком можливо мати певний вплив на продуктивність, це не буде таким жахливим (пропустіть деякі вбудовані оптимізації та отримати додатковий vcall у гіршому випадку - не чудово у вашому внутрішньому циклі, але в іншому випадку, ймовірно, добре).
Ву

5
Ця справді хороша відповідь говорить всім "не судіть рішення за результатами роботи" - і єдине обговорення тут нижче - "як HotSpot справляється з цим, чи є якісь (у 99,9% всіх випадків) нерелевантний вплив на продуктивність?", Хлопці, ви навіть потрудилися прочитати більше, ніж перше речення?
Док Браун

16
+1 для "Замість створення класу, який отримує базовий клас просто заради перейменування, як ви замість цього створити клас, який містить колекцію та пропонує покращений інтерфейс для конкретного випадку?"
користувач11153

6
Практичний приклад , який ілюструє основна точку в цій відповіді: документація для public class Properties extends Hashtable<Object,Object>в java.utilговорить «Оскільки властивості успадковується від Hashtable, надягнутого і методів putAll може бути застосована до об'єкта Властивості Їх використання настійно НЕ рекомендується , оскільки вони дозволяють викликає вставити записи , чиї ключі. або значення не є рядками. " Склад був би набагато чистішим.
Патрісія Шанахан

3
@DocBrown Ні, ця відповідь стверджує, що взагалі немає впливу на продуктивність. Якщо ви робите чіткі заяви, краще зможете їх підтримати, інакше люди, ймовірно, вам зателефонують на це. Вся суть коментарів (у відповідях) полягає в тому, щоб вказати на неточні факти чи припущення або додати корисні замітки, але, звичайно, не для того, щоб привітати людей, саме для цього система голосування.
Ву

25

На думку IBM, це насправді є антидіаграмою. Такі класи типу typedef називаються типами psuedo.

У статті йдеться про це набагато краще, ніж я, але спробую підсумувати його, якщо посилання знизиться:

  • Будь-який код, який очікує, що WidgetCacheне може обробити aMap<String, Widget>
  • Ці псевдотипи є "вірусними" при використанні декількох пакетів, вони призводять до несумісності, тоді як базовий тип (просто дурна карта <...>) працював би у всіх випадках у всіх пакунках.
  • Псевдо типи часто конкретні, вони не реалізують конкретні інтерфейси, оскільки їх базові класи реалізують лише загальну версію.

У статті вони пропонують такий трюк, щоб полегшити життя без використання псевдотипів:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Що працює завдяки автоматичному виводу типу.

(Я прийшов до цієї відповіді з допомогою цієї пов'язаної з переповненням стека питання)


13
Чи не newHashMapвирішується зараз потреба в алмазному операторі?
svick

Абсолютно правда, про це забули. Я зазвичай не працюю на Java.
Рой Т.

Я хочу, щоб мови дозволяли створити всесвіт змінних типів, які містили посилання на екземпляри інших типів, але все-таки могли б підтримувати перевірку часу компіляції, таким чином, щоб значення типу WidgetCacheмогло бути присвоєно змінній типу WidgetCacheабо Map<String,Widget>без вибору, але там - це засіб, за допомогою якого статичний метод WidgetCacheможе зробити посилання на нього Map<String,Widget>та повернути його як тип WidgetCacheпісля виконання будь-якої бажаної перевірки. Така особливість може бути особливо корисною для дженериків, які не потрібно було б стирати (оскільки ...
supercat

... типи все-таки існували б лише на увазі компілятора). Для багатьох типів, що змінюються, є два поширених типи посилальних полів: одне, що містить єдине посилання на екземпляр, який може бути вимкнено, або те, що містить посилання на екземпляр, якому ніхто не може мутувати. Було б корисно мати можливість надати різні імена цим двом типам полів, оскільки вони вимагають різних моделей використання, але обидва повинні містити посилання на однакові типи об'єктних екземплярів.
supercat

5
Це чудовий аргумент, чому Java потребує псевдонімів типу.
GlenPeterson

13

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

Ситуація є достатньо поширеною, що більшість усіх мов програмування, які мають статичний тип, мають спеціальний синтаксис для псевдонімових типів, який зазвичай називають a typedef. Java, ймовірно, не копіювала їх, оскільки спочатку не мала параметрів. Розширення класу не є ідеальним через причини, які Себастьян так добре висвітлював у своїй відповіді, але це може бути розумним вирішенням обмеженого синтаксису Java.

Typedefs має ряд переваг. Вони висловлюють наміри програміста більш чітко, з кращим іменем, на більш відповідному рівні абстракції. Їх простіше шукати для налагодження чи рефакторингу. Розглянемо, що пошук скрізь a WidgetCacheвикористовується в порівнянні з пошуком конкретних цілей використання Map. Їх легше змінити, наприклад, якщо згодом виявиться, що вам потрібен LinkedHashMapзамість цього або навіть власний спеціальний контейнер.


Також у конструкторів є мізерний наклад.
Стівен С

11

Я пропоную вам, як уже згадувалося, використовувати композицію над успадкуванням, тому ви можете розкривати лише методи, які дійсно потрібні, з іменами, що відповідають випадку призначеного використання. Чи дійсно користувачам вашого класу потрібно знати, що WidgetCacheце карта? І чи зможуть зробити з цим все, що хочуть? Або вони просто повинні знати, що це кеш для віджетів?

Приклад класу з моєї бази даних з рішенням подібної проблеми, яку ви описали:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Ви бачите, що всередині це "просто карта", але публічний інтерфейс цього не показує. І в ньому є методи, зручні для програмістів, як appendMessage(Locale locale, String code, String message)для більш легкого і змістовного способу вставки нових записів. І, наприклад, користувачі класу не можуть це зробити translations.clear(), тому що Translationsце не розширюється Map.

За бажанням, ви завжди можете делегувати деякі необхідні методи для внутрішньо використовуваної карти.


6

Я бачу це як приклад змістовної абстракції. Хороша абстракція має кілька рис:

  1. Він приховує деталі реалізації, які не мають значення для коду, який його споживає.

  2. Він лише такий складний, як і повинен бути.

Розширюючи, ви відкриваєте весь інтерфейс батьків, але в багатьох випадках більша частина цього може бути краще прихованою, тому ви хочете зробити те, що пропонує Себастьян Редл, і надайте перевагу композиції над успадкуванням і додайте екземпляр батьків як приватний член вашого користувацького класу. Будь-який з методів інтерфейсу, який має сенс для вашої абстракції, можна легко делегувати (у вашому випадку) внутрішній колекції.

Що стосується впливу на ефективність, то завжди добре спочатку оптимізувати код для читабельності, а якщо підозрюється вплив на ефективність, профіліруйте код для порівняння двох реалізацій.


4

+1 до інших відповідей тут. Я також додам, що спільнотою Design Driven Design (DDD) це насправді вважається дуже хорошою практикою. Вони виступають за те, щоб ваш домен та його взаємодія мали мати смислове значення домену на відміну від основної структури даних. A Map<String, Widget>може бути кешем, але він може бути і чимось іншим, те, що ви правильно зробили в My Not So Humble Opinion (IMNSHO), - це моделювати те, що представляє колекція, у цьому випадку кеш.

Я додаю важливу редакцію в тому, що обгортка доменного класу навколо основної структури даних, ймовірно, також повинна мати інші змінні або функції члена, які справді роблять його доменним класом з взаємодіями на відміну від просто структури даних (якщо тільки Java мала типи значень , ми отримаємо їх на Java 10 - обіцяємо!)

Буде цікаво побачити, який вплив потоки Java 8 матимуть на все це, я можу уявити, що, можливо, деякі публічні інтерфейси вважають за краще мати справу з потоком (вставити загальний Java примітив або String) на відміну від об’єкта Java.

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