Làm cách nào để triển khai mẫu Builder trong Kotlin?


144

Xin chào Tôi là một người mới trong thế giới Kotlin. Tôi thích những gì tôi thấy cho đến nay và bắt đầu nghĩ để chuyển đổi một số thư viện mà chúng tôi sử dụng trong ứng dụng của mình từ Java sang Kotlin.

Các thư viện này chứa đầy Pojos với các lớp setters, getters và Builder. Bây giờ tôi đã googled để tìm ra cách tốt nhất để triển khai các Nhà xây dựng trong Kotlin nhưng không thành công.

Cập nhật lần 2: Câu hỏi đặt ra là làm thế nào để viết một mẫu thiết kế Builder cho một pojo đơn giản với một số tham số trong Kotlin? Mã dưới đây là nỗ lực của tôi bằng cách viết mã java và sau đó sử dụng plugin eclipse-kotlin để chuyển đổi sang 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
Bạn có cần modelyearcó thể thay đổi? Bạn có thay đổi chúng sau khi Carsáng tạo?
voddan 22/03/2016

Tôi đoán họ nên bất biến có. Ngoài ra, bạn muốn chắc chắn rằng chúng được đặt cả hai và không trống
Keyhan

1
Bạn cũng có thể sử dụng github.com/jffiorillo/jvmbuilder Bộ xử lý chú thích này để tạo lớp xây dựng tự động cho bạn.
JoseF

@JoseF Ý tưởng tốt để thêm nó vào kotlin tiêu chuẩn. Nó rất hữu ích cho các thư viện được viết bằng kotlin.
Keyhan

Câu trả lời:


271

Đầu tiên và quan trọng nhất, trong hầu hết các trường hợp, bạn không cần sử dụng các trình xây dựng trong Kotlin vì chúng tôi có các đối số mặc định và được đặt tên. Điều này cho phép bạn viết

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

và sử dụng nó như vậy:

val car = Car(model = "X")

Nếu bạn hoàn toàn muốn sử dụng các trình xây dựng, đây là cách bạn có thể làm điều đó:

Làm cho Builder companion objectkhông có ý nghĩa vì objects là singletons. Thay vào đó khai báo nó là một lớp lồng nhau (mặc định là tĩnh trong Kotlin).

Di chuyển các thuộc tính đến hàm tạo để đối tượng cũng có thể được khởi tạo theo cách thông thường (làm cho hàm tạo riêng tư nếu không nên) và sử dụng một hàm tạo thứ cấp có một hàm tạo và ủy quyền cho hàm tạo chính. Mã này sẽ như sau:

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

Sử dụng: val car = Car.Builder().model("X").build()

Mã này có thể được rút ngắn thêm bằng cách sử dụng DSL xây dựng :

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

Sử dụng: val car = Car.build { model = "X" }

Nếu một số giá trị là bắt buộc và không có giá trị mặc định, bạn cần đặt chúng vào hàm tạo của trình tạo và cả trong buildphương thức chúng ta vừa xác định:

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

Sử dụng: val car = Car.build(required = "requiredValue") { model = "X" }


2
Không có gì, nhưng tác giả của câu hỏi đã hỏi cụ thể cách triển khai mẫu xây dựng.
Kirill Rakhman

4
Tôi nên tự sửa, mẫu trình xây dựng có một số lợi thế, ví dụ: bạn có thể chuyển một trình xây dựng được xây dựng một phần sang phương thức khác. Nhưng bạn nói đúng, tôi sẽ thêm một nhận xét.
Kirill Rakhman

3
@KirillRakhman về cách gọi người xây dựng từ java? Có một cách dễ dàng để làm cho trình xây dựng có sẵn cho java?
Keyhan

6
Tất cả ba phiên bản có thể được gọi từ Java như vậy : Car.Builder builder = new Car.Builder();. Tuy nhiên, chỉ có phiên bản đầu tiên có giao diện lưu loát nên các cuộc gọi đến phiên bản thứ hai và thứ ba không thể thực hiện được.
Kirill Rakhman

