Bagaimana cara menerapkan pola Builder di Kotlin?

146

Hai, saya seorang pemula di dunia Kotlin. Saya suka apa yang saya lihat sejauh ini dan mulai berpikir untuk mengubah beberapa perpustakaan kami yang kami gunakan dalam aplikasi kami dari Jawa ke Kotlin.

Perpustakaan ini penuh dengan Pojo dengan setter, getter dan kelas Builder. Sekarang saya telah mencari di Google apa cara terbaik untuk mengimplementasikan Builder di Kotlin tetapi tidak berhasil.

Pembaruan ke-2: Pertanyaannya adalah bagaimana menulis pola desain Builder untuk pojo sederhana dengan beberapa parameter di Kotlin? Kode di bawah ini adalah usaha saya dengan menulis kode java dan kemudian menggunakan eclipse-kotlin-plugin untuk mengkonversi ke 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
        }
    }
}
Keyhan
sumber
1
Apakah Anda perlu modeldan yearbisa berubah? Apakah Anda mengubahnya setelah Carpenciptaan?
voddan
Saya kira mereka harus abadi ya. Anda juga ingin memastikan keduanya disetel dan tidak kosong
Keyhan
1
Anda juga dapat menggunakan Prosesor Anotasi Catatan github.com/jffiorillo/jvmbuilder ini untuk menghasilkan kelas builder secara otomatis untuk Anda.
JoseF
@ Joseose Ide bagus untuk menambahkannya ke kotlin standar. Ini berguna untuk perpustakaan yang ditulis dalam kotlin.
Keyhan

Jawaban:

273

Pertama dan terutama, dalam banyak kasus Anda tidak perlu menggunakan pembangun di Kotlin karena kami memiliki argumen default dan bernama. Ini memungkinkan Anda untuk menulis

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

dan gunakan seperti ini:

val car = Car(model = "X")

Jika Anda benar-benar ingin menggunakan pembuat, berikut ini cara melakukannya:

Membuat Builder menjadi companion objecttidak masuk akal karena objects adalah lajang. Alih-alih mendeklarasikannya sebagai kelas bersarang (yang statis secara default di Kotlin).

Pindahkan properti ke konstruktor sehingga objek juga dapat dipakai dengan cara biasa (jadikan konstruktor sebagai pribadi jika tidak seharusnya) dan gunakan konstruktor sekunder yang mengambil pembangun dan mendelegasikannya ke konstruktor utama. Kode akan terlihat sebagai berikut:

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)
    }
}

Pemakaian: val car = Car.Builder().model("X").build()

Kode ini juga dapat dipersingkat dengan menggunakan DSL pembangun :

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)
    }
}

Pemakaian: val car = Car.build { model = "X" }

Jika beberapa nilai diperlukan dan tidak memiliki nilai default, Anda harus meletakkannya di konstruktor pembangun dan juga dalam buildmetode yang baru saja kita tetapkan:

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)
    }
}

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

Kirill Rakhman
sumber
2
Tidak ada, tetapi penulis pertanyaan secara khusus bertanya bagaimana menerapkan pola pembangun.
Kirill Rakhman
4
Saya harus memperbaiki sendiri, pola pembangun memiliki beberapa keuntungan, misalnya Anda dapat meneruskan pembangun yang dibangun sebagian ke metode lain. Tapi Anda benar, saya akan menambahkan komentar.
Kirill Rakhman
3
@ KirillRakhman bagaimana memanggil pembangun dari java? Apakah ada cara mudah untuk membuat pembangun tersedia untuk java?
Keyhan
6
Semua tiga versi dapat dipanggil dari Jawa suka begitu: Car.Builder builder = new Car.Builder();. Namun hanya versi pertama yang memiliki antarmuka yang lancar sehingga panggilan ke versi kedua dan ketiga tidak dapat dirantai.
Kirill Rakhman
10
Saya pikir contoh kotlin di atas hanya menjelaskan satu kemungkinan penggunaan. Alasan utama saya menggunakan pembangun adalah untuk mengubah objek yang bisa berubah menjadi yang tidak berubah. Yaitu, saya perlu mengubahnya dari waktu ke waktu ketika saya "membangun" dan kemudian datang dengan objek yang tidak dapat diubah. Setidaknya dalam kode saya hanya ada satu atau 2 contoh kode yang memiliki begitu banyak variasi parameter yang akan saya gunakan pembangun daripada beberapa konstruktor yang berbeda. Tetapi untuk membuat objek yang kekal, saya memiliki beberapa kasus di mana seorang pembangun jelas merupakan cara terbersih yang dapat saya pikirkan.
ycomp
21

