วิธีการนำรูปแบบตัวสร้างไปใช้ใน Kotlin


146

สวัสดีฉันเป็นมือใหม่ในโลก Kotlin ฉันชอบสิ่งที่ฉันเห็นและเริ่มคิดที่จะแปลงห้องสมุดของเราบางส่วนที่เราใช้ในแอปพลิเคชันของเราจาก Java เป็น Kotlin

ไลบรารีเหล่านี้เต็มไปด้วย Pojos ด้วย setters, getters และคลาส Builder ตอนนี้ฉันได้ค้นหาวิธีที่ดีที่สุดในการใช้ Builders ใน Kotlin แต่ไม่ประสบความสำเร็จ

อัปเดตครั้งที่ 2: คำถามคือวิธีเขียนแบบการออกแบบ Builder สำหรับ pojo แบบง่ายพร้อมพารามิเตอร์บางอย่างใน Kotlin รหัสด้านล่างคือความพยายามของฉันโดยการเขียนโค้ดจาวาแล้วใช้ 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 Annotation Processor นี้เพื่อสร้างคลาส builder ให้คุณโดยอัตโนมัติ
JoseF

@JoseF ความคิดที่ดีที่จะเพิ่มไปยัง kotlin มาตรฐาน มันมีประโยชน์สำหรับไลบรารีที่เขียนด้วย kotlin
Keyhan

คำตอบ:


273

ก่อนอื่นในกรณีส่วนใหญ่คุณไม่จำเป็นต้องใช้ผู้สร้างใน Kotlin เพราะเรามีอาร์กิวเมนต์เริ่มต้นและชื่อ สิ่งนี้ทำให้คุณสามารถเขียน

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

และใช้มันอย่างนั้น:

val car = Car(model = "X")

หากคุณต้องการใช้ผู้สร้างจริง ๆ นี่คือวิธีที่คุณทำได้:

การทำให้ตัวสร้าง a companion objectไม่สมเหตุสมผลเนื่องจากobjects เป็นซิงเกิลตัน แทนที่จะประกาศว่าเป็นคลาสที่ซ้อนกัน (ซึ่งเป็นค่าคงที่ตามค่าเริ่มต้นใน 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

4
ฉันควรแก้ไขตัวเองรูปแบบการสร้างมีข้อดีบางอย่างเช่นคุณสามารถส่งตัวสร้างที่สร้างขึ้นบางส่วนไปยังวิธีอื่นได้ แต่คุณพูดถูกฉันจะเพิ่มคำพูด
Kirill Rakhman

3
@KirillRakhman วิธีการเกี่ยวกับการเรียกสร้างจาก java? มีวิธีง่าย ๆ ในการทำให้ตัวสร้างพร้อมใช้งานกับ java หรือไม่?
Keyhan

6
ทั้งสามรุ่นสามารถเรียกจาก Java Car.Builder builder = new Car.Builder();เช่นดังนั้น: อย่างไรก็ตามเฉพาะรุ่นแรกเท่านั้นที่มีส่วนต่อประสานที่คล่องแคล่วดังนั้นการโทรไปยังรุ่นที่สองและสามจึงไม่สามารถถูกผูกมัดได้
คิริลล์ Rakhman

10
ฉันคิดว่าตัวอย่าง kotlin ที่ด้านบนอธิบายกรณีใช้งานได้เพียงกรณีเดียวเท่านั้น เหตุผลหลักที่ฉันใช้ผู้สร้างคือการแปลงวัตถุที่ไม่แน่นอนเป็นวัตถุที่ไม่เปลี่ยนรูป นั่นคือฉันต้องการที่จะกลายพันธุ์เมื่อเวลาผ่านไปในขณะที่ฉัน "อาคาร" และจากนั้นมากับวัตถุที่ไม่เปลี่ยนรูป อย่างน้อยในรหัสของฉันมีเพียงหนึ่งหรือสองตัวอย่างของรหัสที่มีการเปลี่ยนแปลงมากมายของพารามิเตอร์ที่ฉันจะใช้ตัวสร้างแทนการสร้างที่แตกต่างกันหลายอย่าง แต่เพื่อให้เป็นวัตถุที่ไม่เปลี่ยนรูปฉันมีบางกรณีที่ผู้สร้างเป็นวิธีที่สะอาดที่สุดที่ฉันสามารถนึกได้
ycomp

21

วิธีหนึ่งคือทำสิ่งต่อไปนี้:

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

ขอบคุณมาก! คุณทำวันของฉัน! คำตอบของคุณควรถูกทำเครื่องหมายเป็นโซลูชัน
sVd

9

เนื่องจากฉันใช้ห้องสมุด Jackson ในการแยกวิเคราะห์วัตถุจาก JSON ฉันจึงต้องมี Constructor ว่างเปล่าและฉันไม่สามารถมีฟิลด์ตัวเลือกได้ นอกจากนี้ทุกฟิลด์จะต้องไม่แน่นอน จากนั้นฉันสามารถใช้ไวยากรณ์ที่ดีนี้ซึ่งทำสิ่งเดียวกันกับรูปแบบของตัวสร้าง:

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

8
ในแจ็กสันคุณไม่จำเป็นต้องมีคอนสตรัคเตอร์เปล่าและไม่จำเป็นต้องแปลงเป็นฟิลด์ คุณเพียงแค่ใส่คำอธิบายประกอบพารามิเตอร์คอนสตรัคเตอร์ของคุณด้วย@JsonProperty
Bastian Voigt

2
คุณไม่จำเป็นต้องใส่คำอธิบายประกอบ@JsonPropertyอีกต่อไปถ้าคุณคอมไพล์ด้วย-parametersสวิตช์
Amir Abiri

2
แจ็คสันสามารถกำหนดค่าให้ใช้ตัวสร้างได้จริง
Keyhan

1
หากคุณเพิ่มโมดูล jackson-module-kotlin ในโครงการของคุณคุณสามารถใช้คลาสข้อมูลและมันจะทำงาน
Nils Breunese

2
สิ่งนี้ทำในสิ่งเดียวกันกับรูปแบบของตัวสร้างได้อย่างไร คุณกำลังยกตัวอย่างผลิตภัณฑ์ขั้นสุดท้ายแล้วแลกเปลี่ยน / เพิ่มข้อมูล จุดรวมของรูปแบบตัวสร้างคือการไม่สามารถรับผลิตภัณฑ์ขั้นสุดท้ายได้จนกว่าจะมีข้อมูลที่จำเป็นทั้งหมดปรากฏอยู่ การลบ .apply () ทำให้คุณมีรถยนต์ที่ไม่ได้กำหนด การเอาอาร์กิวเมนต์ตัวสร้างออกทั้งหมดจากตัวสร้างทำให้คุณมีตัวสร้างรถและถ้าคุณพยายามที่จะสร้างมันลงในรถคุณอาจพบข้อยกเว้นที่ยังไม่ได้ระบุรุ่นและปี พวกเขาไม่เหมือนกัน
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

1
+ Keyhan อีกสองวิธีที่คุณสามารถทำการตรวจสอบได้ถ้าสมมติว่าการตรวจสอบไม่เกิดขึ้นระหว่างเขตข้อมูล: 1) ใช้ผู้รับมอบคุณสมบัติที่ setter ทำการตรวจสอบ - นี่เป็นสิ่งเดียวกันกับการมี setter ปกติที่ทำการตรวจสอบ 2) ความหลงใหลดั้งเดิมและสร้างรูปแบบใหม่ที่จะผ่านในการตรวจสอบตัวเอง
จาค็อบซิมเมอร์แมน

1
@Keyhan นี่เป็นแนวทางแบบคลาสสิกใน Python มันทำงานได้ดีแม้กับฟังก์ชั่นที่มีอาร์กิวเมนต์เป็นสิบ เคล็ดลับที่นี่คือการใช้อาร์กิวเมนต์ที่มีชื่อ (ไม่มีใน Java!)
voddan

1
ใช่มันเป็นวิธีแก้ปัญหาที่ควรค่าแก่การใช้งานซึ่งดูเหมือนว่าแตกต่างจากจาวาที่ตัวสร้างคลาสมีข้อได้เปรียบที่ชัดเจนใน Kotlin มันไม่ชัดเจนนักคุยกับนักพัฒนา C #, C # ยังมี kotlin เช่นคุณสมบัติ ตัวสร้างการเรียก) พวกเขาไม่ได้ใช้รูปแบบตัวสร้าง
Keyhan