10
Tôi nghĩ rằng ví dụ kotlin ở đầu chỉ giải thích một trường hợp sử dụng có thể. Lý do chính tôi sử dụng các trình xây dựng là để chuyển đổi một đối tượng có thể thay đổi thành một đối tượng bất biến. Đó là, tôi cần phải biến đổi nó theo thời gian trong khi tôi đang "xây dựng" và sau đó tìm ra một vật thể bất biến. Ít nhất trong mã của tôi chỉ có một hoặc 2 ví dụ về mã có rất nhiều biến thể tham số mà tôi sẽ sử dụng một trình xây dựng thay vì một số hàm tạo khác nhau. Nhưng để tạo ra một vật thể bất biến, tôi có một vài trường hợp mà một người xây dựng chắc chắn là cách sạch nhất mà tôi có thể nghĩ ra.
ycomp

19

Một cách tiếp cận là làm một cái gì đó như sau:

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

Mẫu sử dụng:

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

Cảm ơn rất nhiều! Bạn đã làm cho ngày của tôi! Câu trả lời của bạn nên được đánh dấu là GIẢI PHÁP.
sVd

9

Vì tôi đang sử dụng thư viện Jackson để phân tích cú pháp các đối tượng từ JSON, tôi cần có một hàm tạo trống và tôi không thể có các trường tùy chọn. Ngoài ra tất cả các lĩnh vực phải được đột biến. Sau đó, tôi có thể sử dụng cú pháp hay này thực hiện tương tự như mẫu Builder:

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

8
Ở Jackson, bạn thực sự không cần phải có một nhà xây dựng trống và các trường không cần phải có thể thay đổi. Bạn chỉ cần chú thích các tham số hàm tạo của mình với@JsonProperty
Bastian Voigt

2
Bạn thậm chí không phải chú thích với @JsonPropertynữa, nếu bạn biên dịch với công -parameterstắc.
Amir Abiri

2
Jackson thực sự có thể được cấu hình để sử dụng một trình xây dựng.
Keyhan

1
Nếu bạn thêm mô-đun jackson-module-kotlin vào dự án của mình, bạn chỉ cần sử dụng các lớp dữ liệu và nó sẽ hoạt động.
Nils Breunese

2
Làm thế nào điều này làm điều tương tự như Mô hình Builder? Bạn đang khởi tạo sản phẩm cuối cùng và sau đó hoán đổi / thêm thông tin. Toàn bộ quan điểm của mẫu Builder là không thể có được sản phẩm cuối cùng cho đến khi có tất cả các thông tin cần thiết. Loại bỏ .apply () để lại cho bạn một chiếc xe không xác định. Xóa tất cả các đối số của hàm tạo khỏi Builder để lại cho bạn một Trình tạo ô tô và nếu bạn cố gắng xây dựng nó thành một chiếc ô tô, bạn có thể sẽ gặp phải một ngoại lệ vì chưa chỉ định mô hình và năm. Chúng không giống nhau.
ZeroStatic

7

Cá nhân tôi chưa bao giờ thấy một người xây dựng trong Kotlin, nhưng có lẽ đó chỉ là tôi.

Tất cả các xác nhận một nhu cầu xảy ra trong initkhối:

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

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

Ở đây tôi đã tự do đoán rằng bạn không thực sự muốn modelyearcó thể thay đổi. Ngoài những giá trị mặc định dường như không có ý nghĩa, (đặc biệt là nullcho name) nhưng tôi lại một cho mục đích biểu tình.

Một ý kiến: Mẫu trình xây dựng được sử dụng trong Java như một phương tiện để sống mà không có các tham số được đặt tên. Trong các ngôn ngữ có tham số được đặt tên (như Kotlin hoặc Python), nên có các hàm tạo với danh sách dài các tham số (có thể là tùy chọn).


2
Cảm ơn rất nhiều cho câu trả lời. Tôi thích cách tiếp cận của bạn nhưng nhược điểm là đối với một lớp có nhiều tham số, nó trở nên không thân thiện để sử dụng hàm tạo và cũng kiểm tra lớp.
Keyhan

1
+ Keyhan có hai cách khác để bạn xác thực, giả sử việc xác thực không xảy ra giữa các trường: 1) sử dụng các đại biểu thuộc tính trong đó trình xác thực thực hiện xác thực - điều này khá giống với việc thiết lập bình thường có xác thực 2) Tránh ám ảnh nguyên thủy và tạo ra các loại mới để vượt qua chính nó.
Jacob Zimmerman

