Сегодня мой счастливый день. Через 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
}
}
Свойство Closures 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 и код синтаксического анализа, прикрепленный к этому сообщению.
Удачного кодирования!