Menu
Coddy logo textTech

Patron Builder

Fait partie de la section Programmation orientée objet du Journey Kotlin de Coddy. Leçon 48 sur 57.

Le patron builder crée un objet étape par étape et vérifie l’ensemble à la fin. Le builder rassemble les éléments dans des méthodes chaînables qui renvoient le builder lui-même, et build() crée l’objet final :

data class Email(val to: List<String>, val subject: String)

class EmailBuilder {
    private val recipients = mutableListOf<String>()
    private var subject = ""

    fun to(address: String): EmailBuilder {
        recipients.add(address)
        return this
    }
    fun subject(text: String): EmailBuilder {
        subject = text
        return this
    }
    fun build(): Email {
        check(recipients.isNotEmpty()) { "no recipients" }
        return Email(recipients.toList(), subject)
    }
}

Dans main :

val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)

Sortie :

Email(to=[ada@x.io, bo@x.io], subject=Lunch)

apply raccourcit chaque étape : il exécute un bloc avec l’objet comme this et renvoie l’objet, de sorte qu’une étape tient sur une seule ligne. check(condition) { message } lance une IllegalStateException, l’équivalent de require pour l’état de l’objet :

class RequestBuilder {
    var url = ""
    var method = "GET"
    private val headers = mutableListOf<String>()

    fun header(name: String, value: String) = apply { headers.add("$name: $value") }

    fun build(): String {
        check(url.isNotEmpty()) { "no url" }
        return "$method $url " + headers
    }
}

Dans main :

val request = RequestBuilder().apply {
    url = "/users"
    method = "POST"
    header("Accept", "json")
}.build()
println(request)

Sortie :

POST /users [Accept: json]

Une fonction qui accepte une lambda avec récepteur, de type Builder.() -> Unit, transforme le builder en un petit DSL (langage spécifique à un domaine) : à l’intérieur des accolades, le builder est this, de sorte que l’appelant n’écrit que la configuration :

class MenuBuilder(private val title: String) {
    private val items = mutableListOf<String>()

    fun item(name: String, cents: Int) {
        items.add("$name %d.%02d".format(cents / 100, cents % 100))
    }

    fun build() = "== $title ==\n" + items.joinToString("\n")
}

fun menu(title: String, block: MenuBuilder.() -> Unit): String = MenuBuilder(title).apply(block).build()

Dans main :

val lunch = menu("Lunch") {
    item("soup", 450)
    item("salad", 600)
}
println(lunch)

Sortie :

== Lunch ==
soup 4.50
salad 6.00

Les arguments nommés et par défaut de Kotlin couvrent déjà les cas simples : Pizza(size = 30, cheese = true) nomme chaque choix au niveau de l’appel. Un builder devient utile lorsque l’objet est assemblé en plusieurs étapes, collecte des listes ou nécessite des vérifications entre les champs avant de pouvoir exister :

data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true)        // pas besoin de builder

val mail = EmailBuilder()                            // un builder pour un assemblage étape par étape
    .to("ada@x.io")
    .to("bo@x.io")                                  // collecte une liste
    .subject("Plan")
    .build()                                        // vérifie tout à la fin
challenge icon

Défi

Facile

Écrivez un builder pour la classe de données fournie Email. EmailBuilder comporte les étapes chaînables suivantes : to(address) (peut être appelée plusieurs fois et ajoute des destinataires), subject(text), body(text) et urgent(). build() renvoie un Email ou lève IllegalStateException avec no recipients ou no subject (dans cet ordre de vérification). Un e-mail urgent reçoit [URGENT] devant son sujet.

Le code fourni lit des commandes (to ada@x.io, subject Hello, body text, urgent, send), démarre un nouveau builder après chaque ligne --- et affiche l’e-mail construit, To: ada@x.io, bo@x.io | [URGENT] Hello | text, ou error: no subject.

Votre code va dans EmailBuilder.kt et Email.kt. Main.kt contient le code d’entrée/sortie fourni et ne peut pas être modifié.

Essayez vous-même

fun main() {
    // Code d'entrée/sortie fourni : le garder tel quel
    val input = generateSequence(::readLine).toList()
    var builder = EmailBuilder()
    for (cmd in input) {
        when {
            cmd == "send" -> {
                try {
                    println(builder.build())
                } catch (e: IllegalStateException) {
                    println("error: ${e.message}")
                }
            }
            cmd == "---" -> builder = EmailBuilder()
            cmd == "urgent" -> builder.urgent()
            cmd.startsWith("to ") -> builder.to(cmd.drop(3))
            cmd.startsWith("subject ") -> builder.subject(cmd.drop(8))
            else -> builder.body(cmd.drop(5))
        }
    }
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Playground Kotlin