Как реализовать шаблон 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:
Вот пример:
В наши дни люди должны проверять 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")