Kotlin에서 빌더 패턴을 구현하는 방법은 무엇입니까?


145

안녕하세요 저는 Kotlin 세계의 초보자입니다. 나는 지금까지 본 것을 좋아하고 응용 프로그램에서 사용하는 일부 라이브러리를 Java에서 Kotlin으로 변환하려고 생각하기 시작했습니다.

이 라이브러리는 setter, getter 및 Builder 클래스가있는 Pojo로 가득합니다. 이제 Kotlin에서 빌더를 구현하는 가장 좋은 방법은 무엇인지 알아 보았지만 성공하지 못했습니다.

두 번째 업데이트 : 문제는 Kotlin에서 일부 매개 변수를 사용하여 간단한 pojo에 대한 빌더 디자인 패턴을 작성하는 방법입니다. 아래 코드는 Java 코드를 작성한 다음 eclipse-kotlin-plugin을 사용하여 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
당신이 필요 model하고 year변경할 수 있습니까? Car생성 후 변경 합니까?
voddan

나는 그들이 불변해야한다고 생각합니다. 또한 당신은 그들이 비어 있지 않은지 확인하기를 원합니다
Keyhan

1
github.com/jffiorillo/jvmbuilder Annotation Processor를 사용하여 빌더 클래스를 자동으로 생성 할 수도 있습니다.
JoseF

@JoseF 표준 kotlin에 추가하는 것이 좋습니다. kotlin으로 작성된 라이브러리에 유용합니다.
Keyhan

답변:


272

무엇보다도 기본적으로 명명 된 인수가 있기 때문에 대부분의 경우 Kotlin에서 빌더를 사용할 필요가 없습니다. 이것은 당신이 쓸 수 있습니다

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

다음과 같이 사용하십시오.

val car = Car(model = "X")

빌더를 절대적으로 사용하려면 다음과 같이하십시오.

S는 싱글 톤 companion object이기 때문에 Builder를 만드는 것은 의미가 없습니다 object. 대신 중첩 클래스로 선언하십시오 (Kotlin에서는 기본적으로 정적입니다).

객체를 규칙적으로 인스턴스화 할 수 있도록 속성을 생성자로 이동하고 (생성하지 않아야하는 경우 생성자를 비공개로 설정) 빌더를 사용하여 기본 생성자로 위임하는 보조 생성자를 사용하십시오. 코드는 다음과 같습니다.

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

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

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

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

일부 값이 필요하고 기본값이없는 경우 빌더의 생성자 및 build방금 정의한 메소드에 값을 입력해야합니다 .

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

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


2
아무것도 아니지만 질문의 작성자는 빌더 패턴을 구현하는 방법을 구체적으로 묻습니다.
Kirill Rakhman

4
빌더 패턴에는 몇 가지 장점이 있습니다. 예를 들어 부분적으로 생성 된 빌더를 다른 방법으로 전달할 수 있습니다. 하지만 당신 말이 맞아요, 나는 말을 추가합니다.
Kirill Rakhman

3
@KirillRakhman Java에서 빌더를 호출하는 것은 어떻습니까? 빌더를 Java에 사용 가능하게하는 쉬운 방법이 있습니까?
Keyhan

6
세 가지 버전 모두 Java에서 다음과 같이 호출 할 수 있습니다 Car.Builder builder = new Car.Builder();. 그러나 첫 번째 버전에만 유창한 인터페이스가 있으므로 두 번째 및 세 번째 버전에 대한 호출을 연결할 수 없습니다.
Kirill Rakhman

10
상단의 kotlin 예제는 가능한 유스 케이스 하나만 설명합니다. 내가 빌더를 사용하는 주된 이유는 변경 가능한 객체를 변경 불가능한 객체로 변환하기 위해서입니다. 즉, "빌드"하는 동안 시간이 지남에 따라 변경하고 불변의 객체를 만들어야합니다. 적어도 내 코드에는 여러 가지 생성자가 아닌 빌더를 사용하는 매개 변수의 변형이 많은 하나 또는 두 개의 코드 예제 만 있습니다. 그러나 불변의 객체를 만들기 위해 빌더가 내가 생각할 수있는 가장 깨끗한 방법 인 경우가 있습니다.
ycomp

