Статьи

Интеллектуальный курс по Java Fluent API Designer

С тех пор, как Мартин Фаулер говорил о плавных интерфейсах , люди начали повсеместно создавать цепочки методов , создавая плавные API (или DSL ) для каждого возможного варианта использования. В принципе, почти каждый тип DSL может быть сопоставлен с Java. Давайте посмотрим, как это можно сделать

Правила DSL

DSL (предметно-ориентированные языки) обычно создаются из правил, которые выглядят примерно так:


1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD ]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

Кроме того, вы также можете объявить свою грамматику следующим образом (как это поддерживается на этом хорошем сайте Railroad Diagrams ):


Grammar ::= (
  'SINGLE-WORD' |
  'PARAMETERISED-WORD' '('[A-Z]+')' |
  'WORD1' 'OPTIONAL-WORD'? |
  'WORD2' ( 'WORD-CHOICE-A' | 'WORD-CHOICE-B' ) |
  'WORD3'+
)

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

Простая грамматика

Простая грамматика, созданная с помощью http://railroad.my28msec.com/rr/ui

Java реализация этих правил

С интерфейсами Java довольно просто смоделировать вышеуказанный DSL. По сути, вы должны следовать этим правилам преобразования:

  • Каждое «ключевое слово» DSL становится методом Java
  • Каждое DSL-соединение становится интерфейсом
  • Когда у вас есть «обязательный» выбор (вы не можете пропустить следующее ключевое слово), каждое ключевое слово этого выбора является методом в текущем интерфейсе. Если возможно только одно ключевое слово, то есть только один метод
  • Если у вас есть «необязательное» ключевое слово, текущий интерфейс расширяет следующий (со всеми его ключевыми словами / методами)
  • Если у вас есть «повторение» ключевых слов, метод, представляющий ключевое слово repeatable, возвращает сам интерфейс, а не следующий интерфейс
  • Каждое подопределение DSL становится параметром. Это позволит для рекурсивности

Обратите внимание, что также можно смоделировать вышеупомянутый DSL с классами вместо интерфейсов. Но как только вы захотите повторно использовать похожие ключевые слова, множественное наследование методов может оказаться очень полезным, и вам, возможно, будет лучше с интерфейсами.

Установив эти правила, вы можете повторить их по своему желанию для создания DSL произвольной сложности, например jOOQ . Конечно, вам придется как-то реализовать все интерфейсы, но это уже другая история.

Вот как приведенные выше правила переводятся на Java:

// Initial interface, entry point of the DSL
// Depending on your DSL's nature, this can also be a class with static
// methods which can be static imported making your DSL even more fluent
interface Start {
  End singleWord();
  End parameterisedWord(String parameter);
  Intermediate1 word1();
  Intermediate2 word2();
  Intermediate3 word3();
}

// Terminating interface, might also contain methods like execute();
interface End {
  void end();
}

// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
  End optionalWord();
}

// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
  End wordChoiceA();
  End wordChoiceB();
}

// Intermediate interface returning itself on word3(), in order to allow
// for repetitions. Repetitions can be ended any time because this 
// interface extends End
interface Intermediate3 extends End {
  Intermediate3 word3();
}

После определения вышеуказанной грамматики мы можем использовать этот DSL напрямую в Java. Вот все возможные конструкции:

Start start = // ...

start.singleWord().end();
start.parameterisedWord("abc").end();

start.word1().end();
start.word1().optionalWord().end();

start.word2().wordChoiceA().end();
start.word2().wordChoiceB().end();

start.word3().end();
start.word3().word3().end();
start.word3().word3().word3().end();

И самое главное, ваш DSL компилируется прямо в Java! Вы получаете бесплатный парсер. Вы также можете повторно использовать этот DSL в Scala (или Groovy), используя ту же запись или немного другую в Scala, пропуская точки «.» и круглые скобки «()»:

val start = // ...

(start singleWord) end;
(start parameterisedWord "abc") end;

(start word1) end;
((start word1) optionalWord) end;

((start word2) wordChoiceA) end;
((start word2) wordChoiceB) end;

(start word3) end;
((start word3) word3) end;
(((start word3) word3) word3) end;

Примеры из реального мира

Некоторые примеры из реальной жизни можно увидеть в документации и коде jOOQ. Вот выдержка из предыдущего поста довольно сложного SQL-запроса, созданного с помощью jOOQ:

create().select(
    r1.ROUTINE_NAME,
    r1.SPECIFIC_NAME,
    decode()
        .when(exists(create()
            .selectOne()
            .from(PARAMETERS)
            .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
            .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
            .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
                val("void"))
        .otherwise(r1.DATA_TYPE).as("data_type"),
    r1.NUMERIC_PRECISION,
    r1.NUMERIC_SCALE,
    r1.TYPE_UDT_NAME,
    decode().when(
    exists(
        create().selectOne()
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
        create().select(count())
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
    .as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

Вот еще один пример из библиотеки, который выглядит довольно привлекательно для меня. Он называется jRTF и используется для создания RTF-документов в Java в свободном стиле:

rtf()
  .header(
    color( 0xff, 0, 0 ).at( 0 ),
    color( 0, 0xff, 0 ).at( 1 ),
    color( 0, 0, 0xff ).at( 2 ),
    font( "Calibri" ).at( 0 ) )
  .section(
        p( font( 1, "Second paragraph" ) ),
        p( color( 1, "green" ) )
  )
).out( out );

Резюме

Свободные API-интерфейсы были ажиотажем в течение последних 7 лет. Мартин Фаулер стал широко цитируемым человеком и получает большую часть кредитов, даже если бы раньше были открытые API. Один из старейших «плавных API-интерфейсов» Java можно увидеть в java.lang.StringBuffer, который позволяет добавлять произвольные объекты в строку. Но самое большое преимущество свободного API — это его способность легко отображать «внешние DSL» в Java и реализовывать их как «внутренние DSL» произвольной сложности.

 

От http://lukaseder.wordpress.com/2012/01/05/the-java-fluent-api-designer-crash-course/