Kotlinでビルダーパターンを実装する方法


146

こんにちは、コトリンの世界の初心者です。これまで見てきたことが好きで、アプリケーションで使用するライブラリの一部をJavaからKotlinに変換しようと考え始めました。

これらのライブラリーは、setter、getter、およびBuilderクラスを備えたPojoでいっぱいです。今私はKotlinでビルダーを実装するための最良の方法を見つけましたが、成功しませんでした。

2番目の更新:問題は、Kotlinでいくつかのパラメーターを持つ単純なpojoのビルダーデザインパターンを作成する方法ですか?以下のコードは、javaコードを記述してから、eclipse-kotlin-pluginを使用してKotlinに変換するという私の試みです。

class Car private constructor(builder:Car.Builder) {
    var model:String? = null
    var year:Int = 0
    init {
        this.model = builder.model
        this.year = builder.year
    }
    companion object Builder {
        var model:String? = null
        private set

        var year:Int = 0
        private set

        fun model(model:String):Builder {
            this.model = model
            return this
        }
        fun year(year:Int):Builder {
            this.year = year
            return this
        }
        fun build():Car {
            val car = Car(this)
            return car
        }
    }
}

1
変更可能である必要がmodelありyearますか?Car作成後に変更しますか?
voddan

はい、不変でなければならないでしょう。また、両方が設定されており、空ではないことを確認したい
Keyhan

1
このgithub.com/jffiorillo/jvmbuilder注釈プロセッサを使用して、ビルダークラスを自動的に生成することもできます。
JoseF 2018

@JoseF標準のkotlinに追加することをお勧めします。これは、kotlinで作成されたライブラリに役立ちます。
Keyhan、

回答:


272

何よりもまず、デフォルトと名前付きの引数があるため、ほとんどの場合、Kotlinでビルダーを使用する必要はありません。これにより、

class Car(val model: String? = null, val year: Int = 0)

そしてそれを次のように使用します:

val car = Car(model = "X")

どうしてもビルダーを使用したい場合は、次の方法で実行できます。

sはシングルトンcompanion objectなので、ビルダーをaにしても意味がありませんobject。代わりに、ネストされたクラスとして宣言します(Kotlinではデフォルトで静的です)。

プロパティをコンストラクターに移動して、オブジェクトも通常の方法でインスタンス化できるようにし(必要でない場合はコンストラクターをプライベートにします)、ビルダーを取得してプライマリコンストラクターに委任するセカンダリコンストラクターを使用します。コードは次のようになります。

class Car( //add private constructor if necessary
        val model: String?,
        val year: Int
) {

    private constructor(builder: Builder) : this(builder.model, builder.year)

    class Builder {
        var model: String? = null
            private set

        var year: Int = 0
            private set

        fun model(model: String) = apply { this.model = model }

        fun year(year: Int) = apply { this.year = year }

        fun build() = Car(this)
    }
}

使用法: val car = Car.Builder().model("X").build()

このコードは、ビルダーDSLを使用してさらに短縮できます。

class Car (
        val model: String?,
        val year: Int
) {

    private constructor(builder: Builder) : this(builder.model, builder.year)

    companion object {
        inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
    }

    class Builder {
        var model: String? = null
        var year: Int = 0

        fun build() = Car(this)
    }
}

使用法: val car = Car.build { model = "X" }

一部の値が必要で、デフォルト値がない場合は、ビルダーのコンストラクターとbuild定義したメソッドにそれらを配置する必要があります。

class Car (
        val model: String?,
        val year: Int,
        val required: String
) {

    private constructor(builder: Builder) : this(builder.model, builder.year, builder.required)

    companion object {
        inline fun build(required: String, block: Builder.() -> Unit) = Builder(required).apply(block).build()
    }

    class Builder(
            val required: String
    ) {
        var model: String? = null
        var year: Int = 0

        fun build() = Car(this)
    }
}

