Comment implémenter le modèle Builder dans Kotlin?


146

Salut, je suis un débutant dans le monde Kotlin. J'aime ce que je vois jusqu'à présent et j'ai commencé à penser à convertir certaines de nos bibliothèques que nous utilisons dans notre application de Java à Kotlin.

Ces bibliothèques regorgent de Pojos avec des setters, des getters et des classes Builder. Maintenant, j'ai cherché sur Google quel est le meilleur moyen d'implémenter Builders dans Kotlin, mais sans succès.

2ème mise à jour: La question est de savoir comment écrire un design-pattern Builder pour un simple pojo avec quelques paramètres dans Kotlin? Le code ci-dessous est ma tentative en écrivant du code java, puis en utilisant le plugin eclipse-kotlin-plugin pour convertir en 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
avez-vous besoin modelet yearêtre mutable? Les changez-vous après une Carcréation?
voddan

Je suppose qu'ils devraient être immuables oui. Aussi, vous voulez être sûr qu'ils sont définis à la fois et non vides
Keyhan

1
Vous pouvez également utiliser ce processeur d' annotation github.com/jffiorillo/jvmbuilder pour générer automatiquement la classe de générateur pour vous.
JoseF

@JoseF Bonne idée de l'ajouter au kotlin standard. C'est utile pour les bibliothèques écrites en kotlin.
Keyhan

Réponses:


273

Tout d'abord, dans la plupart des cas, vous n'avez pas besoin d'utiliser des générateurs dans Kotlin car nous avons des arguments par défaut et nommés. Cela vous permet d'écrire

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

et utilisez-le comme ceci:

val car = Car(model = "X")

Si vous voulez absolument utiliser des builders, voici comment vous pouvez le faire:

Faire du Builder un companion objectn'a pas de sens car les objects sont des singletons. Déclarez-la plutôt comme une classe imbriquée (qui est statique par défaut dans Kotlin).

Déplacez les propriétés vers le constructeur afin que l'objet puisse également être instancié de la manière habituelle (rendez le constructeur privé s'il ne le devrait pas) et utilisez un constructeur secondaire qui prend un générateur et délègue au constructeur principal. Le code ressemblera à ceci:

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

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

Ce code peut être raccourci en outre en utilisant un DSL de constructeur :

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

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

Si certaines valeurs sont requises et n'ont pas de valeurs par défaut, vous devez les mettre dans le constructeur du constructeur et également dans la buildméthode que nous venons de définir:

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

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


2
Rien, mais l'auteur de la question a spécifiquement demandé comment implémenter le modèle de générateur.
Kirill Rakhman

4
Je devrais me corriger, le modèle de constructeur a certains avantages, par exemple vous pourriez passer un constructeur partiellement construit à une autre méthode. Mais vous avez raison, je vais ajouter une remarque.
Kirill Rakhman

3
@KirillRakhman que diriez-vous d'appeler le constructeur depuis Java? Existe-t-il un moyen simple de rendre le générateur disponible pour Java?
Keyhan

6
Les trois versions peuvent être appelées à partir de Java comme ceci: Car.Builder builder = new Car.Builder();. Cependant, seule la première version a une interface fluide, de sorte que les appels aux deuxième et troisième versions ne peuvent pas être chaînés.
Kirill Rakhman

10
Je pense que l'exemple de kotlin en haut n'explique qu'un cas d'utilisation possible. La principale raison pour laquelle j'utilise des générateurs est de convertir un objet mutable en un objet immuable. Autrement dit, j'ai besoin de le faire muter au fil du temps pendant que je "construis" et ensuite proposer un objet immuable. Au moins dans mon code, il n'y a qu'un ou deux exemples de code qui ont tellement de variations de paramètres que j'utiliserais un constructeur au lieu de plusieurs constructeurs différents. Mais pour créer un objet immuable, j'ai quelques cas où un constructeur est certainement le moyen le plus propre auquel je puisse penser.
ycomp

21

Une approche consiste à faire quelque chose comme ce qui suit:

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

Exemple d'utilisation:

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

Merci beaucoup! Tu as fait ma journée! Votre réponse doit être marquée comme SOLUTION.
sVd

9

Comme j'utilise la bibliothèque Jackson pour analyser des objets à partir de JSON, j'ai besoin d'un constructeur vide et je ne peux pas avoir de champs facultatifs. Tous les champs doivent également être modifiables. Ensuite, je peux utiliser cette belle syntaxe qui fait la même chose que le modèle Builder:

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

8
Dans Jackson, vous n'avez pas besoin d'avoir un constructeur vide, et les champs n'ont pas besoin d'être mutables. Il vous suffit d'annoter vos paramètres constructeur avec@JsonProperty
Bastian Voigt

2
Vous n'avez même plus besoin d'annoter avec @JsonProperty, si vous compilez avec le -parameterscommutateur.
Amir Abiri

2
Jackson peut en fait être configuré pour utiliser un générateur.
Keyhan

1
Si vous ajoutez le module jackson-module-kotlin à votre projet, vous pouvez simplement utiliser des classes de données et cela fonctionnera.
Nils Breunese

2
Comment cela fait-il la même chose qu'un modèle de générateur? Vous instanciez le produit final, puis échangez / ajoutez des informations. L'intérêt du modèle Builder est de ne pas pouvoir obtenir le produit final tant que toutes les informations nécessaires ne sont pas présentes. La suppression du .apply () vous laisse avec une voiture indéfinie. La suppression de tous les arguments du constructeur de Builder vous laisse avec un constructeur de voitures, et si vous essayez de le construire dans une voiture, vous rencontrerez probablement une exception pour ne pas avoir encore spécifié le modèle et l'année. Ce n'est pas la même chose.
ZeroStatic