1
@Keyhan đây là một cách tiếp cận cổ điển trong Python, nó hoạt động rất tốt ngay cả đối với các hàm có hàng chục đối số. Mẹo ở đây là sử dụng các đối số có tên (không có sẵn trong Java!)
voddan

1
Vâng, nó cũng là một giải pháp đáng để sử dụng, có vẻ như không giống java nơi lớp trình xây dựng có một số lợi thế rõ ràng, trong Kotlin không quá rõ ràng, đã nói chuyện với các nhà phát triển C #, C # cũng có các tính năng như kotlin (giá trị mặc định và bạn có thể đặt tên params khi gọi constructor) họ cũng không sử dụng mẫu xây dựng.
Keyhan

1
@ vxh.viet nhiều trường hợp như vậy có thể được giải quyết với @JvmOverloads kotlinlang.org/docs/reference/ mẹo
voddan

4

Tôi đã thấy nhiều ví dụ tuyên bố thêm những niềm vui như những người xây dựng. Cá nhân tôi thích cách tiếp cận này. Tiết kiệm công sức để viết các nhà xây dựng.

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

    }
}

Tôi chưa tìm thấy một cách có thể buộc một số trường được khởi tạo trong DSL như hiển thị lỗi thay vì ném ngoại lệ. Hãy cho tôi biết nếu có ai biết.


2

Đối với một lớp đơn giản, bạn không cần một người xây dựng riêng. Bạn có thể sử dụng các đối số hàm tạo tùy chọn như Kirill Rakhman đã mô tả.

Nếu bạn có lớp phức tạp hơn thì Kotlin cung cấp cách tạo Trình tạo / DSL theo kiểu Groovy:

Nhà xây dựng loại an toàn

Đây là một ví dụ:

Ví dụ về Github - Trình tạo / Trình biên dịch


Cảm ơn, nhưng tôi cũng đã nghĩ đến việc sử dụng nó từ java. Theo tôi biết các đối số tùy chọn sẽ không hoạt động từ java.
Keyhan


1

Tôi đến bữa tiệc muộn. Tôi cũng gặp phải tình huống khó xử tương tự nếu tôi phải sử dụng mẫu Builder trong dự án. Sau này, sau khi nghiên cứu tôi đã nhận ra điều đó là hoàn toàn không cần thiết vì Kotlin đã cung cấp các đối số được đặt tên và các đối số mặc định.

Nếu bạn thực sự cần phải thực hiện, câu trả lời của Kirill Rakhman là câu trả lời chắc chắn về cách thực hiện theo cách hiệu quả nhất. Một điều khác mà bạn có thể thấy hữu ích là https://www.baeldung.com/kotlin-builder-potype bạn có thể so sánh và đối chiếu với Java và Kotlin khi triển khai chúng


0

Tôi có thể nói rằng mô hình và cách triển khai vẫn khá giống nhau trong Kotlin. Đôi khi bạn có thể bỏ qua nó nhờ các giá trị mặc định, nhưng để tạo đối tượng phức tạp hơn, các trình xây dựng vẫn là một công cụ hữu ích không thể bỏ qua.


Theo như các hàm tạo với các giá trị mặc định, bạn thậm chí có thể xác thực đầu vào bằng các khối khởi tạo . Tuy nhiên, nếu bạn cần một cái gì đó có trạng thái (để bạn không phải chỉ định mọi thứ ở phía trước) thì mẫu xây dựng vẫn là con đường để đi.
mfulton26

Bạn có thể cho tôi một ví dụ đơn giản với mã? Nói một lớp Người dùng đơn giản với tên và trường email có xác thực cho email.
Keyhan

0

bạn có thể sử dụng tham số tùy chọn trong ví dụ 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)
}

sau đó

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

Tôi đã triển khai một mẫu Builder cơ bản trong Kotlin với mã sau:

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

Và cuối cùng

Java:

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

Kotlin:

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

0

Tôi đã làm việc với một dự án Kotlin đã phơi bày một API được sử dụng bởi các máy khách Java (không thể tận dụng các cấu trúc ngôn ngữ của Kotlin). Chúng tôi đã phải thêm các trình xây dựng để làm cho chúng có thể sử dụng được trong Java, vì vậy tôi đã tạo một chú thích @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - về cơ bản nó là sự thay thế cho chú thích Lombok @Builder cho Kotlin.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.