使用法: val car = Car.build(required = "requiredValue") { model = "X" }


2
特にありませんが、質問の作成者はビルダーパターンの実装方法を具体的に尋ねました。
Kirill Rakhman 16年

4
自分で修正する必要があります。ビルダーパターンにはいくつかの利点があります。たとえば、部分的に構築されたビルダーを別のメソッドに渡すことができます。しかし、あなたは正しい、私は発言を追加します。
Kirill Rakhman

3
@KirillRakhman Javaからビルダーを呼び出すのはどうですか?ビルダーをJavaで使用できるようにする簡単な方法はありますか?
Keyhan 2016

6
3つのバージョンはすべて、次のようにJavaから呼び出すことができますCar.Builder builder = new Car.Builder();。ただし、流暢なインターフェイスを備えているのは最初のバージョンのみであるため、2番目と3番目のバージョンへの呼び出しは連鎖できません。
Kirill Rakhman

10
上のKotlinの例は、考えられる1つの使用例のみを説明していると思います。私がビルダーを使用する主な理由は、可変オブジェクトを不変オブジェクトに変換することです。つまり、「構築」している間に時間をかけて変異させ、不変オブジェクトを作成する必要があります。少なくとも私のコードには、パラメーターのバリエーションが非常に多く、いくつかの異なるコンストラクターの代わりにビルダーを使用するコードの例が1つまたは2つしかありません。しかし、不変オブジェクトを作成するために、ビルダーが間違いなく私が考えることができる最もクリーンな方法である場合がいくつかあります。
ycomp 2017年

20

1つのアプローチは、次のようなことです。

class Car(
  val model: String?,
  val color: String?,
  val type: String?) {

    data class Builder(
      var model: String? = null,
      var color: String? = null,
      var type: String? = null) {

        fun model(model: String) = apply { this.model = model }
        fun color(color: String) = apply { this.color = color }
        fun type(type: String) = apply { this.type = type }
        fun build() = Car(model, color, type)
    }
}

使用例:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()

どうもありがとう!あなたは私の日を作りました!あなたの答えはSOLUTIONとしてマークされるべきです。
sVd

9

JSONからオブジェクトを解析するためにJacksonライブラリーを使用しているため、空のコンストラクターが必要であり、オプションのフィールドを持つことはできません。また、すべてのフィールドは変更可能でなければなりません。次に、ビルダーパターンと同じことを行うこの構文を使用できます。

val car = Car().apply{ model = "Ford"; year = 2000 }

8
ジャクソンでは、空のコンストラクタを実際に持つ必要はなく、フィールドは変更可能である必要はありません。コンストラクタのパラメータに次の注釈を付けるだけです@JsonProperty
Bastian Voigt

2
スイッチで@JsonPropertyコンパイルすれば、もう注釈を付ける必要はありません-parameters
アミールアビリ2018年

2
ジャクソンは、実際にはビルダーを使用するように構成できます。
Keyhan 2018年

1
jackson-module-kotlinモジュールをプロジェクトに追加すると、データクラスを使用するだけで機能します。
Nils Breunese

2
これはビルダーパターンと同じことをどのように行うのですか?最終製品をインスタンス化してから、情報をスワップアウト/追加しています。Builderパターンの要点は、必要な情報がすべて揃うまで最終製品を入手できないことです。.apply()を削除すると、未定義の車が残ります。Builderからすべてのコンストラクター引数を削除すると、Car Builderが残ります。それを車に構築しようとすると、モデルと年がまだ指定されていないために例外が発生する可能性があります。彼らは同じものではありません。
ZeroStatic

7

私は個人的にKotlinでビルダーを見たことがありませんが、多分それは私だけです。

必要なすべての検証はinitブロックで行われます。

class Car(val model: String,
          val year: Int = 2000) {

    init {
        if(year < 1900) throw Exception("...")
    }
}