19

한 가지 방법은 다음과 같은 작업을 수행하는 것입니다.

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

사용 샘플 :

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

고마워요! 당신은 내 하루를했다! 답변은 솔루션으로 표시되어야합니다.
sVd

9

JSON에서 객체를 구문 분석하기 위해 Jackson 라이브러리를 사용하고 있기 때문에 빈 생성자가 필요하며 선택적 필드를 가질 수 없습니다. 또한 모든 필드는 변경 가능해야합니다. 그런 다음 빌더 패턴과 동일한 기능을하는이 멋진 구문을 사용할 수 있습니다.

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

8
Jackson에서는 실제로 빈 생성자를 가질 필요가 없으며 필드를 변경할 필요가 없습니다. 다음과 같이 생성자 매개 변수에 주석을 달아야합니다.@JsonProperty
Bastian Voigt

2
스위치로 @JsonProperty컴파일하면 더 이상 주석을 달지 않아도됩니다 -parameters.
Amir Abiri

2
Jackson은 실제로 빌더를 사용하도록 구성 할 수 있습니다.
Keyhan

1
jackson-module-kotlin 모듈을 프로젝트에 추가하면 데이터 클래스 만 사용하면 작동합니다.
Nils Breunese

2
이것이 빌더 패턴과 같은 일을 어떻게합니까? 최종 제품을 인스턴스화 한 다음 정보를 교환 / 추가합니다. 빌더 패턴의 요점은 필요한 모든 정보가 표시 될 때까지 최종 제품을 얻을 수 없다는 것입니다. .apply ()를 제거하면 정의되지 않은 자동차가 남습니다. Builder에서 생성자 인수를 모두 제거하면 Car Builder가 남게되며 자동차에 작성하려고하면 모델과 연도를 지정하지 않은 예외가 발생할 수 있습니다. 그들은 같은 것이 아닙니다.
ZeroStatic 2014

7

나는 개인적으로 Kotlin에서 건축업자를 본 적이 없지만 아마도 나일 것입니다.

init블록 에서 필요한 모든 유효성 검사가 발생합니다 .

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

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

여기 당신이 정말로 원하는하지 않는 것이 생각하는 자유를했다 modelyear변경 될 수 있습니다. 또한 그 디폴트 값은 아무 의미가없는 것 같다 (특히 null대한 name)하지만 난 데모 용으로 하나 떠났다.

의견 : 명명 된 매개 변수없이 살기위한 수단으로 Java에서 사용되는 빌더 패턴. Kotlin 또는 Python과 같은 명명 된 매개 변수가있는 언어에서는 매개 변수가 긴 (선택적) 매개 변수 목록이있는 생성자를 사용하는 것이 좋습니다.


2
답변 주셔서 감사합니다. 나는 당신의 접근 방식을 좋아하지만 단점은 많은 매개 변수가있는 클래스의 경우 생성자를 사용하고 클래스를 테스트하는 것이 그리 친절하지 않습니다.
Keyhan

1
+ Keyhan은 필드간에 유효성 검사가 발생하지 않는다고 가정하여 유효성 검사를 수행 할 수있는 두 가지 다른 방법을 제시합니다 .1) setter가 유효성 검사를 수행하는 속성 대리자를 사용합니다. 이것은 유효성 검사를 수행하는 일반 setter를 갖는 것과 거의 같습니다 .2) 피하십시오 원시적 인 강박 관념과 새로운 유형을 만들어서 스스로를 검증합니다.
Jacob Zimmerman

