С тех пор, как Мартин Фаулер говорил о плавных интерфейсах , люди начали повсеместно создавать цепочки методов , создавая плавные 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/