Jak zaimplementować wzorzec Builder w Kotlinie?

145

Cześć, jestem nowicjuszem w świecie Kotlin. Podoba mi się to, co widzę do tej pory i zacząłem myśleć o przekonwertowaniu niektórych naszych bibliotek, których używamy w naszej aplikacji, z Javy na Kotlin.

Te biblioteki są pełne Pojos z klasami ustawiającymi, pobierającymi i budującymi. Teraz szukałem w Google, aby znaleźć najlepszy sposób na wdrożenie Builders w Kotlinie, ale bez sukcesu.

Druga aktualizacja: Pytanie brzmi, jak napisać wzorzec projektowy Builder dla prostego pojo z niektórymi parametrami w Kotlin? Poniższy kod jest moją próbą napisania kodu java, a następnie użycia wtyczki eclipse-kotlin-plugin do konwersji na 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
        }
    }
}
Keyhan
źródło
1
czy potrzebujesz modeli yearbyć zmiennym? Czy zmieniasz je po Carstworzeniu?
voddan
Myślę, że powinny być niezmienne, tak. Chcesz też mieć pewność, że oba są ustawione i nie są puste
Keyhan
1
Możesz również użyć tego procesora adnotacji github.com/jffiorillo/jvmbuilder, aby automatycznie wygenerować klasę konstruktora.
JoseF
@JoseF Dobry pomysł, aby dodać go do standardowego kotlin. Jest to przydatne w przypadku bibliotek napisanych w kotlin.
Keyhan

Odpowiedzi:

272

Przede wszystkim w większości przypadków nie musisz używać konstruktorów w Kotlinie, ponieważ mamy domyślne i nazwane argumenty. To pozwala ci pisać

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

i używaj go tak:

val car = Car(model = "X")

Jeśli absolutnie chcesz korzystać z konstruktorów, oto jak możesz to zrobić:

Tworzenie Buildera companion objectnie ma sensu, ponieważ objectsą one singletonami. Zamiast tego zadeklaruj ją jako klasę zagnieżdżoną (która domyślnie jest statyczna w Kotlinie).

Przenieś właściwości do konstruktora, aby można było również utworzyć wystąpienie obiektu w zwykły sposób (ustaw konstruktor jako prywatny, jeśli nie powinien) i użyj konstruktora pomocniczego, który pobiera konstruktora i delegatów do konstruktora podstawowego. Kod będzie wyglądał następująco:

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

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

Ten kod można dodatkowo skrócić za pomocą konstruktora DSL :

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

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

Jeśli jakieś wartości są wymagane i nie mają wartości domyślnych, musisz umieścić je w konstruktorze kreatora, a także w buildmetodzie, którą właśnie zdefiniowaliśmy:

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

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

Kirill Rakhman
źródło
2
Nic, ale autor pytania konkretnie zapytał, jak zaimplementować wzorzec konstruktora.
Kirill Rakhman
4
Powinienem sam poprawić, wzorzec buildera ma pewne zalety, np. Możesz przekazać częściowo skonstruowany builder do innej metody. Ale masz rację, dodam uwagę.
Kirill Rakhman
3
@KirillRakhman A może zadzwonisz do konstruktora z javy? Czy istnieje łatwy sposób na udostępnienie kreatora w java?
Keyhan,
6
Wszystkie trzy wersje mogą być wywoływane z Java tak: Car.Builder builder = new Car.Builder();. Jednak tylko pierwsza wersja ma płynny interfejs, więc nie można łączyć wywołań drugiej i trzeciej wersji.
Kirill Rakhman
10
Myślę, że przykład kotlin na górze wyjaśnia tylko jeden możliwy przypadek użycia. Głównym powodem, dla którego używam konstruktorów, jest konwersja obiektu zmiennego na niezmienny. Oznacza to, że muszę go z czasem mutować podczas „budowania”, a następnie wymyślać niezmienny obiekt. Przynajmniej w moim kodzie jest tylko jeden lub dwa przykłady kodu, który ma tak wiele wariantów parametrów, że użyłbym konstruktora zamiast kilku różnych konstruktorów. Ale aby stworzyć niezmienny obiekt, mam kilka przypadków, w których budowniczy jest zdecydowanie najczystszym sposobem, jaki mogę wymyślić.
ycomp
19