Salah satu pendekatan adalah melakukan sesuatu seperti berikut:

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)
    }
}

Sampel penggunaan:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()
Dmitrii Bychkov
sumber
Terima kasih banyak! Anda membuat hari saya! Jawaban Anda harus ditandai sebagai SOLUSI.
sVd
9

Karena saya menggunakan perpustakaan Jackson untuk mem-parsing objek dari JSON, saya harus memiliki konstruktor kosong dan saya tidak bisa memiliki bidang opsional. Juga semua bidang harus bisa berubah. Maka saya bisa menggunakan sintaks yang bagus ini yang melakukan hal yang sama dengan pola Builder:

val car = Car().apply{ model = "Ford"; year = 2000 }
David Vávra
sumber
8
Di Jackson, Anda sebenarnya tidak perlu memiliki konstruktor kosong, dan bidang tidak harus bisa diubah. Anda hanya perlu membubuhi keterangan parameter konstruktor Anda dengan@JsonProperty
Bastian Voigt
2
Anda bahkan tidak perlu membubuhi keterangan @JsonPropertylagi, jika Anda mengkompilasi dengan -parameterssakelar.
Amir Abiri
2
Jackson sebenarnya dapat dikonfigurasi untuk menggunakan pembangun.
Keyhan
1
Jika Anda menambahkan modul jackson-module-kotlin ke proyek Anda, Anda bisa menggunakan kelas data dan itu akan berfungsi.
Nils Breunese
2
Bagaimana ini melakukan hal yang sama dengan Pola Builder? Anda membuat instance produk akhir dan kemudian menukar / menambahkan informasi. Inti dari pola Builder adalah untuk tidak bisa mendapatkan produk akhir sampai semua informasi yang diperlukan ada. Menghapus .apply () memberi Anda mobil yang tidak ditentukan. Menghapus semua argumen konstruktor dari Builder membuat Anda memiliki Car Builder, dan jika Anda mencoba membuatnya menjadi mobil, Anda kemungkinan akan mengalami pengecualian karena tidak menentukan model dan tahun. Mereka bukan hal yang sama.
ZeroStatic
7

Saya pribadi belum pernah melihat tukang bangunan di Kotlin, tapi mungkin hanya saya.

Semua validasi yang dibutuhkan terjadi di initblok:

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

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

Di sini saya mengambil kebebasan untuk menebak bahwa Anda tidak benar-benar ingin modeldan yeardapat berubah. Juga nilai-nilai default itu tampaknya tidak masuk akal, (terutama nulluntuk name) tetapi saya meninggalkannya untuk tujuan demonstrasi.

Opini: Pola pembangun yang digunakan di Jawa sebagai sarana untuk hidup tanpa parameter bernama. Dalam bahasa dengan parameter bernama (seperti Kotlin atau Python) adalah praktik yang baik untuk memiliki konstruktor dengan daftar panjang (mungkin opsional) parameter.

voddan
sumber
2
Terima kasih banyak atas jawabannya. Saya suka pendekatan Anda tetapi downside adalah untuk kelas dengan banyak parameter menjadi tidak ramah untuk menggunakan konstruktor dan juga menguji kelas.
Keyhan
1
+ Keydan dua cara lain yang dapat Anda lakukan validasi, dengan asumsi validasi tidak terjadi di antara bidang: 1) gunakan delegasi properti tempat setter melakukan validasi - ini hampir sama dengan memiliki setter normal yang melakukan validasi 2) Hindari obsesi primitif dan membuat jenis baru untuk lulus yang memvalidasi diri mereka sendiri
Jacob Zimmerman
1
@Khanhan ini adalah pendekatan klasik dengan Python, ia bekerja dengan sangat baik bahkan untuk fungsi dengan puluhan argumen. Kuncinya di sini adalah dengan menggunakan argumen bernama (tidak tersedia di Jawa!)
voddan
1
Ya, itu juga solusi yang layak digunakan, sepertinya tidak seperti java di mana kelas builder memiliki beberapa keuntungan yang jelas, di Kotlin tidak begitu jelas, berbicara dengan pengembang C #, C # juga memiliki fitur seperti kotlin (nilai default dan Anda dapat menamai params ketika memanggil konstruktor) mereka tidak menggunakan pola pembangun juga.
Keyhan
1
@ vxh.viet banyak dari kasus-kasus semacam itu dapat diselesaikan dengan @JvmOverloads kotlinlang.org/docs/reference/…
voddan
4