ここで私はあなたが本当に望んでいたしないことを推測するために自由を取ったmodelし、year変更可能に。また、これらのデフォルト値は意味がないように見えますが(特にのnull場合name)、デモのために残しました。

意見: 名前付きパラメーターなしで生きるための手段としてJavaで使用されるビルダーパターン。名前付きパラメーターを持つ言語(KotlinやPythonなど)では、(おそらくオプションの)パラメーターの長いリストを持つコンストラクターを用意することをお勧めします。


2
答えてくれてありがとう。私はあなたのアプローチが好きですが、欠点は、多くのパラメーターを持つクラスの場合、コンストラクターを使用してクラスをテストすることはそれほどフレンドリーではなくなります。
Keyhan 2016年

1
+検証はフィールド間で行われないと仮定して、検証を実行する他の2つの方法です。1)セッターが検証を行うプロパティデリゲートを使用します。これは、検証を行う通常のセッターを使用するのとほとんど同じです2)回避原始的な執着とそれらを検証するために渡す新しいタイプを作成します。
Jacob Zimmerman

1
@KeyhanこれはPythonの古典的なアプローチで、数十の引数を持つ関数でも非常にうまく機能します。ここでの秘訣は名前付き引数を使用することです(Javaでは使用できません!)
voddan

1
はい、それは使用する価値のあるソリューションでもあります。ビルダークラスにいくつかの明確な利点があるjavaとは異なり、Kotlinではそれほど明白ではありません。C#開発者と話しました。C#にもKotlinのような機能があります(デフォルト値で、パラメーターに名前を付けることができます)コンストラクターの呼び出し)ビルダーパターンも使用しませんでした。
Keyhan 2016年

1
@ vxh.vietこのようなケースの多くはで解決することができ@JvmOverloads kotlinlang.org/docs/reference/...
voddan

4

ビルダーとして余分な楽しみを宣言する多くの例を見てきました。私は個人的にこのアプローチが好きです。ビルダーを書く手間を省きます。

package android.zeroarst.lab.koltinlab

import kotlin.properties.Delegates

class Lab {
    companion object {
        @JvmStatic fun main(args: Array<String>) {

            val roy = Person {
                name = "Roy"
                age = 33
                height = 173
                single = true
                car {
                    brand = "Tesla"
                    model = "Model X"
                    year = 2017
                }
                car {
                    brand = "Tesla"
                    model = "Model S"
                    year = 2018
                }
            }

            println(roy)
        }

        class Person() {
            constructor(init: Person.() -> Unit) : this() {
                this.init()
            }

            var name: String by Delegates.notNull()
            var age: Int by Delegates.notNull()
            var height: Int by Delegates.notNull()
            var single: Boolean by Delegates.notNull()
            val cars: MutableList<Car> by lazy { arrayListOf<Car>() }

            override fun toString(): String {
                return "name=$name, age=$age, " +
                        "height=$height, " +
                        "single=${when (single) {
                            true -> "looking for a girl friend T___T"
                            false -> "Happy!!"
                        }}\nCars: $cars"
            }
        }

        class Car() {

            var brand: String by Delegates.notNull()
            var model: String by Delegates.notNull()
            var year: Int by Delegates.notNull()

            override fun toString(): String {
                return "(brand=$brand, model=$model, year=$year)"
            }
        }

        fun Person.car(init: Car.() -> Unit): Unit {
            cars.add(Car().apply(init))
        }

    }
}

例外をスローする代わりにエラーを表示するなど、一部のフィールドをDSLで強制的に初期化する方法をまだ見つけていません。誰かが知っているなら教えてください。


2

単純なクラスの場合、別個のビルダーは必要ありません。Kirill Rakhmanが説明したように、オプションのコンストラクタ引数を使用できます。

より複雑なクラスがある場合、KotlinはGroovyスタイルのビルダー/ DSLを作成する方法を提供します。

タイプセーフなビルダー