Jedną z metod jest wykonanie następujących czynności:

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

Próbka użycia:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()
Dmitrii Bychkov
źródło
Wielkie dzięki! Zrobiłeś mój dzień! Twoja odpowiedź powinna być oznaczona jako ROZWIĄZANIE.
sVd
9

Ponieważ używam biblioteki Jackson do analizowania obiektów z formatu JSON, potrzebuję pustego konstruktora i nie mogę mieć pól opcjonalnych. Również wszystkie pola muszą być zmienne. Następnie mogę użyć tej ładnej składni, która robi to samo, co wzorzec Builder:

val car = Car().apply{ model = "Ford"; year = 2000 }
David Vávra
źródło
8
W Jackson nie musisz mieć pustego konstruktora, a pola nie muszą być modyfikowalne. Musisz tylko dodać adnotacje do parametrów konstruktora za pomocą@JsonProperty
Bastian Voigt,
2
Nie musisz już nawet dodawać adnotacji @JsonProperty, jeśli kompilujesz z -parametersprzełącznikiem.
Amir Abiri
2
Jacksona można tak naprawdę skonfigurować do używania konstruktora.
Keyhan,
1
Jeśli dodasz moduł jackson-module-kotlin do swojego projektu, możesz po prostu użyć klas danych i będzie działać.
Nils Breunese,
2
Jak to działa tak samo, jak wzorzec konstruktora? Tworzysz instancję produktu końcowego, a następnie wymieniasz / dodajesz informacje. Celem wzorca Builder jest to, aby nie być w stanie uzyskać produktu końcowego, dopóki nie będą dostępne wszystkie niezbędne informacje. Usunięcie .apply () pozostawia niezdefiniowany samochód. Usunięcie wszystkich argumentów konstruktora z Buildera pozostawia Cię z Konstruktorem samochodów, a jeśli spróbujesz wbudować go w samochód, prawdopodobnie napotkasz wyjątek, ponieważ nie określiłeś jeszcze modelu i roku. To nie to samo.
ZeroStatic
7

Osobiście nigdy nie widziałem budowniczego w Kotlinie, ale może to tylko ja.

Wszystkie potrzebne walidacje odbywają się w initbloku:

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

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

Tutaj pozwoliłem sobie zgadnąć, że tak naprawdę nie chcesz modeli yearbyć zmiennym. Również te domyślne wartości wydają się nie mieć sensu (szczególnie nulldla name), ale zostawiłem jedną dla celów demonstracyjnych.

Opinia: wzorzec konstruktora używany w Javie jako sposób na życie bez nazwanych parametrów. W językach z nazwanymi parametrami (jak Kotlin czy Python) dobrą praktyką jest posiadanie konstruktorów z długimi listami (być może opcjonalnych) parametrów.

voddan
źródło
2
Bardzo dziękuję za odpowiedź. Podoba mi się twoje podejście, ale wadą jest to, że w przypadku klasy z wieloma parametrami korzystanie z konstruktora i testowanie klasy nie jest tak przyjazne.
Keyhan
1
+ Key niż dwa inne sposoby przeprowadzania walidacji, zakładając, że walidacja nie odbywa się między polami: 1) użyj delegatów właściwości, w których ustawiacz przeprowadza walidację - to prawie to samo, co posiadanie normalnego ustawiacza, który wykonuje walidację 2) Unikaj prymitywną obsesję i stwórz nowe typy, które zostaną przekazane, które się sprawdzą.
Jacob Zimmerman
1
@Keyhan to klasyczne podejście w Pythonie, działa bardzo dobrze nawet dla funkcji z dziesiątkami argumentów. Sztuczka polega na użyciu nazwanych argumentów (niedostępnych w Javie!)
voddan
1
Tak, jest to również rozwiązanie warte zastosowania, wydaje się w przeciwieństwie do javy, gdzie klasa builder ma pewne wyraźne zalety, w Kotlin nie jest to takie oczywiste, rozmawiałem z programistami C #, C # również ma funkcje podobne do kotlin (wartość domyślna i możesz nazwać parametry, gdy wywołanie konstruktora), również nie używali wzorca budującego.
Keyhan
1
@ vxh.viet wiele takich przypadków można rozwiązać za pomocą @JvmOverloads kotlinlang.org/docs/reference/ ...
voddan
4

