Como implementar o padrão Builder no Kotlin?

146

Oi eu sou um novato no mundo Kotlin. Gosto do que vejo até agora e comecei a pensar em converter algumas de nossas bibliotecas que usamos em nosso aplicativo de Java para Kotlin.

Essas bibliotecas estão cheias de Pojos com setters, getters e classes Builder. Agora, pesquisei no Google qual a melhor maneira de implementar os Builders no Kotlin, mas sem sucesso.

2ª Atualização: A questão é como escrever um padrão de design do Builder para um simples pojo com alguns parâmetros no Kotlin? O código abaixo é minha tentativa de escrever código java e, em seguida, usar o eclipse-kotlin-plugin para converter em 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
fonte
1
você precisa modele yearé mutável? Você os altera após uma Carcriação?
voddan
Eu acho que eles deveriam ser imutáveis, sim. Além disso, você quer ter certeza de que eles são definidos tanto e não vazio
Keyhan
1
Você também pode usar este processador de anotação github.com/jffiorillo/jvmbuilder para gerar a classe do construtor automaticamente para você.
Josef
@JoseF Boa idéia para adicioná-lo ao kotlin padrão. É útil para bibliotecas escritas em kotlin.
Keyhan

Respostas:

273

Em primeiro lugar, na maioria dos casos, você não precisa usar construtores no Kotlin porque temos argumentos padrão e nomeados. Isso permite que você escreva

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

e use-o assim:

val car = Car(model = "X")

Se você absolutamente deseja usar construtores, veja como você pode fazê-lo:

Tornar o Builder um companion objectnão faz sentido porque objects são singletons. Em vez disso, declare-a como uma classe aninhada (que é estática por padrão no Kotlin).

Mova as propriedades para o construtor para que o objeto também possa ser instanciado da maneira regular (torne o construtor privado, se não for o caso) e use um construtor secundário que leve um construtor e delegue para o construtor principal. O código terá a seguinte aparência:

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

Esse código pode ser reduzido adicionalmente usando uma DSL do construtor :

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 alguns valores forem necessários e não tiverem valores padrão, você precisará colocá-los no construtor do construtor e também no buildmétodo que acabamos de definir:

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

Kirill Rakhman
fonte
2
Nada, mas o autor da pergunta perguntou especificamente como implementar o padrão do construtor.
Kirill Rakhman 25/03
4
Devo me corrigir, o padrão do construtor tem algumas vantagens, por exemplo, você pode passar um construtor parcialmente construído para outro método. Mas você está certo, vou acrescentar um comentário.
Kirill Rakhman 28/03
3
@KirillRakhman, que tal chamar o construtor de java? Existe uma maneira fácil de disponibilizar o construtor para java?
Keyhan
6
Todas as três versões podem ser chamados a partir de Java assim: Car.Builder builder = new Car.Builder();. No entanto, apenas a primeira versão possui uma interface fluente, portanto, as chamadas para a segunda e terceira versões não podem ser encadeadas.
Kirill Rakhman
10
Acho que o exemplo do kotlin no topo explica apenas um possível caso de uso. A principal razão pela qual uso construtores é converter um objeto mutável em imutável. Ou seja, preciso modificá-lo ao longo do tempo enquanto estou "construindo" e depois criar um objeto imutável. Pelo menos no meu código, existem apenas um ou dois exemplos de código com tantas variações de parâmetros que eu usaria um construtor em vez de vários construtores diferentes. Mas, para criar um objeto imutável, tenho alguns casos em que um construtor é definitivamente a maneira mais limpa que consigo pensar.
ycomp 8/06
21

Uma abordagem é fazer algo como o seguinte:

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

Amostra de uso:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()
Dmitrii Bychkov
fonte
Muito obrigado! Você fez meu dia! Sua resposta deve ser marcada como SOLUÇÃO.
sVd 23/06
9

Como estou usando a biblioteca Jackson para analisar objetos do JSON, preciso ter um construtor vazio e não posso ter campos opcionais. Todos os campos também precisam ser mutáveis. Então eu posso usar essa sintaxe agradável, que faz a mesma coisa que o padrão Builder:

val car = Car().apply{ model = "Ford"; year = 2000 }
David Vávra
fonte
8
Em Jackson, você realmente não precisa ter um construtor vazio, e os campos não precisam ser mutáveis. Você só precisa anotar seus parâmetros de construtor com@JsonProperty
Bastian Voigt
2
Você nem precisa mais fazer anotações @JsonPropertyse compilar com a -parametersopção.
Amir Abiri
2
Jackson pode realmente ser configurado para usar um construtor.
Keyhan
1
Se você adicionar o módulo jackson-module-kotlin ao seu projeto, poderá usar apenas classes de dados e ele funcionará.
Nils Breunese 20/09/19
2
Como isso está fazendo a mesma coisa que um Padrão de Construtor? Você está instanciando o produto final e depois trocando / adicionando informações. O ponto principal do padrão Builder é não conseguir o produto final até que todas as informações necessárias estejam presentes. A remoção do .apply () deixa você com um carro indefinido. A remoção de todos os argumentos de construtor do Builder deixa você com um Car Builder e, se tentar construí-lo em um carro, provavelmente encontrará uma exceção por ainda não ter especificado o modelo e o ano. Eles não são a mesma coisa.
ZeroStatic
7

