Як реалізувати шаблон Builder в Котліні?


145

Привіт, я новачок у світі Котліна. Мені подобається те, що я бачу до цих пір, і почав думати про перетворення деяких наших бібліотек, які ми використовуємо в нашому додатку, з Java на Котлін.

У цих бібліотеках повно Pojos із сетерами, геттерами та класами Builder. Тепер я погуглив, щоб знайти, який найкращий спосіб впровадити Будівельників у Котліні, але успіху немає.

2-е оновлення: Питання полягає в тому, як написати схему дизайнера Builder для простого піхота з деякими параметрами в Котліні? Код нижче - це моя спроба, написавши java-код, а потім використовуючи плагін eclipse-kotlin-плагін для перетворення в 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створення?
воддан

Я думаю, вони повинні бути незмінними. Також ви хочете бути впевнені, що вони встановлені як і не порожні
Кейхан

1
Ви також можете використовувати цей github.com/jffiorillo/jvmbuilder анотаційний процесор, щоб автоматично генерувати клас конструктора для вас.
JoseF

@JoseF Хороша ідея додати його до стандартного котліну. Це корисно для бібліотек, написаних у kotlin.
Кеган

Відповіді:


272

Перш за все, у більшості випадків вам не потрібно використовувати будівельників у Котліні, оскільки у нас є аргументи за замовчуванням та названі. Це дає змогу писати

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

і використовуйте його так:

val car = Car(model = "X")

Якщо ви абсолютно хочете використовувати будівельників, ось як ви могли це зробити:

Зробити Builder 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
Нічого, але автор питання спеціально запитав, як реалізувати схему конструктора.
Кирило Рахман

4
Я повинен виправити себе, що модель конструктора має деякі переваги, наприклад, ви можете передати частково побудований будівельник іншому методу. Але ти маєш рацію, я додам зауваження.
Кирило Рахман

3
@KirillRakhman як щодо дзвінка будівельника з Java? Чи існує простий спосіб зробити доступним для будівельника Java?
Кейхан

6
Всі три версії можуть бути викликані з Java подобається так: Car.Builder builder = new Car.Builder();. Однак лише перша версія має вільний інтерфейс, тому виклики другої та третьої версій не можуть бути прикованими.
Кирило Рахман

10
Я думаю, що приклад kotlin вгорі пояснює лише один можливий випадок використання. Основна причина, з якою я використовую будівельників, - перетворити об'єкт, що змінюється, в незмінний. Тобто мені потрібно мутувати його з часом, поки я "будую", а потім придумую незмінний об'єкт. Принаймні, в моєму коді є лише один або два приклади коду, який має стільки варіацій параметрів, що я б використовував конструктор замість кількох різних конструкторів. Але щоб зробити непорушний предмет, у мене є кілька випадків, коли будівельник - це, безумовно, найчистіший спосіб, про який я можу придумати.
ycomp

19

Один із підходів - зробити щось на кшталт наступного:

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

Оскільки я використовую бібліотеку Джексона для розбору об'єктів з JSON, мені потрібно мати порожній конструктор, і я не можу мати додаткових полів. Також усі поля мають бути зміненими. Тоді я можу використовувати цей симпатичний синтаксис, який робить те саме, що і зразок Builder:

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

8
У Джексона вам насправді не потрібно мати порожній конструктор, і поля не потрібно змінювати. Вам просто потрібно анотувати параметри вашого конструктора за допомогою@JsonProperty
Bastian Voigt

2
Вам навіть не потрібно коментувати @JsonProperty , якщо ви компілюєте з -parametersкомутатором.
Амір Абірі

2
Джексона насправді можна налаштувати на використання будівельника.
Кейхан

1
Якщо ви додасте до свого проекту модуль jackson-module-kotlin, ви можете просто використовувати класи даних, і він буде працювати.
Nils Breunese

2
Як це робить те саме, що і зразок Builder? Ви створюєте екземпляр кінцевого продукту, а потім замінюєте / додаєте інформацію. Вся суть схеми Builder полягає в тому, щоб не мати можливості отримати кінцевий продукт, поки не буде представлена ​​вся необхідна інформація. Видалення .apply () залишає вас невизначеним автомобілем. Видалення всіх аргументів конструктора з Builder залишає вас з конструктором автомобілів, і якщо ви спробуєте вбудувати його в автомобіль, ви, ймовірно, зіткнетеся з винятком, оскільки ще не вказали модель та рік. Вони не одне і те ж.
ZeroStatic

