1
0

More than 3 years have passed since last update.

Groovy の delegate 機能を利用した Gradle の内部処理について考える

Last updated at Posted at 2021-01-05

Gradle の build.gradle が、読めない、書けない、わからないの3拍子整った状態のまま、闇雲にサンプルソースをコピペする危機的生活を脱する試みの一環。

build.gradle の読み書きに必要な Groovy の基本機能については、Qiita の既存記事に大変すばらしい説明があります。その辺りの説明については、それらの記事にお任せします。

私の場合、 Groovy に関する理解があまりに貧弱で、 delegate まわりのところがどうしても消化不良でした。以下、それを解消するための、追加調査のメモ書きです。

前提(読むにあたって)

  • Groovy の基本文法について、ある程度知っていること
  • Gradle のビルドスクリプト build.gradle は Groovy 準拠であることを知っていること
  • とはいえ、 build.gradle の “入れ子の { } ” のコードを Groovy でどう解釈すれば読めるのかわからない
  • Groovy の delegate 機能が、いまいちピンとこない

build.gradle にスクリプトを書く意味

当然といえば当然ですが、私たちが build.gradle にスクリプトを書くのは、私たちが Gradle に「やってほしい」と思っていることを、実際に Gradle にやってもらうためです。

Gradle は処理実行中、必要な設定情報をインスタンスに保持しているようです。 Gradle はこれらインスタンスに設定されている通りに動作しますから、 Gradle に私たちが意図した処理を実行させるには、これら Gradle のインスタンスのフィールド値を書き換える必要があります。

私たちが「 build.gradle にスクリプトを書く」意味はここにあります。 build.gradle に書かれたスクリプトは Gradle に実行されるわけですが、その結果として、上述した Gradle 内のインスタンスのフィールド値が更新されるようになっています。

なぜ delegate なのかがわからない

いろいろ調べたところ、どうやら Gradle 内の上記処理が、 Groovy の delegate 機能で実現されているようです。

しかし私は Gradle どころか Groovy のグの字も知らない無知無知プログラマです。 そう言われても「はい、そうですか」と即座に納得できようもありません。

仕方がないので、その辺りについて調べました。

delegate で、うまくやる仕組み

下図は、 Gradle が Groovy の delegate 機能でうまくやる様子を示しています。

groovy-closure-image.png

クロージャは this, owner, delegate という暗黙的な変数を持っています。それぞれ「クロージャが、どのインスタンスに所属するか」を示すようなもので、これら3つの変数に格納されたインスタンスには、クロージャからアクセス可能です。
乱暴に言ってしまえば、オブジェクト指向言語での自クラスにあたるのが this で、スーパークラスにあたるのが owner & delegate みたいな感じです。

これらについて、 this と owner は変更不可ですが、 delegate は変更可となっています。
クロージャの delegate を差し替えると、「クロージャの owner を別のインスタンスに差し替える」ようなことができます。この機能を利用すれば、 Gradle 内で下記のような処理を実現できます。

クロージャをよこしな!
そうすれば、そいつで Gradle 内部のインスタンスを更新してやんよ!

私たちは、build.gradle に Gradle の設定情報を更新するクロージャを定義すればよいのです。
そのクロージャは Gradle に届き、 Gradle が実行してくれます。その際 Gradle は、私たちが作成したクロージャの delegate を、 Gradle が保持する設定情報を管理するインスタンスに差し替えたうえで実行 します。

私たちがビルドスクリプトに「 Gradle の設定情報を持つインスタンスの、○○フィールドの値を変更する」というクロージャを書いておけば、それだけでうまくいくよう Gradle の内部処理が作られています。

巷でよく見かける「 build.gradle に、これさえ書けばうまくいくよ!」という類のサンプルコードの中身は、ほぼ例外なく、そういうクロージャです。

試してみる

以上が「 Gradle が Groovy の delegate 機能でうまくやる仕組み」ですが、実際のところ、何をどうやっているのでしょうか。

気になったので、「スクリプトの内部インスタンスのフィールド値を、第三者が作成したクロージャで更新する」処理の、再現を試みました。

※実際に Gradle がこの通りにやっているかどうかは、定かでありません。あくまで、同じようなことを Groovy の delegate でできるかどうかの確認です。

目標は、以下のような感じのインスタンスを用意して、

delegate_tutorial.groovy
class JavaCompilerConfig {
    def option = ""  // これが設定項目
}
class JavaConfig {
    def homeDir = ""  // これが設定項目
    def JavaCompilerConfig compilerConfig = new JavaCompilerConfig()   // 設定項目も持つインスタンスの、内部インスタンス
}

// ★★ これが「設定情報を持つ、インスタンス」ということにする ★★
javaConfig = new JavaConfig()  // これが更新対象

以下のようなスクリプトで、インスタンスのフィールド値を更新することです。

