No Programming, No Life

プログラミング関連の話題や雑記

ゼロから2時間で作るGroovy DSL

はじめに

この記事はA Groovy DSL from scratch in 2 hours | Groovy Zoneを意訳したものです。訳者の力不足により翻訳に未熟な部分が多数あると思われますこと、ご勘弁願います。

ゼロから2時間で作るGroovy DSL

今日はついてるな。DZoneでArchitecture Rulesという小さくて素敵なフレームーワークのJDependの抄録を見つけたからだ。

Architecture Rulesは自分自身の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>

私は自分用に最も良いConfiguration APIをGroovy DSLを使って2時間で書き上げました。

architecture {
    // cyclic dependency はデフォルトでtrueとする
    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を構築できるようになるか学ぶことができるでしょう。

このDSLは他の多くのGroovyのBuilder構文に似ています。メソッド呼び出しはClosureを引数にとります。念のため補足しておきますと、Closureは関数でもありオブジェクトでもあります。関数のように呼び出して実行することもできますし、オブジェクトのようにメソッドやプロパティを呼び出すこともできます。

このビルダー構文を実現するためには、最後の引数がgroovy.lang.Closureを取るメソッドを書かなければなりません。

// ビルダー構文の例
someMethod {

}

// このメソッドのシグネチャ
// voidかObjectを返すようにすることができる。
void someMethod(Closure cl) {
    // 何かの処理
    cl() // クロージャオブジェクトの呼び出し
}

最初のステップは、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)を通してそれを追加する方法を提供しています。

簡単なことを難しい言葉で言ってしまいました。それぞれのGroovyオブジェクトはMetaClassオブジェクトを持っています。それはすべてのメソッド呼び出しを制御しており、そのオブジェクト上で実行されます。私たちがすべきことは、独自のMetaClassオブジェクトを作成して、それをスクリプトオブジェクトに組み込むことです。

 1: class GroovyArchitecture {
 2:     static void main(String[] args) {
 3:         runArchitectureRules(new File("architecture.groovy"))
 4:     }
 5:     static void runArchitectureRules(File dsl) {
 6:         Script dslScript = new GroovyShell().parse(dsl.text)
 7: 
 8:         dslScript.metaClass = createEMC(dslScript.class, {
 9:             ExpandoMetaClass emc ->
10: 
11: 
12:         })
13:         dslScript.run()
14:     }
15: 
16:     static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
17:         ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
18: 
19:         cl(emc)
20: 
21:         emc.initialize()
22:         return emc
23:     }
24: }

さて、順番に追って行ってみましょう。私が追加したcreateEMC()メソッドはgroovy.lang.ExpandoMetaClassオブジェクトを作成し初期化してそれ自身を返します(16行目〜23行目)。初期化前にオブジェクトはClosureに渡されます(19行目)。このClosure はcreateEMC()メソッドへの引数として渡されます(8行目〜12行目)。

ここでは、ClosureをExpandoMetaClassオブジェクトをカスタマイズして、詳細な設定のためにコールバックさせるようにしています。createEMC()メソッドの戻り値は、DSL ScriptオブジェクトのmetaClassプロパティとして代入しています(8行目)。

DSL Scriptオブジェクトのrun()メソッドを、DSL Scriptを実行するために呼び出しています(13行目)。

ExpandoMetaClassクラスはGroovy1.1から導入され、それ以降メタオブジェクトプロトコルを介してカスタムメソッドの追加を許可してくれてるようになりました。言い換えれば、ExpandoMetaClassオブジェクトを他のオブジェクトのmetaClassプロパティに代入することで、あらゆるオブジェクトにあらゆるメソッドを追加できるということです。

じゃあ、こんなメソッドはどうやって追加すればいい?それにはExpandoMetaClassオブジェクトを設定する必要があります。

 1: class GroovyArchitecture {
 2:     static void main(String[] args) {
 3:         runArchitectureRules(new File("architecture.groovy"))
 4:     }
 5:     static void runArchitectureRules(File dsl) {
 6:         Script dslScript = new GroovyShell().parse(dsl.text)
 7: 
 8:         dslScript.metaClass = createEMC(dslScript.class, {
 9:             ExpandoMetaClass emc ->
10: 
11:             emc.architecture = {
12:                 Closure cl ->
13: 
14: 
15:             }
16:         })
17:         dslScript.run()
18:     }
19: 
20:     static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
21:         ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
22: 
23:         cl(emc)
24: 
25:         emc.initialize()
26:         return emc
27:     }
28: }

ExpandoMetaClassオブジェクトのarchitectureプロパティにClosureを代入しました(11行目〜15行目)。このClosureはarchitecture()メソッドの実装になり、また引数も決定されます。architectureプロパティを代入することによって、MOPを介してDSLスクリプトにメソッドを追加します。architecture(Closure) 。

なので、すでにエラーなしでこのDSLのスクリプトを実行することができます。

// architecture.groovyファイル
architecture {

}

