Статьи

Отличный DSL с нуля за два часа

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

Удачного кодирования!