Статьи

Страшный DefaultAbstractHelperImpl

Некоторое время назад мы опубликовали эту забавную игру, которую мы называем Spring API Bingo . Это дань и лесть огромному творчеству Spring при формировании значимых имен классов, таких как

  • FactoryAdvisorAdapterHandlerLoader
  • ContainerPreTranslatorInfoDisposable
  • BeanFactoryDestinationResolver
  • LocalPersistenceManagerFactoryBean

Два из вышеперечисленных классов действительно существуют. Вы можете их заметить? Если нет, играйте в Spring API Bingo !

Очевидно, что Spring API страдает от…

Называть вещи

Есть только две трудные проблемы в информатике. Аннулирование кэша, присвоение имен вещам и отдельные ошибки

– Тим Брей цитирует Фила Карлтона

Есть пара таких префиксов или суффиксов, от которых просто трудно избавиться в программном обеспечении Java. Рассмотрим недавнюю дискуссию в Твиттере, которая неизбежно приведет к (очень) интересной дискуссии :

Да, суффикс Impl – интересная тема. Почему у нас это есть, и почему мы продолжаем называть вещи таким образом?

Спецификация против тела

Ява это причудливый язык. В то время, когда это было изобретено, предметная ориентация была горячей темой. Но у процедурных языков были и интересные особенности. Одним из очень интересных языков в то время была Ада (а также PL / SQL, который был в основном получен из Ады). Ада (как PL / SQL) разумно организует процедуры и функции в пакеты, которые бывают двух видов: спецификация и тело. Из примера википедии :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
-- Specification
package Example is
  procedure Print_and_Increment (j: in out Number);
end Example;
 
-- Body
package body Example is
  
  procedure Print_and_Increment (j: in out Number) is
  begin
    -- [...]
  end Print_and_Increment;
  
begin
  -- [...]
end Example;

Вы всегда должны делать это, и две вещи названы одинаково: Example . И они хранятся в двух разных файлах: Example.ads (реклама для Ada и s для спецификации) и Example.adb (b для тела). PL / SQL следовал примеру и называет файлы пакетов Example.pks и Example.pkb с pk для Package.

Java пошла другим путем, в основном из-за полиморфизма и из-за того, как работают классы:

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

В частности, классы могут быть гибридом только для спецификации, с частичным телом (когда они абстрактны) и с полной спецификацией и телом (когда они конкретны).

Как это приводит к именованию в Java

Не все ценят чистое разделение характеристик и кузова, и это, безусловно, можно обсудить. Но когда вы находитесь в этом духе Ada-esque, то, вероятно, вам нужен один интерфейс для каждого класса, по крайней мере, там, где выставлен API. Мы делаем то же самое для jOOQ , где мы установили следующую политику для именования вещей:

* Impl

Все реализации (тела), которые находятся в отношении 1: 1 с соответствующим интерфейсом, имеют суффикс Impl . Если возможно, мы стараемся сохранять эти реализации закрытыми и таким образом запечатанными в пакете org.jooq.impl . Примеры:

Эта строгая схема именования сразу дает понять, какой интерфейс (и, следовательно, публичный API), а какой – реализация. Хотелось бы, чтобы Java была больше похожа на Аду в этом отношении, но у нас есть полиморфизм, и это здорово, и…

Аннотация*

… и это приводит к повторному использованию кода в базовых классах. Как мы все знаем, общие базовые классы должны (почти) всегда быть абстрактными. Просто потому, что они чаще всего являются неполными реализациями (телами) их соответствующей спецификации. Таким образом, у нас есть много частичных реализаций, которые также находятся в соотношении 1: 1 с соответствующим интерфейсом, и мы ставим их префиксом Abstract . Чаще всего эти частичные реализации также являются частными для пакета и запечатаны в пакете org.jooq.impl . Примеры:

В частности, ResultQuery – это интерфейс, расширяющий Query , и, следовательно, AbstractResultQuery – это частичная реализация, которая расширяет AbstractQuery , который также является частичной реализацией.