7

Personnellement, je n'ai jamais vu de constructeur à Kotlin, mais c'est peut-être juste moi.

Toutes les validations dont on a besoin se produisent dans le initbloc:

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

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

Ici, je me suis permis de deviner que vous ne vouliez pas vraiment modelet yeard'être changeant. De plus, ces valeurs par défaut semblent n'avoir aucun sens, (surtout nullpour name) mais j'en ai laissé une à des fins de démonstration.

Une opinion: Le modèle de constructeur utilisé en Java pour vivre sans paramètres nommés. Dans les langages avec des paramètres nommés (comme Kotlin ou Python), il est recommandé d'avoir des constructeurs avec de longues listes de paramètres (peut-être facultatifs).


2
Merci beaucoup pour la réponse. J'aime votre approche mais l'inconvénient est que pour une classe avec de nombreux paramètres, il n'est pas si convivial d'utiliser le constructeur et de tester également la classe.
Keyhan

1
+ Keyhan deux autres façons de faire la validation, en supposant que la validation ne se produit pas entre les champs: 1) utilisez des délégués de propriété là où le setter effectue la validation - c'est à peu près la même chose que d'avoir un setter normal qui effectue la validation 2) Évitez obsession primitive et créer de nouveaux types à passer qui se valident.
Jacob Zimmerman

1
@Keyhan c'est une approche classique en Python, cela fonctionne très bien même pour les fonctions avec des dizaines d'arguments. L'astuce ici est d'utiliser des arguments nommés (non disponibles en Java!)
voddan

1
Oui, c'est aussi une solution qui vaut la peine d'être utilisée, cela semble contrairement à java où la classe de constructeur a des avantages évidents, dans Kotlin ce n'est pas si évident, discuté avec les développeurs C #, C # a également des fonctionnalités de type kotlin (valeur par défaut et vous pouvez nommer les paramètres lorsque constructeur appelant), ils n'ont pas non plus utilisé de modèle de générateur.
Keyhan

1
@ vxh.viet beaucoup de ces cas peuvent être résolus avec @JvmOverloads kotlinlang.org/docs/reference
...

4

J'ai vu de nombreux exemples qui déclarent des amusements supplémentaires en tant que constructeurs. J'aime personnellement cette approche. Économisez des efforts pour écrire des générateurs.

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

    }
}

Je n'ai pas encore trouvé de moyen de forcer l'initialisation de certains champs dans DSL, comme afficher des erreurs au lieu de lancer des exceptions. Faites-moi savoir si quelqu'un sait.


2

Pour une classe simple, vous n'avez pas besoin d'un générateur distinct. Vous pouvez utiliser des arguments de constructeur optionnels comme l'a décrit Kirill Rakhman.

Si vous avez une classe plus complexe, Kotlin fournit un moyen de créer des Builders / DSL de style Groovy:

Constructeurs de type sécurisé

Voici un exemple:

Exemple Github - Générateur / Assembleur


Merci, mais je pensais aussi l'utiliser à partir de Java. Autant que je sache, les arguments facultatifs ne fonctionneraient pas à partir de java.
Keyhan


1

Je suis en retard à la fête. J'ai également rencontré le même dilemme si je devais utiliser le modèle Builder dans le projet. Plus tard, après des recherches, j'ai réalisé que c'était absolument inutile puisque Kotlin fournit déjà les arguments nommés et les arguments par défaut.

Si vous avez vraiment besoin de mettre en œuvre, la réponse de Kirill Rakhman est une réponse solide sur la façon de mettre en œuvre de la manière la plus efficace. Une autre chose que vous pouvez trouver utile est https://www.baeldung.com/kotlin-builder-pattern que vous pouvez comparer et contraster avec Java et Kotlin sur leur implémentation


0

Je dirais que le modèle et la mise en œuvre restent à peu près les mêmes dans Kotlin. Vous pouvez parfois l'ignorer grâce aux valeurs par défaut, mais pour la création d'objets plus compliqués, les constructeurs sont toujours un outil utile qui ne peut pas être omis.


En ce qui concerne les constructeurs avec des valeurs par défaut, vous pouvez même faire la validation de l'entrée en utilisant des blocs d'initialisation . Cependant, si vous avez besoin de quelque chose avec état (pour ne pas avoir à tout spécifier à l'avance), le modèle de générateur est toujours la voie à suivre.
mfulton26

Pouvez-vous me donner un exemple simple avec du code? Dites une classe d'utilisateur simple avec le nom et le champ d'e-mail avec validation pour l'e-mail.
Keyhan

0

vous pouvez utiliser le paramètre facultatif dans l'exemple 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)
}

puis

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

J'ai implémenté un modèle Builder de base dans Kotlin avec le code suivant:

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

et enfin

Java:

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

Kotlin:

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

0

Je travaillais sur un projet Kotlin qui exposait une API consommée par les clients Java (qui ne peuvent pas tirer parti des constructions du langage Kotlin). Nous avons dû ajouter des générateurs pour les rendre utilisables en Java, j'ai donc créé une annotation @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - c'est essentiellement un remplacement de l'annotation Lombok @Builder pour Kotlin.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.