Widziałem wiele przykładów, które deklarują dodatkowe frajdy jako konstruktorzy. Osobiście podoba mi się to podejście. Oszczędzaj wysiłek pisania konstruktorów.

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

    }
}

Nie znalazłem jeszcze sposobu, który mógłby wymusić zainicjowanie niektórych pól w DSL, na przykład pokazywanie błędów zamiast rzucania wyjątków. Daj mi znać, jeśli ktoś wie.

Arst
źródło
2

Do prostych zajęć nie potrzebujesz osobnego kreatora. Możesz skorzystać z opcjonalnych argumentów konstruktora, jak opisał Kirill Rakhman.

Jeśli masz bardziej złożoną klasę, Kotlin zapewnia sposób na tworzenie Groovy style Builders / DSL:

Twórcy bezpiecznych typów

Oto przykład:

Przykład Github - Builder / Assembler

Dariusz Bacinski
źródło
Dzięki, ale myślałem też o użyciu go w Javie. O ile wiem, opcjonalne argumenty nie działałyby z javy.
Keyhan,
1

Spóźniłem się na imprezę. Napotkałem też ten sam dylemat, gdybym musiał użyć wzorca Builder w projekcie. Później, po badaniach, zdałem sobie sprawę, że jest to absolutnie niepotrzebne, ponieważ Kotlin już dostarcza wymienione argumenty i argumenty domyślne.

Jeśli naprawdę musisz wdrożyć, odpowiedź Kirilla Rakhmana jest solidną odpowiedzią na temat tego, jak wdrożyć w najbardziej efektywny sposób. Inną rzeczą, która może Ci się przydać, jest https://www.baeldung.com/kotlin-builder-pattern, którą możesz porównać i porównać z Javą i Kotlinem w zakresie ich implementacji

Farruh Habibullaev
źródło
0

Powiedziałbym, że wzór i implementacja pozostają prawie takie same w Kotlinie. Czasami można to pominąć dzięki wartościom domyślnym, ale w przypadku bardziej skomplikowanego tworzenia obiektów konstruktory są nadal przydatnym narzędziem, którego nie można pominąć.

Ritave
źródło
Jeśli chodzi o konstruktory z wartościami domyślnymi, możesz nawet sprawdzić poprawność danych wejściowych za pomocą bloków inicjalizujących . Jeśli jednak potrzebujesz czegoś stanowego (aby nie musieć określać wszystkiego z góry), to wzorzec konstruktora jest nadal właściwą drogą.
mfulton26
Czy możesz podać mi prosty przykład z kodem? Wypowiedz prostą klasę użytkownika z nazwą i polem e-mail z potwierdzeniem dla adresu e-mail.
Keyhan,
0

możesz użyć opcjonalnego parametru w przykładzie 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)
}

następnie

myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")
vuhung3990
źródło
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 Rude
źródło
0

Zaimplementowałem podstawowy wzorzec Builder w Kotlinie z następującym kodem:

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

I w końcu

Jawa:

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

Kotlin:

DialogMessage.Builder( context )
       .title("Title")
       .message("")
       .build()
       .show()
Moises Portillo
źródło
0

Pracowałem nad projektem Kotlin, który udostępnił API używane przez klientów Java (które nie mogą korzystać z konstrukcji języka Kotlin). Musieliśmy dodać konstruktory, aby były użyteczne w Javie, więc stworzyłem adnotację @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - jest to w zasadzie zamiennik dla adnotacji Lombok @Builder dla Kotlin.

YetAnotherMatt
źródło