Сегодня мой счастливый день. Через DZone я нашел Архитектурные Правила , прекрасную небольшую структуру, которая абстрагирует JDepend.
Правила архитектуры настраиваются через собственную XML-схему . Вот пример:
<architecture>
<configuration>
<sources no-packages="exception">
<source not-found="exception">spring.jar</source>
</sources>
<cyclicaldependency test="true"/>
</configuration>
<rules>
<rule id="beans-web">
<comment>
org.springframework.beans.factory cannot depend on
org.springframework.web
</comment>
<packages>
<package>org.springframework.beans.factory</package>
</packages>
<violations>
<violation>org.springframework.web</violation>
</violations>
</rule>
<rule id="must-fail">
<comment>
org.springframework.orm.hibernate3 cannot depend on
org.springframework.core.io
</comment>
<packages>
<package>org.springframework.orm.hibernate3</package>
</packages>
<violations>
<violation>org.springframework.core.io</violation>
</violations>
</rule>
</rules>
</architecture>
В дополнение к красивому API конфигурации я написал свой собственный Groovy DSL. И я сделал это за 2 часа.
architecture {
// cyclic dependency check enabled by default
jar "spring.jar"
rules {
"beans-web" {
comment = "org.springframework.beans.factory cannot depend on org.springframework.web"
'package' "org.springframework.beans"
violation "org.springframework.web"
}
"must-fail" {
comment = "org.springframework.orm.hibernate3 cannot depend on org.springframework.core.io"
'package' "org.springframework.orm.hibernate3"
violation "org.springframework.core.io"
}
}
}
Теперь я покажу вам, как я построил этот DSL, чтобы вы могли научиться писать свои собственные.
Этот DSL, как и многие другие в Groovy, использует синтаксис Builder : вызовы методов, которые принимают в Closure
качестве аргумента. Напоминаем, что a Closure
является одновременно функцией и объектом. Вы можете вызывать и выполнять его как функцию, а также вызывать методы и свойства как объект.
Для поддержки этого синтаксиса компоновщика вы должны написать методы, которые принимают в качестве последнего аргумента groovy.lang.Closure
объект:
// example of builder syntax
someMethod {
}
// signature of method that will be called
// can be void or return an object, that's up to you
void someMethod(Closure cl) {
// do some other work
cl() // call Closure object
}
Первым шагом является создание класса, который будет оценивать файлы конфигурации DSL. Я назвал это GroovyArchitecture
:
class GroovyArchitecture {
static void main(String[] args) {
runArchitectureRules(new File("architecture.groovy"))
}
static void runArchitectureRules(File dsl) {
Script dslScript = new GroovyShell().parse(dsl.text)
}
}
GroovyArchitecture
Класс будет оценивать DSL — файл и получить groovy.lang.Script
объект. Если класс запускается через его main()
метод, он будет читать architecture.groovy
файл в текущем каталоге.
Теперь, когда у меня есть скелет, я должен добавить первый метод, который будет вызываться сценарием DSL: architecture()
метод.
Первый метод обычно сложнее всего реализовать, так как при выполнении скрипта этот метод вызывается для Script
объекта. Излишне говорить, что у этого объекта нет architecture()
метода. Groovy предоставляет способ добавить его через MOP или Meta-Object Protocol.
Сложные слова для достаточно простой техники. Каждый объект в Groovy имеет MetaClass
объект, который обрабатывает все вызовы методов, которые выполняются для этого объекта. Нам нужно создать собственный MetaClass
объект и назначить его объекту сценария.
class GroovyArchitecture {
static void main(String[] args) {
runArchitectureRules(new File("architecture.groovy"))
}
static void runArchitectureRules(File dsl) {
Script dslScript = new GroovyShell().parse(dsl.text)
dslScript.metaClass = createEMC(dslScript.class, {
ExpandoMetaClass emc ->
})
dslScript.run()
}
static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
cl(emc)
emc.initialize()
return emc
}
}
Давайте пройдемся по этому шаг за шагом. Я добавил createEMC()
метод, который создает groovy.lang.ExpandoMetaClass
объект, инициализирует его и возвращает его (строки с 16 по 23). Перед инициализацией объект передается в Closure
(строка 19). Это Closure
передается в качестве аргумента createEMC()
методу (строки с 8 по 12).
Я использую Closure
функцию обратного вызова для настройки ExpandoMetaClass
объекта, скрывая детали его создания и настройки. Возвращаемое значение createEMC()
метода присваивается metaClass
свойству Script
объекта DSL (строка 8).
Я также вызываю run()
метод Script
объекта DSL для выполнения сценария DSL (строка 13).
ExpandoMetaClass
Класс поставляется с Groovy 1.1 , а затем и позволяет добавлять пользовательские методы с помощью протокола в Meta-Object. Другими словами, мы можем добавить любой метод, который мы хотим, к любому объекту, который мы хотим, назначив ExpandoMetaClass
объект metaClass
свойству другого объекта.
Теперь, как добавить эти методы тогда? Мне нужно настроить ExpandoMetaClass
объект:
class GroovyArchitecture {
static void main(String[] args) {
runArchitectureRules(new File("architecture.groovy"))
}
static void runArchitectureRules(File dsl) {
Script dslScript = new GroovyShell().parse(dsl.text)
dslScript.metaClass = createEMC(dslScript.class, {
ExpandoMetaClass emc ->
emc.architecture = {
Closure cl ->
}
})
dslScript.run()
}
static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
cl(emc)
emc.initialize()
return emc
}
}
Я назначая Closure
в architecture
свойстве ExpandoMetaClass
объекта (строки 11 до 15). Это Closure
будет реализация architecture()
метода, а также определит аргументы, которые метод принимает. Назначая architecture
свойство я добавил этот метод к DSL — скрипт через СС: architecture(Closure)
.
Итак, сейчас я могу выполнить этот скрипт DSL без ошибок:
// architecture.groovy file
architecture {
}
Следующим шагом будет добавление классов правил архитектуры в смесь.
import com.seventytwomiles.architecturerules.configuration.Configuration
import com.seventytwomiles.architecturerules.services.CyclicRedundancyServiceImpl
import com.seventytwomiles.architecturerules.services.RulesServiceImpl
class GroovyArchitecture {
static void main(String[] args) {
runArchitectureRules(new File("architecture.groovy"))
}
static void runArchitectureRules(File dsl) {
Script dslScript = new GroovyShell().parse(dsl.text)
Configuration configuration = new Configuration()
configuration.doCyclicDependencyTest = true
configuration.throwExceptionWhenNoPackages = true
dslScript.metaClass = createEMC(dslScript.class, {
ExpandoMetaClass emc ->
emc.architecture = {
Closure cl ->
}
})
dslScript.run()
new CyclicRedundancyServiceImpl(configuration)
.performCyclicRedundancyCheck()
new RulesServiceImpl(configuration).performRulesTest()
}
static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
cl(emc)
emc.initialize()
return emc
}
}
Configuration
Класс принимает конфигурацию рамок архитектуры правил. Я назначаю два полезных значения по умолчанию (строки с 12 по 14). CyclicRedundancyServiceImpl
И RulesServiceImpl
классы выполняют фактические проверки на исходном коде (строки 27 и 29).
Следующим шагом является добавление расположения классов или JAR-файлов. Я хочу расширить DSL следующим образом:
// architecture.groovy file
architecture {
classes "target/classes"
jar "myLibrary.jar"
}
Добавить эти два метода проще, так как мне больше не нужно их использовать ExpandoMetaClass
. Вместо этого я назначая делегат к Closure
объекту , который передается в качестве аргумента при выполнении architecture()
метода.
Перед назначением делегата , однако я должен создать новый класс: ArchitectureDelegate
. Любые методы и свойства, которые вызываются внутри, Closure
будут делегированы ArchitectureDelegate
объекту. Следовательно, ArchitectureDelegate
класс должен предоставлять два метода: classes(String)
и jar(String)
.
import com.seventytwomiles.architecturerules.configuration.Configuration
class ArchitectureDelegate {
private Configuration configuration
ArchitectureDelegate(Configuration configuration) {
this.configuration = configuration
}
void classes(String name) {
this.configuration.addSource new SourceDirectory(name, true)
}
void jar(String name) {
classes name
}
}
Как вы можете видеть classes()
и jar()
методы фактические методы на ArchitectureDelegate
классе. Следующим шагом является назначение ArchitectureDelegate
объекта в качестве делегата Closure
переданному architecture()
методу.
import com.seventytwomiles.architecturerules.configuration.Configuration
import com.seventytwomiles.architecturerules.services.CyclicRedundancyServiceImpl
import com.seventytwomiles.architecturerules.services.RulesServiceImpl
class GroovyArchitecture {
static void main(String[] args) {
runArchitectureRules(new File("architecture.groovy"))
}
static void runArchitectureRules(File dsl) {
Script dslScript = new GroovyShell().parse(dsl.text)
Configuration configuration = new Configuration()
configuration.doCyclicDependencyTest = true
configuration.throwExceptionWhenNoPackages = true
dslScript.metaClass = createEMC(dslScript.class, {
ExpandoMetaClass emc ->
emc.architecture = {
Closure cl ->
cl.delegate = new ArchitectureDelegate(configuration)
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
})
dslScript.run()
new CyclicRedundancyServiceImpl(configuration)
.performCyclicRedundancyCheck()
new RulesServiceImpl(configuration).performRulesTest()
}
static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
cl(emc)
emc.initialize()
return emc
}
}
Свойство Closure
s delegate
принимает ArchitectureDelegate
объект (строка 22). resolveStrategy
Свойство имеет значение Closure.DELEGATE_FIRST
(строка 23). Это означает, что любой метод или свойство, вызываемое внутри объекта, Closure
будет делегировано ArchitectureDelegate
объекту. Я звоню Closure
на линии 25.
Время добавить rules()
метод в DSL:
// architecture.groovy file
architecture {
classes "target/classes"
jar "myLibrary.jar"
rules {
}
}
Где добавить этот метод? Для делегата объекта, конечно.
import com.seventytwomiles.architecturerules.configuration.Configuration
class ArchitectureDelegate {
private Configuration configuration
ArchitectureDelegate(Configuration configuration) {
this.configuration = configuration
}
void classes(String name) {
this.configuration.addSource new SourceDirectory(name, true)
}
void jar(String name) {
classes name
}
void rules(Closure cl) {
cl.delegate = new RulesDelegate(configuration)
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
}
Добавить rules()
метод к синтаксису DSL так же просто, как добавить rules()
метод к ArchitectureDelegate
классу (строки с 18 по 23). И так далее. Каждый новый Closure
в DSL получает свой собственный объект делегата. Вы найдете скрипт DSL и код синтаксического анализа, прикрепленный к этому сообщению.
Удачного кодирования!