次のステップは、構築ルールのクラスをミックスすることです。

 1: import com.seventytwomiles.architecturerules.configuration.Configuration
 2: import com.seventytwomiles.architecturerules.services.CyclicRedundancyServiceImpl
 3: import com.seventytwomiles.architecturerules.services.RulesServiceImpl
 4: 
 5: class GroovyArchitecture {
 6:     static void main(String[] args) {
 7:         runArchitectureRules(new File("architecture.groovy"))
 8:     }
 9:     static void runArchitectureRules(File dsl) {
10:         Script dslScript = new GroovyShell().parse(dsl.text)
11: 
12:         Configuration configuration = new Configuration()
13:         configuration.doCyclicDependencyTest = true
14:         configuration.throwExceptionWhenNoPackages = true
15: 
16:         dslScript.metaClass = createEMC(dslScript.class, {
17:             ExpandoMetaClass emc ->
18: 
19:             emc.architecture = {
20:                 Closure cl ->
21: 
22: 
23:             }
24:         })
25:         dslScript.run()
26: 
27:         new CyclicRedundancyServiceImpl(configuration)
28:             .performCyclicRedundancyCheck()
29:         new RulesServiceImpl(configuration).performRulesTest()
30:     }
31: 
32:     static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
33:         ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
34: 
35:         cl(emc)
36: 
37:         emc.initialize()
38:         return emc
39:     }
40: }

Configurationクラスは、構築ルールグレームワークを使って設定します。私は2つの有用なデフォルト値を追加しました(12行目〜14行目)。CyclicRedundancyServiceImplとRulesServiceImplクラスは、実際にソースコード上でチェックを行います(27行目〜29行目)。

次のステップは、クラスまたはJARファイルの場所を追加することです。私はこのようにDSLを拡張したいと思います。

// architecture.groovyファイル
architecture {
    classes "target/classes"
    jar "myLibrary.jar"
}

これらの2つのメソッドを追加するのはExpandoMetaClassを使わなくてよかったのでとてもまっすぐでした。その代わりに、delegateをClosureに代入しました。それはarchitecture()メソッド実行時に引数として渡されます。

delegateを代入する前に新しい、ArchitectureDelegateクラスを作成しなければなりません。すべてのメソッドとプロパティはClosure内部で呼び出され、ArchitectureDelegateオブジェクトに委譲されます。なので、ArchitectureDelegateクラスは2つのメソッドを提供する必要があります。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クラスのものです。次のステップはarchitecture()メソッドに渡されるClosureへArchitectureDelegateオブジェクトをdelegateとして代入することです。

 1: import com.seventytwomiles.architecturerules.configuration.Configuration
 2: import com.seventytwomiles.architecturerules.services.CyclicRedundancyServiceImpl
 3: import com.seventytwomiles.architecturerules.services.RulesServiceImpl
 4: 
 5: class GroovyArchitecture {
 6:     static void main(String[] args) {
 7:         runArchitectureRules(new File("architecture.groovy"))
 8:     }
 9:     static void runArchitectureRules(File dsl) {
10:         Script dslScript = new GroovyShell().parse(dsl.text)
11: 
12:         Configuration configuration = new Configuration()
13:         configuration.doCyclicDependencyTest = true
14:         configuration.throwExceptionWhenNoPackages = true
15: 
16:         dslScript.metaClass = createEMC(dslScript.class, {
17:             ExpandoMetaClass emc ->
18: 
19:             emc.architecture = {
20:                 Closure cl ->
21: 
22:                 cl.delegate = new ArchitectureDelegate(configuration)
23:                 cl.resolveStrategy = Closure.DELEGATE_FIRST
24: 
25:                 cl()
26:             }
27:         })
28:         dslScript.run()
29: 
30:         new CyclicRedundancyServiceImpl(configuration)
31:             .performCyclicRedundancyCheck()
32:         new RulesServiceImpl(configuration).performRulesTest()
33:     }
34: 
35:     static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
36:         ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
37: 
38:         cl(emc)
39: 
40:         emc.initialize()
41:         return emc
42:     }
43: }

ClosureのdelegateプロパティはArchitectureDelegateオブジェクトを取ります(22行目)。resolveStrategyプロパティにClosure.DELEGATE_FIRSTが設定されています(23行目)。これは、Closure内部での任意のメソッドやプロパティの呼び出しが、ArchitectureDelegateオブジェクトに委譲されることを意味しています。25行目でClosureを呼び出しています。

DSLにrules()メソッドを追加する時が来ました。

// architecture.groovyファイル
architecture {
    classes "target/classes"
    jar "myLibrary.jar"

    rules {

    }
}

どこにこのメソッドを追加するのでしょうか?もちろん、デリゲートオブジェクトですよね。

 1: import com.seventytwomiles.architecturerules.configuration.Configuration
 2: 
 3: class ArchitectureDelegate {
 4:     private Configuration configuration
 5: 
 6:     ArchitectureDelegate(Configuration configuration) {
 7:         this.configuration = configuration
 8:     }
 9: 
10:     void classes(String name) {
11:         this.configuration.addSource new SourceDirectory(name, true)
12:     }
13: 
14:     void jar(String name) {
15:         classes name
16:     }
17: 
18:     void rules(Closure cl) {
19:         cl.delegate = new RulesDelegate(configuration)
20:         cl.resolveStrategy = Closure.DELEGATE_FIRST
21: 
22:         cl()
23:     }
24: }

DSLの構文にrules()メソッドを追加するのは、rules()メソッドをArchitectureDelegateクラスに追加するのと同じくらい簡単です(18行目〜23行目)。そして、DSL内のそれぞれの新しいClosureはそれ自身のデリゲートオブジェクトを取得します。DSLのスクリプトと解析するコードをこの記事に添付しておきます。

ハッピーコーディング!

添付ファイル サイズ
architecture.groovy 490 bytes
GroovyArchitecture.groovy 3.87 KB
GroovyArchitecture.zip 5.49 MB