Статьи

Серия: DSL в Groovy, часть: 2

В моем предыдущем посте о создании доменных языков в Groovy я показал, как легко написать DSL, указав, кто за кем следует в Twitter. Результат моей работы выглядел так:

List<User> users = FollowersGraphBuilder.build {
users 'Jim', 'Tom', 'Jane'
user('Jim').follows('Tom').and('Jane')
user('Tom').follows 'Jane'
}

Есть два способа улучшить этот DSL:

  • Использование динамических возможностей Groovy
  • Использование статически типизированных функций Groovy

Стоит отметить, что Groovy позволяет нам использовать оба стиля, в отличие, например, от Ruby. В этом посте я собираюсь показать вам, как улучшить этот DSL, используя несколько динамических приемов.

Чтобы он выглядел лучше, я собираюсь использовать шаблон под названием «Динамический прием». Многие динамические языки позволяют вам отвечать на неизвестные сообщения. Если вы вызываете метод, который не существует, будет выполнен специальный метод. Эта функция не нова, она была известна в сообществе Smalltalk некоторое время назад. В Groovy есть три метода, позволяющих вам ответить на неизвестное сообщение: methodMissing, propertyMissing (для получения), propertyMissing (для установки).

Взгляните на этот кусок кода:

class FollowersGraphBuilder {
...
def propertyMissing(String name) {
new UserFollowingBuilder(user: getUserByName(name))
}

private getUserByName(String name) {
assert users.containsKey(name), "Invalid user name $name"
users[name]
}
...
}

class UserFollowingBuilder {
User user

def follows(UserFollowingBuilder userBuilder) {
userBuilder.user.addFollower user
this
}

def and(UserFollowingBuilder userBuilder) {
follows userBuilder
}
}

Я использую свойство Missing Hook здесь. Каждый раз, когда я пытаюсь получить какое-либо несуществующее свойство, оно вызывает метод propertyMissing и возвращается конструктор. Наш DSL выглядит намного лучше после этой настройки.

List<User> users = FollowersGraphBuilder.build {
users 'Jim', 'Tom', 'Jane'
Jim.follows Tom
Tom.follows Jane
}

Вы можете заметить, что UserFollowingBuilder в нашем примере играет две роли: объект и субъект. На мой взгляд, это хорошо для этой конкретной ситуации. Но для более сложных сценариев это может вызвать проблемы. Я бы присматривал за такими строителями, которые играют слишком много ролей.

Эта команда также может быть улучшена:

users 'Jim', 'Tom', 'Jane'

Первый способ сделать это лучше, просто удалив его. Единственное, что нам нужно изменить, — это метод propertyMissing:

class FollowersGraphBuilder {
private users = [:]

def propertyMissing(String name) {
users[name] = users[name] ?: new User(name)
new UserFollowingBuilder(user: users[name])
}
...
}

Теперь все пользователи создаются по запросу. Я также удалил методы пользователей, так как они нам больше не нужны. После этой настройки наш DSL выглядит лучше:

List<User> users = FollowersGraphBuilder.build {
Jim.follows Tom
Tom.follows Jane
}

Проблема, с которой я сталкиваюсь при таком подходе, заключается в том, что если у вас много пользователей, вы можете напечатать чье-то имя, и будет создан новый пользователь. Также, если в нашей семантической модели есть не только пользователи, но и другие классы, будет очень трудно угадать, какой из них должен быть создан в нашем методе propertyMissing. Вот почему я предпочитаю объявлять пользователей явно.

Поэтому я бы определенно оставил объявление пользователей, но я бы удалил кавычки, чтобы наш DSL выглядел так:

List<User> users = FollowersGraphBuilder.build {
users Jim, Tom, Jane
Jim.follows Tom
Tom.follows Jane
}

Вы можете быть удивлены, насколько легко добиться такого поведения в Groovy:

class FollowersGraphBuilder {
private users = [:]

def users(NewUserBuilder... userNames) {
userNames.each {creator ->
users[creator.name] = new User(creator.name)
}
}

def propertyMissing(String name) {
if(users.containsKey(name)){
new UserFollowingBuilder(user: users[name])
} else {
new NewUserBuilder(name: name)
}
}
...
}

И новый строитель:

class NewUserBuilder {
String name
}

Как это работает прямо сейчас, очень просто. Если propertyMissing вызывается и пользователь с указанным именем еще не создан, то будет возвращен NewUserBuilder. Если он создан, наш старый друг UserFollowingBuilder будет возвращен. Это так просто.

Я мог бы вернуть String вместо NewUserBuilder, но наличие класса может быть полезно, если мне нужно хранить немного больше информации, чем просто имя. Также это может сбить с толку, если произошло исключение.

Например,

List<User> users = FollowersGraphBuilder.build {
users Jim, Tom
Jim.follows Tom1
}

Если вы используете NewUserBuilder, вы увидите это сообщение об ошибке:

No signature of method: UserFollowingBuilder.follows() is applicable for argument types:(NewUserBuilder)

Если вы используете String, сообщение будет не таким хорошим:

No signature of method: UserFollowingBuilder.follows() is applicable for argument types: (String)

Конечно, все ясно в нашем примере, но представьте, что у нас есть 5 классов в нашей семантической модели + группа строителей. Использование String в этом случае сделает отладку намного дольше.

Groovy 1.8 (в частности, GEP-3) может сделать ваш код еще лучше, и вам не нужно ничего делать:

List<User> users = FollowersGraphBuilder.build {
users Jim, Tom, Jane, Rob
Jim.follows Tom and Jane and Rob
Tom.follows Rob and Jim
}

Это все на сегодня. Мы рассмотрели одну из самых классных функций динамических языков — шаблон Dynamic Reception. Но есть и другой способ достижения этого результата:

Jim.follows Tom and Jane and Rob
Tom.follows Rob and Jim

И вам не придется использовать какие-либо динамические возможности Groovy. Я покажу, как это сделать в следующий раз.

 

С http://vsavkin.tumblr.com/post/3254409403/series-dsls-in-groovy-part-2