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

Привет, я новичок в мире 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
        }
    }
}

18 ответов

Решение

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

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

и использовать это так:

val car = Car(model = "X")

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

Создание Строителя companion object не имеет смысла, потому что objectсинглтоны. Вместо этого объявите его как вложенный класс (который является статическим по умолчанию в 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").builder()

Этот код может быть сокращен дополнительно с помощью построителя 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" }

Один из подходов - сделать что-то вроде следующего:

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

Лично я никогда не видел строителя в Котлине, но, возможно, это только я.

Все необходимые проверки происходят в init блок:

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

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

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

Мнение: шаблон построителя, используемый в Java как средство существования без именованных параметров. В языках с именованными параметрами (например, Kotlin или Python) рекомендуется иметь конструкторы с длинными списками (возможно, необязательных) параметров.

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

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

Я видел много примеров, которые объявляют дополнительные забавы строителями. Мне лично нравится такой подход. Экономьте усилия, чтобы писать строителям.

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, например, показывать ошибки, а не генерировать исключения. Дайте мне знать, если кто-нибудь знает.

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

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

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

Вот пример:

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

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

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

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

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

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

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

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

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

Немного измененная и улучшенная версия ответов выше

      class MyDialog {
  private var title: String? = null
  private var content: String? = null
  private var confirmButtonTitle: String? = null
  private var rejectButtonTitle: String? = null

  @DrawableRes
  private var icon: Int? = null


  fun show() {
    // set dialog content here and show at the end
  }

  class Builder {
      private var dialog: MyDialog = MyDialog()

      fun title(title: String) = apply { dialog.title = title }

      fun icon(@DrawableRes icon: Int) = apply { dialog.icon = icon }

      fun content(content: String) = apply { dialog.content = content }

      fun confirmTitle(confirmTitle: String) = apply { dialog.confirmButtonTitle = confirmTitle }

      fun rejectButtonTitle(rejectButtonTitle: String) = apply { dialog.rejectButtonTitle = rejectButtonTitle }

      fun build() = MyDialog()
  }
}

И использование

      MyDialog.Builder()
        .title("My Title")
        .content("My content here")
        .icon(R.drawable.bg_edittext)
        .confirmTitle("Accept")
        .rejectButtonTitle("Cancel")
        .build()
        .show()

Я только что нашел интересный способ создать билдер в kotlin:

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

Вот код:

      class Grafana(
    private val module: String,
    private val scene: String,
    private val action: String,
    private val metric: String
) {
    companion object {
        fun build(module: String, scene: String, action: String, metric: String) =
            Grafana(module, scene, action, metric)

        val builder = ::build.curriedBuilder()

        private fun <P1, P2, P3, P4, R> Function4<P1, P2, P3, P4, R>.curriedBuilder() =
            fun(p1: P1) = fun(p2: P2) = fun(p3: P3) = fun(p4: P4) = this(p1, p2, p3, p4)
    }

    fun report() = Unit
}


val moduleBuilder = Grafana.builder("module")
val scene = moduleBuilder("scene")
val gfA = scene("action")("metric")
gfA.report()

val sceneB = moduleBuilder("sceneB")
val gfB = sceneB("action")("metric")
gfB.report()

val gfC = Grafana.builder("xx")("xxx")("xxxx")("xxxx")
gfC.report()

      class Person(
    val name:String,
    val family:String,
    val age:Int,
    val nationalCode: String?,
    val email: String?,
    val phoneNumber: String?
) {

    // Private constructor
    private constructor(builder: Builder) : this (
        builder.name,
        builder.family,
        builder.age,
        builder.nationalCode,
        builder.email,
        builder.phoneNumber
    )

    // Builder class

    // 1 Necessary parameters in Builder class : name , family
    class Builder(val name :String,val family :String) {

        // 2 Optional parameters in Builder class :
        var age: Int = 0
            private set
        var nationalCode: String? = null
            private set
        var email: String? = null
            private set
        var phoneNumber: String? = null
            private set

        fun age(age: Int) = apply { this.age = age }
        fun nationalCode(nationalCode: String) =
            apply { this.nationalCode = nationalCode }
        fun email(email: String) = apply { this.email = email }
        fun phoneNumber(phoneNumber: String) =
            apply { this.phoneNumber = phoneNumber }

        // 3 Create
        fun create() = Person(this)

    }
}

для доступа:

      val firstPerson = Person.Builder(
    name = "Adnan",
    family = "Abdollah Zaki")
    .age(32)
    .email("Adnan9011@gmail.com")
    .phoneNumber("+989333030XXX")
    .nationalCode("04400XXXXX")
    .create()

val secondPerson = Person.Builder(
    name = "Foroogh",
    family = "Varmazyar")
    .create()
      data class Car(
    val model: String,// NonNull
    val color: String?,// Nullable
    val type: String// NonNull
) {
    init {
        // do check between variables 
        if (color != "black" && color != "white") {
            throw Exception()
        }
    }
}

// Применение:

         val car =  Car(....)

Я реализовал базовый шаблон 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()
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)
        }

    }

}

Единственное, о чем я здесь не упомянул, так это о том, что функциональность компоновщика полностью покрывается комбинацией уже упомянутых значений по умолчанию и метода.

Единственный вариант использования, который не охватывает наличие именованных параметров со значениями по умолчанию, — это случай, когда вы можете захотеть постепенно заполнить построитель данными, которые вы получаете асинхронно, и фактически построить свой класс на каком-то более позднем этапе, когда у вас будут все ингредиенты. . Котлин позволяет это сделать благодаря.copy()метод, реализуемый всемиdata classesпозволяя вам создавать класс постепенно, без особых хлопот и без шаблонов, используя возможности именованных параметров.

Нет необходимости использовать сборщики в Котлине вообще. Вам нужно будет продолжать использовать/понимать их, поскольку они используются в ваших Java-зависимостях, но они должны начать уходить в прошлое.

Вы можете использовать необязательный параметр в примере 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")
Другие вопросы по тегам