1
@ vxh.viet หลายกรณีสามารถแก้ไขได้ด้วย@JvmOverloads kotlinlang.org/docs/reference/ …
voddan

4

ฉันได้เห็นตัวอย่างมากมายที่ประกาศ funs พิเศษในฐานะผู้สร้าง ฉันชอบวิธีนี้เป็นการส่วนตัว ประหยัดความพยายามในการเขียนผู้สร้าง

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 มีวิธีการสร้าง Builders Groovy / DSL:

ผู้สร้างที่ปลอดภัยประเภท

นี่คือตัวอย่าง:

ตัวอย่าง Github - เครื่องมือสร้าง / แอสเซมเบลอร์


ขอบคุณ แต่ฉันคิดว่าจะใช้จาก java เช่นกัน เท่าที่ฉันรู้อาร์กิวเมนต์ตัวเลือกจะไม่ทำงานจาก java
Keyhan

2

คน Nowdays ควรตรวจสอบ Kotlin ของผู้สร้างชนิดปลอดภัย

การใช้วิธีการสร้างวัตถุดังกล่าวจะมีลักษณะดังนี้:

html {
    head {
        title {+"XML encoding with Kotlin"}
    }
    // ...
}

ที่ดีในการกระทำ 'ตัวอย่างการใช้งานเป็นvaadin-on-Kotlinกรอบซึ่งใช้สร้าง typesafe เพื่อรวบรวมมุมมองและส่วนประกอบ


1

ฉันไปงานปาร์ตี้สาย ฉันพบปัญหาเดียวกันถ้าฉันต้องใช้รูปแบบตัวสร้างในโครงการ ต่อมาหลังจากการวิจัยฉันได้ตระหนักว่ามันไม่จำเป็นอย่างยิ่งเนื่องจาก Kotlin ได้เตรียมอาร์กิวเมนต์ที่มีชื่อและอาร์กิวเมนต์เริ่มต้นไว้แล้ว

หากคุณต้องการนำไปใช้จริง ๆ คำตอบของคิริลล์ราห์มันคือคำตอบที่ชัดเจนเกี่ยวกับวิธีการใช้งานอย่างมีประสิทธิภาพสูงสุด สิ่งที่คุณอาจพบว่ามีประโยชน์ก็คือhttps://www.baeldung.com/kotlin-builder-patternคุณสามารถเปรียบเทียบและเปรียบเทียบกับ Java และ Kotlin ได้


0

ฉันจะบอกว่ารูปแบบและการใช้งานยังคงเหมือนเดิมใน Kotlin บางครั้งคุณสามารถข้ามไปได้เนื่องจากค่าเริ่มต้น แต่สำหรับการสร้างวัตถุที่ซับซ้อนมากขึ้นผู้สร้างยังคงเป็นเครื่องมือที่มีประโยชน์ที่ไม่สามารถข้ามได้


เท่าที่ก่อสร้างที่มีค่าเริ่มต้นที่คุณสามารถทำได้แม้กระทั่งการตรวจสอบของการป้อนข้อมูลโดยใช้การเริ่มต้นบล็อก อย่างไรก็ตามหากคุณต้องการบางสิ่งที่เป็นสถานะ (เพื่อที่คุณจะได้ไม่ต้องระบุทุกอย่างไว้ล่วงหน้า) รูปแบบการสร้างยังคงเป็นวิธีที่จะไป
mfulton26

คุณช่วยยกตัวอย่างง่ายๆให้ฉันด้วยรหัสได้ไหม พูดคลาสผู้ใช้อย่างง่ายพร้อมฟิลด์ชื่อและอีเมลพร้อมการตรวจสอบอีเมล
Keyhan

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

Kotlin:

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

0

ฉันกำลังทำงานในโครงการ Kotlin ที่เปิดเผย API ที่ลูกค้า Java ใช้ (ซึ่งไม่สามารถใช้ประโยชน์จากโครงสร้างภาษา Kotlin) เราต้องเพิ่มผู้สร้างเพื่อให้สามารถใช้งานได้ใน Java ดังนั้นฉันจึงสร้างคำอธิบายประกอบ @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - โดยพื้นฐานแล้วมันเป็นการแทนที่คำอธิบายประกอบ Lombok @Builder สำหรับ Kotlin

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.