Wie implementiere ich ein Builder-Muster in Kotlin?

144

Hallo, ich bin ein Neuling in der Kotlin-Welt. Mir gefällt, was ich bisher gesehen habe, und ich habe darüber nachgedacht, einige unserer Bibliotheken, die wir in unserer Anwendung verwenden, von Java nach Kotlin zu konvertieren.

Diese Bibliotheken sind voll von Pojos mit Setter-, Getter- und Builder-Klassen. Jetzt habe ich gegoogelt, um herauszufinden, wie Builder in Kotlin am besten implementiert werden können, aber ohne Erfolg.

2. Update: Die Frage ist, wie man ein Builder-Entwurfsmuster für ein einfaches Pojo mit einigen Parametern in Kotlin schreibt. Der folgende Code ist mein Versuch, Java-Code zu schreiben und dann das Eclipse-Kotlin-Plugin zu verwenden, um nach Kotlin zu konvertieren.

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
quelle
1
brauchst du modelund yearum veränderlich zu sein? Ändern Sie sie nach einer CarKreation?
Voddan
Ich denke, sie sollten ja unveränderlich sein. Außerdem möchten Sie sicher sein, dass beide festgelegt und nicht leer sind
Keyhan
1
Sie können auch diesen Annotationsprozessor github.com/jffiorillo/jvmbuilder verwenden , um die Builder-Klasse automatisch für Sie zu generieren.
JoseF
@JoseF Gute Idee, es zu Standard-Kotlin hinzuzufügen. Es ist nützlich für Bibliotheken, die in Kotlin geschrieben sind.
Keyhan

Antworten:

271

In erster Linie müssen Sie in Kotlin in den meisten Fällen keine Builder verwenden, da wir Standardargumente und benannte Argumente haben. Dadurch können Sie schreiben

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

und benutze es so:

val car = Car(model = "X")

Wenn Sie unbedingt Builder verwenden möchten, gehen Sie wie folgt vor:

Den Builder zu einem zu machen, companion objectmacht keinen Sinn, weil objects Singletons sind. Deklarieren Sie es stattdessen als verschachtelte Klasse (die in Kotlin standardmäßig statisch ist).

Verschieben Sie die Eigenschaften in den Konstruktor, damit das Objekt auch auf normale Weise instanziiert werden kann (machen Sie den Konstruktor privat, falls dies nicht der Fall sein sollte), und verwenden Sie einen sekundären Konstruktor, der einen Builder übernimmt und an den primären Konstruktor delegiert. Der Code sieht wie folgt aus:

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

Verwendung: val car = Car.Builder().model("X").build()

Dieser Code kann mithilfe eines Builder-DSL zusätzlich gekürzt werden :

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

Verwendung: val car = Car.build { model = "X" }

Wenn einige Werte erforderlich sind und keine Standardwerte haben, müssen Sie sie in den Konstruktor des Builders und auch in die buildgerade definierte Methode einfügen:

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

Verwendung: val car = Car.build(required = "requiredValue") { model = "X" }

Kirill Rakhman
quelle
2
Nichts, aber der Autor der Frage fragte speziell, wie das Builder-Muster implementiert werden soll.
Kirill Rakhman
4
Ich sollte mich korrigieren, das Builder-Muster hat einige Vorteile, z. B. könnten Sie einen teilweise konstruierten Builder an eine andere Methode übergeben. Aber du hast recht, ich werde eine Bemerkung hinzufügen.
Kirill Rakhman
3
@KirillRakhman Wie wäre es, wenn Sie den Builder von Java aus anrufen? Gibt es eine einfache Möglichkeit, den Builder für Java verfügbar zu machen?
Keyhan
6
Alle drei Versionen können wie folgt von Java aus aufgerufen werden : Car.Builder builder = new Car.Builder();. Allerdings verfügt nur die erste Version über eine flüssige Oberfläche, sodass die Aufrufe der zweiten und dritten Version nicht verkettet werden können.
Kirill Rakhman
10
Ich denke, das Kotlin-Beispiel oben erklärt nur einen möglichen Anwendungsfall. Der Hauptgrund, warum ich Builder verwende, ist die Umwandlung eines veränderlichen Objekts in ein unveränderliches. Das heißt, ich muss es im Laufe der Zeit mutieren, während ich "baue", und dann ein unveränderliches Objekt finden. Zumindest in meinem Code gibt es nur ein oder zwei Codebeispiele mit so vielen Variationen von Parametern, dass ich einen Builder anstelle mehrerer verschiedener Konstruktoren verwenden würde. Aber um ein unveränderliches Objekt herzustellen, habe ich einige Fälle, in denen ein Baumeister definitiv die sauberste Art ist, die ich mir vorstellen kann.
ycomp
19

Ein Ansatz besteht darin, Folgendes zu tun:

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

Anwendungsbeispiel:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()
Dmitrii Bychkov
quelle
Vielen Dank! Du hast meinen Tag gerettet! Ihre Antwort sollte als LÖSUNG markiert sein.
SVD
9

Da ich die Jackson-Bibliothek zum Parsen von Objekten aus JSON verwende, benötige ich einen leeren Konstruktor und kann keine optionalen Felder haben. Auch müssen alle Felder veränderbar sein. Dann kann ich diese nette Syntax verwenden, die dasselbe tut wie das Builder-Muster:

val car = Car().apply{ model = "Ford"; year = 2000 }
David Vávra
quelle
8
In Jackson muss kein leerer Konstruktor vorhanden sein, und Felder müssen nicht veränderbar sein. Sie müssen nur Ihre Konstruktorparameter mit@JsonProperty
Bastian Voigt
2
Sie müssen nicht einmal mehr mit Anmerkungen versehen @JsonProperty, wenn Sie mit dem -parametersSwitch kompilieren .
Amir Abiri
2
Jackson kann tatsächlich für die Verwendung eines Builders konfiguriert werden.
Keyhan
1
Wenn Sie Ihrem Projekt das jackson-module-kotlin-Modul hinzufügen, können Sie einfach Datenklassen verwenden, und es funktioniert.
Nils Breunese
2
Wie funktioniert dies mit einem Builder-Muster? Sie instanziieren das Endprodukt und tauschen dann Informationen aus / fügen sie hinzu. Der springende Punkt des Builder-Musters ist, dass das Endprodukt erst dann abgerufen werden kann, wenn alle erforderlichen Informationen vorliegen. Wenn Sie .apply () entfernen, erhalten Sie ein undefiniertes Auto. Wenn Sie alle Konstruktorargumente aus Builder entfernen, erhalten Sie einen Car Builder. Wenn Sie versuchen, ihn in ein Auto einzubauen, tritt wahrscheinlich eine Ausnahme auf, weil Sie das Modell und das Jahr noch nicht angegeben haben. Sie sind nicht dasselbe.
ZeroStatic
7

Ich persönlich habe noch nie einen Baumeister in Kotlin gesehen, aber vielleicht bin es nur ich.

Alle Validierungen, die benötigt werden, erfolgen im initBlock:

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

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

Hier habe ich mir erlaubt zu erraten, dass du nicht wirklich wolltest modelund yearveränderlich sein willst . Auch diese Standardwerte scheinen keinen Sinn zu haben (besonders nullfür name), aber ich habe einen zu Demonstrationszwecken hinterlassen.

Eine Meinung: Das Builder-Muster, das in Java verwendet wird, um ohne benannte Parameter zu leben. In Sprachen mit benannten Parametern (wie Kotlin oder Python) empfiehlt es sich, Konstruktoren mit langen Listen von (möglicherweise optionalen) Parametern zu haben.

Voddan
quelle
2
Vielen Dank für die Antwort. Ich mag Ihren Ansatz, aber der Nachteil ist, dass es für eine Klasse mit vielen Parametern nicht so freundlich ist, den Konstruktor zu verwenden und die Klasse auch zu testen.
Keyhan
1
+ Keyhan zwei weitere Möglichkeiten, wie Sie die Validierung durchführen können, vorausgesetzt, die Validierung findet nicht zwischen den Feldern statt: 1) Verwenden Sie Eigenschaftendelegierte, bei denen der Setter die Validierung durchführt - dies ist so ziemlich das Gleiche wie bei einem normalen Setter, der die Validierung durchführt. 2) Vermeiden Sie primitive Besessenheit und schaffen neue Typen, die sich selbst validieren.
Jacob Zimmerman
1
@Keyhan Dies ist ein klassischer Ansatz in Python, der auch für Funktionen mit Dutzenden von Argumenten sehr gut funktioniert. Der Trick hier ist, benannte Argumente zu verwenden (nicht in Java verfügbar!)
voddan
1
Ja, es ist auch eine Lösung, die es wert ist, verwendet zu werden. Es scheint anders als in Java, wo die Builder-Klasse einige klare Vorteile hat. In Kotlin ist es nicht so offensichtlich, mit C # -Entwicklern gesprochen. C # hat auch kotlinähnliche Funktionen (Standardwert und Sie können Parameter benennen, wenn Konstruktor aufrufen) verwendeten sie auch kein Builder-Muster.
Keyhan
1
@ vxh.viet viele solcher Fälle können mit @JvmOverloads kotlinlang.org/docs/reference/…
voddan
4

Ich habe viele Beispiele gesehen, die als Bauherren zusätzlichen Spaß erklären. Ich persönlich mag diesen Ansatz. Sparen Sie Aufwand beim Schreiben von Buildern.

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

    }
}

Ich habe noch keinen Weg gefunden, der die Initialisierung einiger Felder in DSL erzwingen kann, z. B. das Anzeigen von Fehlern, anstatt Ausnahmen auszulösen. Lassen Sie mich wissen, wenn jemand weiß.

Arst
quelle
2

Für eine einfache Klasse benötigen Sie keinen separaten Builder. Sie können optionale Konstruktorargumente verwenden, wie von Kirill Rakhman beschrieben.

Wenn Sie eine komplexere Klasse haben, bietet Kotlin eine Möglichkeit, Builder / DSL im Groovy-Stil zu erstellen:

Typensichere Builder

Hier ist ein Beispiel:

Github-Beispiel - Builder / Assembler

Dariusz Bacinski
quelle
Danke, aber ich habe darüber nachgedacht, es auch von Java aus zu verwenden. Soweit ich weiß, würden optionale Argumente mit Java nicht funktionieren.
Keyhan
1

Ich bin zu spät zur Party. Das gleiche Dilemma hatte ich auch, wenn ich im Projekt das Builder-Muster verwenden musste. Später, nach Recherchen, habe ich festgestellt, dass dies absolut unnötig ist, da Kotlin bereits die genannten Argumente und Standardargumente bereitstellt.

Wenn Sie wirklich implementieren müssen, ist Kirill Rakhmans Antwort eine solide Antwort darauf, wie Sie am effektivsten implementieren können. Eine andere Sache, die Sie vielleicht nützlich finden, ist https://www.baeldung.com/kotlin-builder-pattern, die Sie bei ihrer Implementierung mit Java und Kotlin vergleichen und gegenüberstellen können

Farruh Habibullaev
quelle
0

Ich würde sagen, dass das Muster und die Implementierung in Kotlin ziemlich gleich bleiben. Sie können es manchmal dank Standardwerten überspringen, aber für eine kompliziertere Objekterstellung sind Builder immer noch ein nützliches Werkzeug, das nicht weggelassen werden kann.

Ritave
quelle
Bei Konstruktoren mit Standardwerten können Sie die Eingabe sogar mithilfe von Initialisierungsblöcken überprüfen . Wenn Sie jedoch etwas Stateful benötigen (damit Sie nicht alles im Voraus angeben müssen), ist das Builder-Muster immer noch der richtige Weg.
mfulton26
Können Sie mir ein einfaches Beispiel mit Code geben? Sagen Sie eine einfache Benutzerklasse mit Name und E-Mail-Feld mit Validierung für E-Mail.
Keyhan
0

Sie können optionale Parameter im Kotlin-Beispiel verwenden:

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

dann

myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")
vuhung3990
quelle
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)
        }

    }

}
Brandon unhöflich
quelle
0

Ich habe ein grundlegendes Builder-Muster in Kotlin mit dem folgenden Code implementiert:

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

Und schlussendlich

Java:

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

Kotlin:

DialogMessage.Builder( context )
       .title("Title")
       .message("")
       .build()
       .show()
Moises Portillo
quelle
0

Ich habe an einem Kotlin-Projekt gearbeitet, das eine von Java-Clients verwendete API verfügbar gemacht hat (die die Kotlin-Sprachkonstrukte nicht nutzen kann). Wir mussten Builder hinzufügen, um sie in Java verwendbar zu machen. Deshalb habe ich eine @ Builder-Annotation erstellt: https://github.com/ThinkingLogic/kotlin-builder-annotation - sie ist im Grunde ein Ersatz für die Lombok @ Builder-Annotation für Kotlin.

Noch ein anderes Matt
quelle