С тех пор, как Мартин Фаулер говорил о плавных интерфейсах , люди начали повсеместно создавать цепочки методов , создавая плавные 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'+
)
Проще говоря, у вас есть начальное условие или состояние, из которого вы можете выбрать слова из некоторых языков до достижения конечного условия или состояния. Это как конечный автомат, и поэтому его можно нарисовать на картинке так:
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/
