Kotlin Builder Pattern: пошаговое руководство с кодом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Builder в Kotlin — это не просто "копипаста" из Java?

Если вы когда-нибудь писали на Java, вы наверняка сталкивались с классическим Builder pattern. Десятки строк кода, внутренние статические классы, методы set и финальный build(). Всё это работает, но выглядит громоздко.

А теперь представьте, что вы перешли на Kotlin. Язык, который создавался, чтобы быть лаконичным и выразительным. И тут вы сталкиваетесь с задачей: нужно создать объект со множеством параметров, часть из которых — опциональные. Ваша рука тянется написать классический Builder из Java...

Стоп! В Kotlin этот паттерн можно реализовать в 10 раз проще, элегантнее и безопаснее. В этой статье мы разберём 4 способа реализации Builder pattern в Kotlin — от самого простого до продвинутого DSL-подхода.

Этот материал основан на реальном вопросе со Stack Overflow, который набрал тысячи просмотров. Мы адаптировали его для образовательного контента, чтобы вы могли сразу применить знания на практике.

Проблема: когда нужен Builder?

Представьте, что у вас есть класс Pizza:

class Pizza(
    val size: String,
    val cheese: Boolean = false,
    val pepperoni: Boolean = false,
    val mushrooms: Boolean = false,
    val olives: Boolean = false,
    val extraCheese: Boolean = false
)

Вроде бы всё хорошо. Но когда вы создаёте пиццу с 5 опциональными параметрами, код становится нечитаемым:

val pizza = Pizza("Large", true, false, true, false, true)

Глазами искать, какой параметр что означает — мучительно. Тут на помощь и приходит Builder pattern.

Способ 1. Классический Java-style Builder (для совместимости)

Если вам нужно поддерживать Java-код или вы хотите максимально явный API, можно использовать классический подход:

class Pizza private constructor(
    val size: String,
    val cheese: Boolean,
    val pepperoni: Boolean,
    val mushrooms: Boolean,
    val olives: Boolean,
    val extraCheese: Boolean
) {
    class Builder(private val size: String) {
        private var cheese: Boolean = false
        private var pepperoni: Boolean = false
        private var mushrooms: Boolean = false
        private var olives: Boolean = false
        private var extraCheese: Boolean = false

        fun cheese(value: Boolean) = apply { cheese = value }
        fun pepperoni(value: Boolean) = apply { pepperoni = value }
        fun mushrooms(value: Boolean) = apply { mushrooms = value }
        fun olives(value: Boolean) = apply { olives = value }
        fun extraCheese(value: Boolean) = apply { extraCheese = value }

        fun build() = Pizza(size, cheese, pepperoni, mushrooms, olives, extraCheese)
    }
}

// Использование:
val pizza = Pizza.Builder("Large")
    .cheese(true)
    .pepperoni(true)
    .mushrooms(true)
    .build()

Плюсы: очевидный код, понятный Java-разработчикам.
Минусы: много boilerplate-кода, не использует возможности Kotlin.

Способ 2. Kotlin-стиль с именованными аргументами и значениями по умолчанию

Самый простой и часто лучший способ — это вообще не использовать Builder, а положиться на возможности самого языка:

data class Pizza(
    val size: String,
    val cheese: Boolean = false,
    val pepperoni: Boolean = false,
    val mushrooms: Boolean = false,
    val olives: Boolean = false,
    val extraCheese: Boolean = false
)

// Использование:
val pizza = Pizza(
    size = "Large",
    cheese = true,
    pepperoni = true,
    mushrooms = true
)

Это чистый, читаемый и типобезопасный код. Именованные аргументы Kotlin решают проблему "магических чисел" и порядка параметров.

Когда стоит использовать: всегда, когда достаточно значений по умолчанию.

Способ 3. Builder через apply() и скоуп-функции

Иногда вам нужно постепенно собирать объект, передавая его между функциями. Тут пригодится подход с использованием apply():

class PizzaBuilder {
    var size: String = ""
    var cheese: Boolean = false
    var pepperoni: Boolean = false
    var mushrooms: Boolean = false
    var olives: Boolean = false
    var extraCheese: Boolean = false

    fun build(): Pizza = Pizza(
        size = size,
        cheese = cheese,
        pepperoni = pepperoni,
        mushrooms = mushrooms,
        olives = olives,
        extraCheese = extraCheese
    )
}

fun pizza(block: PizzaBuilder.() -> Unit): Pizza {
    return PizzaBuilder().apply(block).build()
}

// Использование:
val pizza = pizza {
    size = "Large"
    cheese = true
    pepperoni = true
    mushrooms = true
}

Это уже DSL-подобный синтаксис. Выглядит как настоящий конструктор пиццы, не правда ли?

Способ 4. Type-safe Builder (DSL) — высший пилотаж

Если вы хотите создать настоящий Domain-Specific Language (DSL) для построения объектов, используйте ресиверы (receiver) и вложенные билдеры:

data class Topping(
    val name: String,
    val double: Boolean = false
)

data class Pizza(
    val size: String,
    val toppings: List<Topping>,
    val cheese: Boolean = false
)

class ToppingBuilder {
    private val toppings = mutableListOf<Topping>()

    fun add(name: String, double: Boolean = false) {
        toppings.add(Topping(name, double))
    }

    fun build(): List<Topping> = toppings.toList()
}

class PizzaBuilder {
    var size: String = "Medium"
    var cheese: Boolean = false
    private val toppingBuilder = ToppingBuilder()

    fun toppings(block: ToppingBuilder.() -> Unit) {
        toppingBuilder.apply(block)
    }

    fun build(): Pizza = Pizza(
        size = size,
        cheese = cheese,
        toppings = toppingBuilder.build()
    )
}

fun pizza(block: PizzaBuilder.() -> Unit): Pizza {
    return PizzaBuilder().apply(block).build()
}

// Использование:
val pizza = pizza {
    size = "Large"
    cheese = true
    toppings {
        add("Pepperoni")
        add("Mushrooms")
        add("Olives", double = true)
    }
}

Внимание: этот подход требует больше кода, но даёт максимальную гибкость и читаемость. Идеально для сложных конфигураций.

Сравнение подходов: что выбрать?

  • Именованные аргументы + default values — для 80% случаев. Просто и эффективно.
  • Java-style Builder — когда нужна совместимость с Java или очень строгий API.
  • apply() + скоуп-функции — для постепенного построения объектов в цепочке вызовов.
  • Type-safe DSL — для сложных иерархических структур (например, конфигурации, HTML-генерация, UI).

Лучшие практики и частые ошибки

Ошибка №1: Использовать Builder там, где не нужно

Если у класса 2-3 параметра — не нужен Builder. Именованные аргументы решают проблему.

Ошиб

Блоги

Рекомендаций по книгам