Как реализовать шаблон Builder в Kotlin?

146

Привет, я новичок в мире Kotlin. Мне нравится то, что я вижу до сих пор, и начал думать о том, чтобы преобразовать некоторые из наших библиотек, которые мы используем в нашем приложении, из Java в Kotlin.

Эти библиотеки полны Pojos с сеттерами, геттерами и классами Builder. Теперь я погуглил, чтобы найти лучший способ реализации Builders в Kotlin, но безуспешно.

2-е обновление: вопрос в том, как написать шаблон проектирования Builder для простого pojo с некоторыми параметрами в Kotlin? Приведенный ниже код является моей попыткой написать код 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
        }
    }
}
Keyhan
источник
1
тебе нужно modelи yearбыть изменчивым? Вы меняете их после Carсоздания?
Воддан
Я думаю, что они должны быть неизменными, да. Также вы хотите быть уверены, что они установлены и не пусты
Keyhan
1
Вы также можете использовать этот обработчик аннотаций github.com/jffiorillo/jvmbuilder для автоматической генерации класса построителя.
JoseF
@JoseF Хорошая идея добавить его в стандартный kotlin. Это полезно для библиотек, написанных на kotlin.
Кейхан
github.com/ThinkingLogic/kotlin-builder-annotation
Даниэль С. Яицков

Ответы:

273

Прежде всего, в большинстве случаев вам не нужно использовать компоновщики в Kotlin, потому что у нас есть аргументы по умолчанию и именованные аргументы. Это позволяет вам писать

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

и используйте это так:

val car = Car(model = "X")

Если вы абсолютно хотите использовать сборщики, вот как вы можете это сделать:

Делать Builder 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
Ничего, но автор вопроса специально спросил, как реализовать шаблон компоновщика.
Кирилл Рахман
4
Я должен исправить себя, шаблон компоновщика имеет некоторые преимущества, например, вы можете передать частично построенный компоновщик другому методу. Но ты прав, я добавлю замечание.
Кирилл Рахман
3
@KirillRakhman как насчет того, чтобы позвонить строителю из Java? Есть ли простой способ сделать конструктор доступным для Java?
Кейхан
6
Все три версии могут быть вызваны из Java нравится так: Car.Builder builder = new Car.Builder();. Однако только первая версия имеет свободный интерфейс, поэтому вызовы ко второй и третьей версиям не могут быть связаны.
Кирилл Рахман
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

Поскольку я использую библиотеку Джексона для анализа объектов из JSON, мне нужен пустой конструктор, и у меня не может быть необязательных полей. Также все поля должны быть изменяемыми. Затем я могу использовать этот красивый синтаксис, который делает то же самое, что и шаблон Builder:

val car = Car().apply{ model = "Ford"; year = 2000 }
Давид Вавра
источник
8
В Джексоне вам на самом деле не нужен пустой конструктор, а поля не должны быть изменяемыми. Вы просто должны аннотировать ваши параметры конструктора с@JsonProperty
Бастиан Фойгт
2
Вам даже не нужно больше комментировать @JsonProperty, если вы компилируете с -parametersключом.
Амир Абири
2
Джексона можно настроить на использование строителя.
Кейхан
1
Если вы добавите в свой проект модуль jackson-module-kotlin, вы можете просто использовать классы данных, и он будет работать.
Нильс Брунез,
2
Как это делает то же самое, что и Pattern Builder? Вы создаете экземпляр конечного продукта, а затем меняете / добавляете информацию. Смысл паттерна Builder в том, что он не может получить конечный продукт, пока не будет представлена ​​вся необходимая информация. Удаление .apply () оставляет вас с неопределенным автомобилем. Удаление всех аргументов конструктора из Builder оставляет вам Car 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) рекомендуется иметь конструкторы с длинными списками (возможно, необязательных) параметров.

voddan
источник
2
Большое спасибо за ответ. Мне нравится ваш подход, но недостатком является то, что для класса с множеством параметров становится не очень удобно использовать конструктор, а также тестировать класс.
Keyhan
1
+ Keyhan два других способа сделать проверку, предполагая, что проверка не происходит между полями: 1) использовать делегаты свойств, где установщик выполняет проверку - это почти то же самое, что иметь нормальный установщик, который выполняет проверку 2) Избегать примитивная одержимость и создание новых типов для передачи, которые подтверждают себя.
Джейкоб Циммерман
1
@Keyhan, это классический подход в Python, он очень хорошо работает даже для функций с десятками аргументов. Хитрость здесь в том, чтобы использовать именованные аргументы (недоступно в Java!)
voddan
1
Да, это также решение, которое стоит использовать, в отличие от java, где класс компоновщика имеет некоторые явные преимущества, в Kotlin это не так очевидно, говорили с разработчиками C #, C # также имеет функции, подобные kotlin (значение по умолчанию, и вы можете называть params, когда вызывающий конструктор) они также не использовали шаблон компоновщика.
Кейхан
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, например, показывать ошибки, а не генерировать исключения. Дайте мне знать, если кто-нибудь знает.

Arst
источник
2

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

Если у вас более сложный класс, то Kotlin предоставляет способ создания Groovy в стиле Builders / DSL:

Тип-Сейф Строители

Вот пример:

Пример Github - Строитель / Ассемблер

Дариуш Бачински
источник
Спасибо, но я подумывал об использовании его и из Java. Насколько я знаю, необязательные аргументы не будут работать с Java.
Кейхан
2

Люди в наши дни должны проверить Type-Safe Builders от Kotlin .

Использование указанного способа создания объекта будет выглядеть примерно так:

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

Хороший пример использования «в действии» - это фреймворк vaadin-on-kotlin , в котором используются сборщики безопасных типов для сборки представлений и компонентов .

danield
источник
1

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

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

Фаррух Хабибуллаев
источник
0

Я бы сказал, что шаблон и реализация в Kotlin остаются практически такими же. Иногда вы можете пропустить это благодаря значениям по умолчанию, но для более сложного создания объектов, строители все еще являются полезным инструментом, который нельзя пропустить.

Ritave
источник
Что касается конструкторов со значениями по умолчанию, вы можете даже выполнить проверку ввода, используя блоки инициализатора . Тем не менее, если вам нужно что-то с состоянием (чтобы вам не нужно было все задавать заранее), тогда шаблон построения все еще остается в силе.
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")
vuhung3990
источник
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 в 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()
        }
    }
}

И наконец

Ява:

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

Котлин:

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.

YetAnotherMatt
источник