1
@Keyhan 이것은 Python의 고전적인 접근 방식이며 수십 개의 인수가있는 함수에서도 매우 잘 작동합니다. 여기서 속임수는 명명 된 인수 (Java에서는 사용할 수 없음)를 사용하는 것입니다.
voddan

1
예, 그것은 또한 가치가있는 솔루션입니다. 빌더 클래스가 분명한 이점을 갖는 Java와는 달리 Kotlin에서는 C # 개발자와 이야기하지 않았으며 C #에는 기능과 같은 kotlin이 있습니다 (기본값은 매개 변수 이름을 지정할 수 있습니다) 호출 생성자) 빌더 패턴을 사용하지 않았습니다.
Keyhan

1
@ vxh.viet 이러한 많은 사례는 @JvmOverloads kotlinlang.org/docs/reference/…
voddan

4

추가 재미를 빌더로 선언하는 많은 예제를 보았습니다. 나는 개인적으로이 접근법을 좋아한다. 빌더를 작성하는 노력을 절약하십시오.

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

    }
}

아직 예외를 throw하는 대신 오류를 표시하는 것처럼 일부 필드를 DSL에서 초기화 할 수있는 방법을 아직 찾지 못했습니다. 아는 사람이 있으면 알려주세요.


2

간단한 수업의 경우 별도의 빌더가 필요하지 않습니다. Kirill Rakhman이 설명한대로 선택적 생성자 인수를 사용할 수 있습니다.

더 복잡한 수업이있는 경우 Kotlin은 Groovy 스타일 빌더 / DSL을 작성하는 방법을 제공합니다.

유형 안전 빌더

예를 들면 다음과 같습니다.

Github 예제-빌더 / 어셈블러


고맙지 만 Java에서도 사용하려고 생각했습니다. 내가 아는 한 선택적 인수는 java에서 작동하지 않습니다.
Keyhan


1

나는 파티에 늦었다. 프로젝트에서 빌더 패턴을 사용해야한다면 같은 딜레마가 발생했습니다. 나중에 Kotlin은 이미 명명 된 인수와 기본 인수를 제공하기 때문에 연구 후에는 절대적으로 불필요하다는 것을 깨달았습니다.

Kirill Rakhman의 답변은 실제로 구현해야하는 경우 가장 효과적인 방법으로 구현하는 방법에 대한 확실한 답변입니다. 당신이 유용하다고 생각하는 또 다른 것은 https://www.baeldung.com/kotlin-builder-pattern 당신은 그들의 구현에서 Java와 Kotlin과 비교하고 대조 할 수 있습니다


0

Kotlin의 패턴과 구현은 거의 동일하게 유지됩니다. 때로는 기본값으로 인해 생략 할 수 있지만 더 복잡한 객체 생성을 위해 빌더는 여전히 생략 할 수없는 유용한 도구입니다.


기본값을 가진 생성자까지는 초기화 블록을 사용하여 입력의 유효성을 검사 할 수도 있습니다 . 그러나 스테이트 풀 한 것이 필요한 경우 (모든 것을 미리 지정할 필요가없는 경우) 빌더 패턴은 여전히 ​​진행 중입니다.
mfulton26

코드로 간단한 예제를 제공해 주시겠습니까? 전자 메일에 대한 유효성 검사와 함께 이름과 전자 메일 필드가있는 간단한 사용자 클래스를 말합니다.
Keyhan

0

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

그때

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

다음 코드를 사용하여 Kotlin에서 기본 빌더 패턴을 구현했습니다.

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

그리고 마지막으로

자바:

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

코 틀린 :

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

0

Java 클라이언트가 사용하는 API를 공개하는 Kotlin 프로젝트를 진행하고있었습니다 (이는 Kotlin 언어 구문을 이용할 수 없음). Java에서 사용할 수 있도록 빌더를 추가해야했기 때문에 @Builder 주석을 작성했습니다. https://github.com/ThinkingLogic/kotlin-builder-annotation- 기본적으로 Kotlin의 Lombok @Builder 주석을 대체합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.