Частичная реализация имеет смысл в нашем API, потому что наш API является внутренним DSL (предметно-ориентированным языком) и, следовательно, имеет тысячи методов, которые всегда одинаковы, независимо от того, что на самом деле делает конкретное Field , например, Substring

По умолчанию*

Мы делаем все API, связанные с интерфейсами. Это оказалось очень эффективным уже в популярных API Java SE, таких как:

  • Коллекции
  • Streams
  • JDBC
  • DOM

Мы также делаем все SPI (Service Provider Interface), связанный с интерфейсами. Существует одно существенное различие между API и SPI с точки зрения развития API:

  • API потребляются пользователями, вряд ли реализованы
  • SPI реализуются пользователями, практически не потребляются

Если вы не разрабатываете JDK (и, следовательно, не имеете совершенно безумных правил обратной совместимости ), вы, вероятно, в основном безопасно добавляете новые методы в интерфейсы API . Фактически, мы делаем это в каждом дополнительном выпуске, так как мы не ожидаем, что кто-то реализует наш DSL (кто захочет реализовать методы Field 286 или DSL 677. Это безумие!)

Но SPI разные. Всякий раз, когда вы предоставляете своему пользователю SPI, такие как что-либо с суффиксом *Listener или *Provider , вы не можете просто добавить к ним новые методы – по крайней мере, до Java 8, так как это может нарушить реализации, и их много. ,

Что ж. Мы все еще делаем это, потому что у нас нет тех правил обратной совместимости JDK. У нас есть более расслабленные . Но мы предлагаем нашим пользователям самим не реализовывать интерфейсы, а расширять реализацию по Default , которая пуста. Например, ExecuteListener и соответствующий DefaultExecuteListener :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public interface ExecuteListener {
    void start(ExecuteContext ctx);
    void renderStart(ExecuteContext ctx);
    // [...]
}
 
public class DefaultExecuteListener
implements ExecuteListener {
 
    @Override
    public void start(ExecuteContext ctx) {}
 
    @Override
    public void renderStart(ExecuteContext ctx) {}
 
    // [...]
}

Таким образом, Default* – это префикс, который обычно используется для предоставления единой общедоступной реализации, которую пользователи API могут использовать и создавать, либо разработчики SPI могут расширять без риска обратной совместимости. Это в значительной степени обходной путь для отсутствия методов интерфейса по умолчанию в Java 6/7, поэтому наименование префиксов еще более уместно.

Java 8 версия этого правила

Фактически, из этой практики становится очевидным, что «хорошим» правилом для определения совместимых с Java-8 SPI является использование интерфейсов и установка всех методов по умолчанию с пустым телом. Если бы jOOQ не поддерживал Java 6, мы, вероятно, указали бы наш ExecuteListener следующим образом:

1
2
3
4
5
public interface ExecuteListener {
    default void start(ExecuteContext ctx) {}
    default void renderStart(ExecuteContext ctx) {}
    // [...]
}

* Утилиты или * Помощник

Хорошо, вот один из них для экспертов по тестированию / тестированию / освещению и поклонникам.

НУЖНО иметь «дамп» для всех видов статических служебных методов. Я имею в виду, конечно, вы могли бы быть сотрудником объектно-ориентированной полиции . Но…

Пожалуйста. Не будь «тем парнем»!

Итак, существуют различные методы идентификации служебных классов. В идеале вы берете соглашение об именовании и затем придерживаетесь его. Например, * использует .

С нашей точки зрения, в идеале вы бы просто сбросили все служебные методы, которые строго не связаны с очень специфической областью в одном классе, потому что, честно говоря, когда вы в последний раз оценивали необходимость проходить миллионы классов, чтобы найти этот служебный метод? Никогда. У нас есть org.jooq.impl.Utils . Почему? Потому что это позволит вам сделать:

1
import static org.jooq.impl.Utils.*;

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

обсуждение

… или, собственно, давайте начнем обсуждение. Какие у вас приемы и почему? Вот пара реакций на оригинальный твит Тома Бужока, чтобы помочь вам начать:

Поехали !

Ссылка: Страшный DefaultAbstractHelperImpl от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .