Come implementare il modello Builder in Kotlin?


146

Ciao, sono un principiante nel mondo Kotlin. Mi piace quello che vedo finora e ho iniziato a pensare di convertire alcune delle nostre librerie che utilizziamo nella nostra applicazione da Java a Kotlin.

Queste librerie sono piene di Pojo con setter, getter e classi Builder. Ora ho cercato su Google di trovare qual è il modo migliore per implementare i costruttori a Kotlin, ma senza successo.

2 ° aggiornamento: la domanda è come scrivere un modello di progettazione Builder per un semplice pojo con alcuni parametri in Kotlin? Il codice qui sotto è il mio tentativo scrivendo codice java e quindi usando il plugin eclipse-kotlin per convertire in 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
hai bisogno modele yeardi essere mutevole? Li cambi dopo una Carcreazione?
Voddan,

Immagino che dovrebbero essere immutabili sì. Inoltre, vuoi essere sicuro che siano impostati entrambi e non vuoti
Keyhan,

1
È inoltre possibile utilizzare questo processore di annotazione github.com/jffiorillo/jvmbuilder per generare automaticamente la classe builder.
JoseF,

@JoseF Buona idea per aggiungerlo al kotlin standard. È utile per le biblioteche scritte in kotlin.
Keyhan,

Risposte:


273

Innanzitutto, nella maggior parte dei casi non è necessario utilizzare i costruttori in Kotlin perché abbiamo argomenti predefiniti e denominati. Questo ti permette di scrivere

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

e usalo così:

val car = Car(model = "X")

Se vuoi assolutamente usare i costruttori, ecco come puoi farlo:

Rendere il Builder a companion objectnon ha senso perché objects sono dei singoli. Dichiaralo invece come una classe nidificata (che è statica per impostazione predefinita in Kotlin).

Spostare le proprietà sul costruttore in modo che l'oggetto possa anche essere istanziato in modo regolare (rendere privato il costruttore se non dovrebbe) e utilizzare un costruttore secondario che porta un costruttore e delega al costruttore principale. Il codice apparirà come segue:

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

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

Questo codice può essere abbreviato ulteriormente utilizzando un DSL builder :

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

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

Se alcuni valori sono richiesti e non hanno valori predefiniti, è necessario inserirli nel costruttore del builder e anche nel buildmetodo che abbiamo appena definito:

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

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


2
Nulla, ma l'autore della domanda ha specificamente chiesto come implementare il modello di costruzione.
Kirill Rakhman,

4
Dovrei correggermi, il modello del costruttore presenta alcuni vantaggi, ad esempio potresti passare un costruttore parzialmente costruito ad un altro metodo. Ma hai ragione, aggiungerò un commento.
Kirill Rakhman,

3
@KirillRakhman che ne dici di chiamare il costruttore da Java? C'è un modo semplice per rendere disponibile il costruttore a Java?
Keyhan,

6
Tutte e tre le versioni può essere chiamato da Java in questo modo: Car.Builder builder = new Car.Builder();. Tuttavia, solo la prima versione ha un'interfaccia fluida, quindi le chiamate alla seconda e alla terza versione non possono essere concatenate.
Kirill Rakhman,

10
Penso che l'esempio di kotlin in alto spieghi solo un possibile caso d'uso. Il motivo principale per cui utilizzo i costruttori è convertire un oggetto mutabile in immutabile. Cioè, ho bisogno di mutarlo nel tempo mentre sto "costruendo" e poi inventare un oggetto immutabile. Almeno nel mio codice ci sono solo uno o 2 esempi di codice che ha così tante varianti di parametri che vorrei usare un builder invece di diversi costruttori diversi. Ma per creare un oggetto immutabile, ho alcuni casi in cui un costruttore è sicuramente il modo più pulito a cui riesco a pensare.
ycomp

21

Un approccio consiste nel fare qualcosa di simile al seguente:

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

Esempio di utilizzo:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()

Molte grazie! Mi hai reso felice! La tua risposta dovrebbe essere contrassegnata come SOLUZIONE.
venerdì

9

Poiché sto usando la libreria Jackson per analizzare oggetti da JSON, ho bisogno di un costruttore vuoto e non posso avere campi opzionali. Inoltre, tutti i campi devono essere modificabili. Quindi posso usare questa bella sintassi che fa la stessa cosa del modello Builder:

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

8
A Jackson in realtà non è necessario disporre di un costruttore vuoto e i campi non devono essere mutabili. Devi solo annotare i parametri del costruttore con@JsonProperty
Bastian Voigt

2
Non è nemmeno più necessario annotare con @JsonProperty, se si compila con lo -parametersswitch.
Amir Abiri,

2
Jackson può effettivamente essere configurato per utilizzare un builder.
Keyhan,

1
Se aggiungi il modulo jackson-module-kotlin al tuo progetto, puoi semplicemente usare le classi di dati e funzionerà.
Nils Breunese,

2
Come sta facendo la stessa cosa di un Builder Pattern? Stai istanziando il prodotto finale e quindi scambiando / aggiungendo informazioni. Il punto centrale del modello Builder è di non essere in grado di ottenere il prodotto finale finché non sono presenti tutte le informazioni necessarie. La rimozione di .apply () ti lascia con un'auto non definita. La rimozione di tutti gli argomenti del costruttore da Builder ti lascia con un Car Builder e se provi a costruirlo in un'auto, probabilmente ti imbatterai in un'eccezione per non aver ancora specificato il modello e l'anno. Non sono la stessa cosa.
ZeroStatic

7

Personalmente non ho mai visto un costruttore a Kotlin, ma forse sono solo io.

Tutta la convalida di cui si ha bisogno avviene nel initblocco:

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

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

Qui mi sono preso la libertà di indovinare che non volevi davvero modele yeardi essere mutevole. Anche quei valori predefiniti sembrano non avere senso, (specialmente nullper name) ma ne ho lasciato uno a scopo dimostrativo.

Un'opinione: il modello di generatore utilizzato in Java come mezzo per vivere senza parametri nominati. Nei linguaggi con parametri nominati (come Kotlin o Python) è buona norma avere costruttori con lunghe liste di parametri (forse facoltativi).


2
Grazie mille per la risposta. Mi piace il tuo approccio, ma il rovescio della medaglia è per una classe con molti parametri che non è così facile usare il costruttore e anche testare la classe.
Keyhan,

1
+ Keyhan in altri due modi in cui è possibile eseguire la convalida, supponendo che la convalida non avvenga tra i campi: 1) utilizzare delegati di proprietà in cui il setter esegue la convalida - è praticamente la stessa cosa che avere un setter normale che esegue la convalida 2) Evitare ossessione primitiva e creare nuovi tipi da trasmettere per convalidare se stessi.
Jacob Zimmerman,

1
@Keyhan questo è un approccio classico in Python, funziona molto bene anche per funzioni con decine di argomenti. Il trucco qui è usare argomenti con nome (non disponibile in Java!)
voddan

1
Sì, è anche una soluzione che vale la pena usare, sembra diversamente da Java dove la classe builder ha alcuni chiari vantaggi, in Kotlin non è così ovvio, parlando con gli sviluppatori C #, C # ha anche funzionalità simili a kotlin (valore predefinito e potresti nominare i parametri quando chiamando il costruttore) non usavano nemmeno il modello di generatore.
Keyhan,

1
@ vxh.viet molti di questi casi possono essere risolti con @JvmOverloads kotlinlang.org/docs/reference/…
voddan

4

Ho visto molti esempi che dichiarano divertimenti extra come costruttori. Personalmente mi piace questo approccio. Risparmia lo sforzo di scrivere costruttori.

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

    }
}

Non ho ancora trovato un modo per forzare l'inizializzazione di alcuni campi in DSL come mostrare errori invece di generare eccezioni. Fammi sapere se qualcuno lo sa.


2

Per una classe semplice non è necessario un costruttore separato. Puoi usare argomenti del costruttore opzionali come descritto da Kirill Rakhman.

Se hai una classe più complessa, Kotlin offre un modo per creare Builders / DSL in stile Groovy:

Costruttori sicuri

Ecco un esempio:

Esempio di Github - Costruttore / Assemblatore


Grazie, ma stavo pensando di usarlo anche da Java. Per quanto ne so, gli argomenti opzionali non funzionerebbero da Java.
Keyhan,


1

Sono in ritardo alla festa. Ho anche incontrato lo stesso dilemma se dovessi usare il modello Builder nel progetto. Più tardi, dopo la ricerca, mi sono reso conto che non è assolutamente necessario poiché Kotlin fornisce già gli argomenti nominati e gli argomenti predefiniti.

Se hai davvero bisogno di implementare, la risposta di Kirill Rakhman è una risposta solida su come implementare nel modo più efficace. Un'altra cosa che potresti trovare utile è https://www.baeldung.com/kotlin-builder-pattern che puoi confrontare e confrontare con Java e Kotlin sulla loro implementazione


0

Direi che il modello e l'implementazione rimangono praticamente gli stessi in Kotlin. A volte puoi saltarlo grazie ai valori predefiniti, ma per la creazione di oggetti più complicati, i costruttori sono ancora uno strumento utile che non può essere omesso.


Per quanto riguarda i costruttori con valori predefiniti, puoi persino eseguire la convalida dell'input usando i blocchi di inizializzazione . Tuttavia, se hai bisogno di qualcosa di stato (in modo da non dover specificare tutto in anticipo), il modello del generatore è ancora la strada da percorrere.
mfulton26,

Potresti darmi un semplice esempio con il codice? Pronuncia una semplice classe utente con nome e campo e-mail con convalida per e-mail.
Keyhan,

0

puoi usare un parametro opzionale nell'esempio di 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)
}

poi

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

Ho implementato un modello Builder di base in Kotlin con il seguente codice:

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

E infine

Giava:

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

Kotlin:

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

0

Stavo lavorando a un progetto Kotlin che esponeva un'API consumata dai client Java (che non può sfruttare i costrutti del linguaggio Kotlin). Abbiamo dovuto aggiungere costruttori per renderli utilizzabili in Java, quindi ho creato un'annotazione @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - è sostanzialmente un sostituto dell'annotazione Lombok @Builder per Kotlin.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.