// このスクリプトで javaConfig 変数のインスタンスを更新する
java {
    homeDir = "path"
    compileOption {
        option = "option"
    }
}

サンプル

後述のとおりです。

コード中の「内部処理」部には、内部インスタンスと、それを更新する処理が定義されています。「我々のスクリプト」部には、内部処理を呼び出す処理が定義されています。

巷でよく見かける、私たちがよくコピペして使用するコードにあたるものが「我々のビルドスクリプト」です。そこから呼び出している「内部処理」は Gradle の内部処理…のつもりです。

サンプルから、スクリプトが内部に持つインスタンス(サンプル中の javaConfig 変数)のフィールド値を、「我々のビルドスクリプト」で書き換えられることがわかります。

delegate_tutorial.groovy
// 「Java の設定情報を持つ内部のインスタンスを、我々のクロージャで更新する」
// という感じの処理を作ってみた。
//
// ----- 内部処理 -----
class JavaCompilerConfig {
    def option = ""
}
class JavaConfig {
    def homeDir = ""
    def JavaCompilerConfig compilerConfig = new JavaCompilerConfig()

    def compileOption(Closure clos) { 
        clos.resolveStrategy = Closure.DELEGATE_FIRST
        clos.delegate = compilerConfig   
        clos()
    }
}
// これが「設定情報を持つ、インスタンス」ということにする
javaConfig = new JavaConfig() 

def java(Closure clos) {
    clos.resolveStrategy = Closure.DELEGATE_FIRST
    clos.delegate = javaConfig
    clos()
}


// ----- 我々のビルドスクリプト -----
java {
    homeDir = "path"
    compileOption {
        option = "option"
    }
}

// (確認用の処理:ビルドスクリプトの設定が、各インスタンスに反映されていること)
assert javaConfig.homeDir == "path"
assert javaConfig.compilerConfig.option == "option"

余計なコメント付き
delegate_tutorial.groovy
// 「Java の設定情報を持つ内部のインスタンスを、我々のクロージャで更新する」
// という処理を作ってみた。
//
// ----- 内部処理 -----
// 設定情報を管理するクラス
class JavaCompilerConfig {
    def option = ""
}
class JavaConfig {
    def homeDir = ""
    // ビルドスクリプトが「入れ子のクロージャ」だから、
    // このインスタンスも JavaConfig に持たせておくのが自然かなと思った。
    // こうしなければならない、というわけではない。
    def JavaCompilerConfig compilerConfig = new JavaCompilerConfig()

    // 処理(クロージャ)を受け取って、「インスタンスのフィールドを更新する処理」を実行。
    // JavaConfig クラス内に定義しておけば、
    // 「java() の引数として設定するクロージャ内の 
    // "入れ子のクロージャ" となっていないと呼び出せない」という
    // 制約を表現できる。
    def compileOption(Closure clos) {
        // クロージャへの「delegate を最優先しろ」という命令
        clos.resolveStrategy = Closure.DELEGATE_FIRST  
        clos.delegate = compilerConfig   
        clos()
    }
}
javaConfig = new JavaConfig();  // これが「設定情報を持つ、インスタンス」

// 「インスタンスのフィールドを更新する処理(クロージャ)」を受け取って、実行。
// 実行前に、クロージャの delegate を javaConfig インスタンスに差し替えるのがポイント。
// こうすると、クロージャが「javaConfig インスタンスのフィールドやメソッド」に
// 直接アクセスできるようになる。
// イメージ的には「親クラスを、プログラム実行中に、後付けする」ような感じ。
// ダックタイピングを許容する言語だからこそ、できること。
def java(Closure clos) {
    // クロージャへの「delegate を最優先しろ」という命令
    clos.resolveStrategy = Closure.DELEGATE_FIRST
    clos.delegate = javaConfig
    clos()
}


// ----- 我々のビルドスクリプト -----
java {
    homeDir = "path"
    compileOption {
        option = "option"
    }
}

// これは無理
// compileOption {
//        option = "option"
// }

// (確認用の処理:ビルドスクリプトの設定が、各インスタンスに反映されていること)
assert javaConfig.homeDir == "path"
assert javaConfig.compilerConfig.option == "option"

蛇足:なぜ Groovy だったのか

いろいろ調べてみて、 Gradle が Groovy を採用したのは、以下のようなことができるからだったのかな、と解釈しました。

  • Java とシームレス
  • その気になれば、ビルドスクリプトに直接 Java でコーディング可
  • ビルドスクリプトを、こういう↓ふうに書ける
build.gradle
eclipse {
    project {
        name = 'Sample'
    }
}

↑のようなビルドスクリプトで、 Gradle が動くようにできたら素敵! という想いが先にあって、そのための最もよい手段が Groovy だったのかな、と私は解釈しました。

ともかくこれで、以前よりは安心して build.gradle にスクリプトを書けそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0