Хорошая или плохая практика маскировать коллекции 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>> {
}

Коллега указал мне, что это плохая практика, и вводит задержку / задержку, а также является анти-паттерном ОО. Я могу понять, что это приводит к очень небольшому снижению производительности, но не могу представить, что это вообще важно. Поэтому я спрашиваю: хорошо это или плохо, и почему?

herpylderp
источник
11
Это намного проще, чем это. Это плохая практика, потому что я предполагаю, что вы расширяете реализации этих базовых Java JDK Collection. В Java вы можете расширить только один класс, поэтому вам нужно больше думать и проектировать, когда у вас есть расширение. В Java используйте расширение экономно.
InformedA
ChangeListкомпиляция прервется extends, потому что List является интерфейсом, требует implements. @randomA то, что вы представляете, не находит смысла из-за этой ошибки
комнат
@gnat Это не упущение, я предполагаю, что он продлевал то implementationесть HashMapили TreeMapто, что у него было, было опечаткой.
InformedA
4
Это плохая практика. ПЛОХО ПЛОХО ПЛОХО. Не делай этого. Все знают, что такое карта <String, Widget>. Но WidgetCache? Теперь мне нужно открыть WidgetCache.java, я должен помнить, что WidgetCache - это просто карта. Каждый раз, когда выходит новая версия, я должен проверять, что вы не добавили что-то новое в WidgetCache. Бог нет, никогда не делай этого.
Майлз Рут
«Если бы вы видели, как ArrayList <ArrayList <? >> передавался в коде, вы бы убежали с криком…?» Нет, мне вполне комфортно с вложенными общими коллекциями. И ты должен быть тоже.
Кевин Крумвиде,

Ответы:

75

Лаг / латентность? Я звоню BS по этому вопросу. От этой практики накладных расходов должно быть ровно ноль. ( Редактировать: в комментариях было указано, что это может фактически препятствовать оптимизации, выполняемой виртуальной машиной HotSpot. Я не знаю достаточно о реализации виртуальной машины, чтобы подтвердить или опровергнуть это. Я основывал свой комментарий на 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 за "Вместо создания класса, который выводит базовый класс просто для переименования, как насчет того, чтобы создать класс, который содержит коллекцию и предлагает улучшенный интерфейс для конкретного случая?"
user11153
6
Практический пример, иллюстрирующий главное в этом ответе: документация для public class Properties extends Hashtable<Object,Object>in java.utilговорит: «Поскольку свойства наследуются от Hashtable, методы put и putAll можно применять к объекту Properties. Их использование настоятельно не рекомендуется, поскольку они позволяют вызывающей стороне вставлять записи, чьи ключи или значения не являются строками. " Композиция была бы намного чище.
Патриция Шанахан,
3
@DocBrown Нет, в этом ответе утверждается, что никакого влияния на производительность вообще нет. Если вы делаете сильные заявления, вам лучше их поддержать, иначе люди, вероятно, будут вас об этом призывать. Весь смысл комментариев (ответов) состоит в том, чтобы указывать на неточные факты или предположения или добавлять полезные заметки, но, конечно, не для того, чтобы поздравлять людей, для этого и нужна система голосования.
Во
25

По мнению IBM, на самом деле это анти-паттерн. Эти классы типа typedef называются псевдо-типами.

Статья объясняет это намного лучше, чем я, но я постараюсь обобщить это в случае, если ссылка не работает:

  • Любой код, который ожидает, WidgetCacheне может обработатьMap<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после выполнения любой желаемой проверки. Такая функция может быть особенно полезна с обобщениями, которые не нужно стирать типом (так как ...
суперкат
... типы будут существовать только в уме компилятора в любом случае). Для многих изменяемых типов существует два общих типа ссылочных полей: одно, которое содержит единственную ссылку на экземпляр, который может быть изменен, или другое, которое содержит разделяемую ссылку на экземпляр, который никто не может изменять. Было бы полезно иметь возможность давать разные имена этим двум типам полей, поскольку они требуют разных шаблонов использования, но они оба должны содержать ссылки на одни и те же типы экземпляров объектов.
суперкат
5
Это отличный аргумент, почему Java нуждается в псевдонимах типов.
ГленПетерсон
13

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

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

У typedefs есть ряд преимуществ. Они выражают намерение программиста более четко, с лучшим названием, на более подходящем уровне абстракции. Их легче искать в целях отладки или рефакторинга. Рассмотрим нахождение везде 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.

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

user11153
источник
6

Я вижу это как пример значимой абстракции. У хорошей абстракции есть пара черт:

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

  2. Это настолько сложно, насколько это необходимо.

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

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

Майк Партридж
источник
4

+1 к другим ответам здесь. Я также добавлю, что это действительно считается очень хорошей практикой в ​​сообществе Domain Driven Design (DDD). Они утверждают, что ваш домен и взаимодействия с ним должны иметь смысловой домен, а не основную структуру данных. A Map<String, Widget>может быть кешем, но это может быть и что-то еще, что вы правильно сделали. В моем не столь скромном мнении (IMNSHO) нужно смоделировать то, что представляет коллекция, в данном случае кеш.

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

Будет интересно посмотреть, как потоки Java 8 будут оказывать на все это, я могу представить, что, возможно, некоторые общедоступные интерфейсы предпочтут иметь дело с потоком (вставьте общий Java-примитив или String), а не с объектом Java.

Мартейн Вербург
источник