【備忘録】DomainObjectCollectionの一族にオブジェクトをaddする時に詰まった話【Gradleプラグイン】

# こんな感じのコードを書いてました
Gradleプラグインの開発中、以下のようなコードを書いていました。

allメソッドはクロージャ（ラムダ式？）を渡された時点でコレクションが持っているオブジェクトを流し込んでくれる上、
後から追加したオブジェクトも随時クロージャに流し込んでくれるオブザーバっぽい動きもします。

この動きを利用して、buildTypesとflavorsに更新があった際にvariantsへオブジェクトを追加するというもの。

```kotlin
open class Yakiniku(val project: Project) {
    val buildTypes: NamedDomainObjectContainer<ProjectBuildOptions> =
        project.container(ProjectBuildOptions::class.java)
    val flavors: NamedDomainObjectContainer<ProjectFlavorOptions> =
        project.container(ProjectFlavorOptions::class.java)
    val variants: DomainObjectSet<BuildVariant> =
        DefaultDomainObjectSet<BuildVariant>(BuildVariant::class.java, CollectionCallbackActionDecorator.NOOP)

    init {
        buildTypes.all { buildOption ->
            variants.add(BuildVariant(buildOption, null))
        }

        flavors.all { flavorOption ->
            buildTypes.all { buildOption ->
                variants.add(BuildVariant(buildOption, flavorOption))
            }
        }
    }
}

class BuildVariant(val build: ProjectBuildOptions?, val flavor: ProjectFlavorOptions?) : ObjBase() {
    fun getName() = listOfNotNull(build, flavor).joinToString("") { it.name.capitalize() }
    override fun toString(): String {
        return getName()
    }
}
```

で、build.gradle側からこんな感じで呼び出したかったんです。

```gradle:build.gradle
apply plugin: 'com.osm.sample.gralde.yakiniku'

yakiniku {
    buildTypes {
        debug {
            target "hogehoge"
        }
        
        release {
            target "fugafuga"
        }
    }

    flavors {
        free {
            target "piyopiyo"
        }

        paid {
            target "こんにちは！"
        }
    }

    // ★★★ここ★★★
    variants.all {
        println(it)
    }
```

"debugFree"とか"releasePaid"とかを期待していたわけなんですが…動かしてみると、**null**。
呼び出される回数は想定通り。しかし全部**null**。

あるぇ…

ソースを追跡して原因を特定するため、スタックトレースを吐き出しつつ紐解いていったところ、
以下のような箇所を見つけました。

```java:ConfigureUtil.java
    public static <T> T configure(@Nullable Closure configureClosure, T target) {
        if (configureClosure == null) {
            return target;
        } else {
            if (target instanceof Configurable) {
                ((Configurable)target).configure(configureClosure);
            } else {
                configureTarget(configureClosure, target, new ConfigureDelegate(configureClosure, target));
            }

            return target;
        }
    }

    /** 中略 */

    private static <T> void configureTarget(Closure configureClosure, T target, ConfigureDelegate closureDelegate) {
        if (!(configureClosure instanceof GeneratedClosure)) {
            (new ClosureBackedAction(configureClosure, 1, false)).execute(target);
        } else {
            Closure withNewOwner = configureClosure.rehydrate(target, closureDelegate, configureClosure.getThisObject());
            (new ClosureBackedAction(withNewOwner, 2, false)).execute(target);
        }
    }
```

このtargetという変数には、巡り巡ってaddされたオブジェクトが入ってくるようです。
これだけ見てると、targetがConfigurableじゃなければうまくいきそう。

しかし、スタックトレースを見ているとtargetがConfigurableとして判定されて、
configure()が呼ばれた形跡があったので「んん？」となったわけです。

おかしいなあと思ってクラスの定義を追ってみました。ObjBaseになんかあるのかなあ。

```kotlin
class BuildVariant(val build: ProjectBuildOptions?, val flavor: ProjectFlavorOptions?) : ObjBase() {
    fun getName() = listOfNotNull(build, flavor).joinToString("") { it.name.capitalize() }
    override fun toString(): String {
        return getName()
    }
}
```

どれどれ…

```kotlin
open class ObjBase: Configurable<Any> {
    var resolveStrategy = Closure.DELEGATE_FIRST

    override fun configure(closure: Closure<*>?): Any {
        closure?.also {
            it.delegate = this
            it.resolveStrategy = resolveStrategy
            it.run()
        }
        return this
    }
}
```

**ちくしょう！**

ObjBaseの継承をやめたところ、想定通りallメソッドに渡したクロージャの中でaddされたオブジェクトが取れました。

# まとめ
- addするオブジェクトがConfigurableの実装であるかどうかによって、DomainObjectCollection#all()の挙動が変わる。
- Configurableの実装である場合、addされたオブジェクトが持ってるconfigureが呼び出される。
- Configurableの実装でない場合、DomainObjectCollection#all()に渡したクロージャに、addされたオブジェクトが渡される