Eu pessoalmente nunca vi um construtor em Kotlin, mas talvez seja apenas eu.

Toda a validação necessária é realizada no initbloco:

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

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

Aqui, tomei a liberdade de adivinhar que você realmente não queria modele yearser mutável. Além disso, esses valores padrão parecem não ter sentido (principalmente nullpara name), mas deixei um para fins de demonstração.

Uma Opinião: O padrão do construtor usado em Java como um meio de viver sem parâmetros nomeados. Em linguagens com parâmetros nomeados (como Kotlin ou Python), é uma boa prática ter construtores com longas listas de parâmetros (talvez opcionais).

voddan
fonte
2
Muito obrigado pela resposta. Eu gosto da sua abordagem, mas a desvantagem é para uma classe com muitos parâmetros, não sendo tão amigável usar o construtor e também testar a classe.
Keyhan
1
+ Keyhan: duas outras maneiras de realizar a validação, assumindo que a validação não ocorra entre os campos: 1) use delegados de propriedades onde o setter faz a validação - isso é praticamente a mesma coisa que ter um setter normal que faz a validação 2) Evite obsessão primitiva e criar novos tipos para passar que se validam.
23416 Jacob Zimmerman
1
@Keyhan, esta é uma abordagem clássica em Python, funciona muito bem mesmo para funções com dezenas de argumentos. O truque aqui é usar argumentos nomeados (não disponível em Java!)
voddan
1
Sim, também é uma solução que vale a pena usar, ao contrário do java, onde a classe construtora tem algumas vantagens claras, no Kotlin não é tão óbvio, conversei com desenvolvedores de C #, o C # também possui recursos semelhantes ao kotlin (valor padrão e você pode nomear parâmetros quando chamando o construtor) eles também não usaram o padrão do construtor.
Keyhan
1
@ vxh.viet muitos desses casos podem ser resolvidos com @JvmOverloads kotlinlang.org/docs/reference/…
voddan
4

Eu já vi muitos exemplos que declaram divertimentos extras como construtores. Eu pessoalmente gosto dessa abordagem. Economize esforço para escrever construtores.

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

    }
}

Ainda não encontrei uma maneira que possa forçar a inicialização de alguns campos no DSL, como mostrar erros em vez de gerar exceções. Deixe-me saber se alguém souber.

Arst
fonte
2

Para uma classe simples, você não precisa de um construtor separado. Você pode usar argumentos opcionais do construtor, como Kirill Rakhman descreveu.

Se você possui uma classe mais complexa, o Kotlin fornece uma maneira de criar Builders / DSL no estilo Groovy:

Construtores com segurança de tipo

Aqui está um exemplo:

Exemplo do Github - Construtor / Montador

Dariusz Bacinski
fonte
Obrigado, mas eu estava pensando em usá-lo de java também. Tanto quanto eu sei argumentos opcionais não iria funcionar a partir de java.
Keyhan
1

Estou atrasado para a festa. Eu também encontrei o mesmo dilema se tivesse que usar o padrão Builder no projeto. Mais tarde, depois da pesquisa, percebi que é absolutamente desnecessário, pois o Kotlin já fornece os argumentos nomeados e os argumentos padrão.

Se você realmente precisa implementar, a resposta de Kirill Rakhman é uma resposta sólida sobre como implementar da maneira mais eficaz. Outra coisa que você pode achar útil é que, em https://www.baeldung.com/kotlin-builder-pattern, você pode comparar e contrastar com Java e Kotlin em sua implementação

Farruh Habibullaev
fonte
0

Eu diria que o padrão e a implementação permanecem praticamente os mesmos no Kotlin. Às vezes, você pode ignorá-lo graças aos valores padrão, mas, para criar objetos mais complicados, os construtores ainda são uma ferramenta útil que não pode ser omitida.

Ritave
fonte
Quanto aos construtores com valores padrão, você pode até validar a entrada usando blocos inicializadores . No entanto, se você precisar de algo com estado (para não precisar especificar tudo de antemão), o padrão do construtor ainda é o caminho a percorrer.
precisa saber é o seguinte
Você poderia me dar um exemplo simples com código? Diga uma classe de usuário simples com nome e campo de email com validação para email.
Keyhan
0

você pode usar o parâmetro opcional no exemplo do 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)
}

então

myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")
vuhung3990
fonte
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
fonte
0

Implementei um padrão básico do Builder no Kotlin com o seguinte código:

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 finalmente

Java:

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

Kotlin:

DialogMessage.Builder( context )
       .title("Title")
       .message("")
       .build()
       .show()
Moises Portillo
fonte
0

Eu estava trabalhando em um projeto Kotlin que expôs uma API consumida por clientes Java (que não pode tirar proveito das construções da linguagem Kotlin). Tivemos que adicionar construtores para torná-los utilizáveis ​​em Java, então criei uma anotação do @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation - é basicamente um substituto para a anotação do Lombok @Builder para o Kotlin.

YetAnotherMatt
fonte