次に例を示します。

Githubの例-ビルダー/アセンブラ


おかげで、Javaからも使用することを考えていました。私の知る限り、オプションの引数はJavaでは機能しません。
Keyhan


1

パーティーに遅れました。プロジェクトでビルダーパターンを使用する必要がある場合も、同じジレンマに遭遇しました。後で調査したところ、Kotlinはすでに名前付き引数とデフォルト引数を提供しているため、これは絶対に不要であることに気付きました。

本当に実装する必要がある場合、Kirill Rakhmanの答えは、最も効果的な方法で実装する方法に関する確かな答えです。あなたがそれが便利であると思うかもしれないもう一つはhttps://www.baeldung.com/kotlin-builder-patternであり、JavaとKotlinとの実装を比較することができます


0

Kotlinでもパターンと実装はほとんど同じです。デフォルト値のおかげでそれをスキップできることもありますが、より複雑なオブジェクトの作成では、ビルダーは省略できない便利なツールです。


デフォルト値を持つコンストラクターについては、初期化ブロックを使用して入力の検証を行うこともできます。ただし、ステートフルなものが必要な場合(事前にすべてを指定する必要がないようにするため)は、ビルダーパターンが適切です。
mfulton26 2016年

コードの簡単な例を教えていただけませんか?電子メールの検証を行う名前と電子メールフィールドを持つ単純なUserクラスを言います。
Keyhan 2016年

0

kotlinの例ではオプションのパラメーターを使用できます。

fun myFunc(p1: String, p2: Int = -1, p3: Long = -1, p4: String = "default") {
    System.out.printf("parameter %s %d %d %s\n", p1, p2, p3, p4)
}

その後

myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")

0
class Foo private constructor(@DrawableRes requiredImageRes: Int, optionalTitle: String?) {

    @DrawableRes
    @get:DrawableRes
    val requiredImageRes: Int

    val optionalTitle: String?

    init {
        this.requiredImageRes = requiredImageRes
        this.requiredImageRes = optionalTitle
    }

    class Builder {

        @DrawableRes
        private var requiredImageRes: Int = -1

        private var optionalTitle: String? = null

        fun requiredImageRes(@DrawableRes imageRes: Int): Builder {
            this.intent = intent
            return this
        } 

        fun optionalTitle(title: String): Builder {
            this.optionalTitle = title
            return this
        }

        fun build(): Foo {
            if(requiredImageRes == -1) {
                throw IllegalStateException("No image res provided")
            }
            return Foo(this.requiredImageRes, this.optionalTitle)
        }

    }

}

0

基本的なビルダーパターンをKotlinに次のコードで実装しました。

data class DialogMessage(
        var title: String = "",
        var message: String = ""
) {


    class Builder( context: Context){


        private var context: Context = context
        private var title: String = ""
        private var message: String = ""

        fun title( title : String) = apply { this.title = title }

        fun message( message : String ) = apply { this.message = message  }    

        fun build() = KeyoDialogMessage(
                title,
                message
        )

    }

    private lateinit var  dialog : Dialog

    fun show(){
        this.dialog= Dialog(context)
        .
        .
        .
        dialog.show()

    }

    fun hide(){
        if( this.dialog != null){
            this.dialog.dismiss()
        }
    }
}

そして最後に

Java:

new DialogMessage.Builder( context )
       .title("Title")
       .message("Message")
       .build()
       .show();

コトリン:

DialogMessage.Builder( context )
       .title("Title")
       .message("")
       .build()
       .show()

0

私は、Javaクライアントが消費するAPIを公開するKotlinプロジェクトに取り組んでいました(Kotlin言語構造を利用できない)。Javaで使用できるようにビルダーを追加する必要があったため、@ Builderアノテーションを作成しました:https : //github.com/ThinkingLogic/kotlin-builder-annotation- これは、基本的にはKotlinのLombok @Builderアノテーションの代わりです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.