7

Я особисто ніколи не бачив будівельника в Котліні, але, можливо, це тільки я.

Вся необхідність перевірки відбувається в initблоці:

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

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

Тут я взяв на себе сміття здогадуватися, що ви насправді не хочете modelі yearбути змінним. Також ці значення за замовчуванням, здається, не мають сенсу (особливо nullдля name), але я його залишив для демонстраційних цілей.

Думка: модель побудови, яка використовується на Java як середній спосіб життя без названих параметрів. У мовах з названими параметрами (наприклад, Kotlin або Python) є хорошою практикою мати конструктори з довгими списками (можливо, необов'язкових) параметрів.


2
Дякую за відповідь. Мені подобається ваш підхід, але недоліком для класу з багатьма параметрами стає не так зручно використовувати конструктор, а також перевірити клас.
Кейхан

1
+ Keyhan - два інші способи перевірки, припускаючи, що перевірка не відбувається між полями: 1) використовувати делегати властивостей, де сеттер робить перевірку - це майже те саме, що мати звичайний сеттер, який робить перевірку 2) Уникайте примітивної одержимості і створювати нові типи, щоб перейти в цю перевірку себе.
Яків Циммерман

1
@Keyhan - це класичний підхід у Python, він дуже добре працює навіть для функцій з десятками аргументів. Хитрість тут полягає у використанні названих аргументів (недоступні на Java!)
voddan

1
Так, це також рішення, яке варто використовувати, здається, на відміну від java, де клас будівельника має деякі явні переваги, в Котліні це не так очевидно, розмовляли з розробниками C #, у C # також є kotlin, як функції (значення за замовчуванням, і ви можете назвати парами, коли виклику конструктора) вони також не використовували шаблон конструктора.
Кейхан

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

Для простого класу вам не потрібен окремий конструктор. Ви можете скористатися необов'язковими аргументами конструктора, як описано Кірілом Рахманом.

Якщо у вас складніший клас, то Котлін пропонує спосіб створити будівельники стилю Groovy / DSL:

Типові безпечні будівельники

Ось приклад:

Приклад Github - Builder / Assembler


Дякую, але я думав також використовувати його від Java. Наскільки мені відомо, необов'язкові аргументи не спрацювали з Java.
Кейхан

2

Люди сьогодні повинні перевірити безпечні будівельники Котліна .

Використання зазначеного способу створення об'єкта буде виглядати приблизно так:

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

Хороший приклад використання «під дією» - це рамка vaadin-on-kotlin , яка використовує безпечні будівельники для збирання поглядів та компонентів .


1

Я спізнююсь на вечірку. Я також зіткнувся з такою ж дилемою, якби мені довелося використовувати зразок Builder у проекті. Пізніше, після дослідження, я зрозумів, що це абсолютно непотрібно, оскільки Котлін вже надає названі аргументи та аргументи за замовчуванням.

Якщо вам справді потрібно реалізувати, відповідь Кирила Рахмана є надійною відповіддю про те, як реалізувати найефективнішим чином. Ще одна річ, яка може бути вам корисною, це https://www.baeldung.com/kotlin-builder-pattern, яку ви можете порівняти та порівняти з Java та Kotlin за їх реалізацією


0

Я б сказав, що модель і реалізація залишаються майже однаковими в Kotlin. Іноді його можна пропустити завдяки значень за замовчуванням, але для створення складніших об'єктів будівельники все ще є корисним інструментом, який не можна опустити.


Що стосується конструкторів зі значеннями за замовчуванням, ви навіть можете зробити перевірку введення, використовуючи блоки ініціалізатора . Однак якщо вам потрібно щось загадкове (так що вам не доведеться вказувати все наперед), тоді модель розробника - це все-таки шлях.
mfulton26

Чи можете ви надати простий приклад з кодом? Скажіть простий клас користувача з полем імені та електронної пошти з підтвердженням електронної пошти.
Кейхан

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

Я реалізував основний шаблон Builder у Котліні з наступним кодом:

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

Я працював над проектом Котліна, який розкривав API, спожитий клієнтами Java (який не може скористатися конструкціями мови Kotlin). Нам довелося додати будівельників, щоб зробити їх корисними на Java, тому я створив анотацію @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - це в основному заміна анотації Lombok @Builder для Kotlin.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.