Saya telah melihat banyak contoh yang menyatakan kesenangan ekstra sebagai pembangun. Saya pribadi menyukai pendekatan ini. Simpan upaya untuk menulis pembangun.

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))
        }

    }
}

Saya belum menemukan cara yang dapat memaksa beberapa bidang diinisialisasi dalam DSL seperti menunjukkan kesalahan alih-alih melempar pengecualian. Beri tahu saya jika ada yang tahu.

Arst
sumber
2

Untuk kelas sederhana Anda tidak perlu pembangun yang terpisah. Anda dapat menggunakan argumen konstruktor opsional seperti yang dijelaskan Kirill Rakhman.

Jika Anda memiliki kelas yang lebih kompleks maka Kotlin menyediakan cara untuk membuat Builder / DSL gaya Groovy:

Pembangun Tipe-Aman

Berikut ini sebuah contoh:

Contoh Github - Builder / Assembler

Dariusz Bacinski
sumber
Terima kasih, tapi saya juga berpikir untuk menggunakannya dari java. Sejauh yang saya tahu argumen opsional tidak akan berfungsi dari java.
Keyhan
1

Saya terlambat ke pesta. Saya juga mengalami dilema yang sama jika saya harus menggunakan pola Builder dalam proyek tersebut. Kemudian, setelah penelitian saya menyadari itu sama sekali tidak perlu karena Kotlin sudah memberikan argumen bernama dan argumen default.

Jika Anda benar-benar perlu menerapkan, jawaban Kirill Rakhman adalah jawaban yang solid tentang cara menerapkan dengan cara yang paling efektif. Hal lain yang mungkin bermanfaat bagi Anda adalah https://www.baeldung.com/kotlin-builder-pattern yang dapat Anda bandingkan dan kontras dengan Java dan Kotlin dalam penerapannya

Farruh Habibullaev
sumber
0

Saya akan mengatakan pola dan implementasinya tetap sama di Kotlin. Anda kadang-kadang dapat melewatinya berkat nilai default, tetapi untuk pembuatan objek yang lebih rumit, pembangun masih merupakan alat yang berguna yang tidak dapat dihilangkan.

Ritave
sumber
Sejauh konstruktor dengan nilai default Anda bahkan dapat melakukan validasi input menggunakan blok penginisialisasi . Namun, jika Anda membutuhkan sesuatu yang stateful (sehingga Anda tidak harus menentukan semuanya di muka) maka pola builder masih merupakan cara yang harus dilakukan.
mfulton26
Bisakah Anda memberi saya contoh sederhana dengan kode? Ucapkan kelas Pengguna sederhana dengan bidang nama dan email dengan validasi untuk email.
Keyhan
0

Anda dapat menggunakan parameter opsional di kotlin contoh:

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)
}

kemudian

myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")
vuhung3990
sumber
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)
        }

    }

}
Brandon Rude
sumber
0

Saya menerapkan pola dasar Builder di Kotlin dengan kode berikut:

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()
        }
    }
}

Dan akhirnya

Jawa:

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

Kotlin:

DialogMessage.Builder( context )
       .title("Title")
       .message("")
       .build()
       .show()
Moises Portillo
sumber
0

Saya sedang mengerjakan proyek Kotlin yang mengekspos API yang dikonsumsi oleh klien Java (yang tidak bisa mengambil keuntungan dari konstruksi bahasa Kotlin). Kami harus menambahkan pembangun agar dapat digunakan di Jawa, jadi saya membuat anotasi @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - ini pada dasarnya merupakan pengganti anotasi Lombok @Builder untuk Kotlin.

YetAnotherMatt
sumber