— kotlin, programming — 2 min read
Kotlin 에서는 대부분의 경우 primary constructor 를 사용하여 object를 생성하는 것이 좋은 방법이다.
그 이해를 돕기 위해서 Java 에서 많이 사용하는 클래스 생성 디자인 패턴에 대해서 살펴보자.
점층적 생성자 패턴은 생성자의 부분집합들을 여러가지 만들어서 사용하는 것이다. 예시를 살펴보자
1class Pizza {2 val size: String3 val cheese: Int4 val olives: Int5 val bacon: Int6 7 constructor(size: String, cheese: Int, olives: Int, bacon:Int){8 this.size = size 9 this.cheese = cheese10 this.olives = olives 11 this.bacon = bacon12 }13 constructor(size: String, cheese: Int, olives: Int): this(size,cheese,olives,0)14 constructor(size: String, cheese: Int): this(size,cheese,0)15 constructor(size: String): this(size, 0)16}
하지만 kotlin 에서는 default argument 라는 좋은 기능을 가지고 있으므로 이것을 사용할 필요는 없다. 위의 코드를 default argument를 사용하면 다음과 같이 변경된다.
1class Pizza(2 val size: String,3 val cheese: Int = 0,4 val olives: Int = 0,5 val bacon: Int = 06)
또한 한가지 더 kotlin 에서 제공하는 기능중에 중요한 기능인 named arguments 가 있다.
object를 생성할때 각각 arguments의 순서들을 모두 기억하는 것은 어렵다. 특히 직접 class 에 대한 코드를 작성하지 않은 사람이라면...
하지만, named arguments 를 사용하면 쉽게 해결할수 있고 좀 더 명확한 코드가 된다.
1// not using named arguments2val villagePizza = Pizza("L", 1, 2, 3)3// using named arguments4val myFavorite = Pizza("L", olives = 3)5val myFavorite = Pizza("L", olives = 3, cheese = 1)
Why we use Default Argument
rather then telescoping constructor pattern
Java 진영에서 주로 사용하는 생성 패턴이다. ( java 에서는 named parameters 와 default arguments 를 제공하지 않기 때문에... )
빌더 패턴은 다음과 같은 것을 가능하게 해주기 때문에 주로 사용된다
예시를 살펴보자.
1class Pizza private constructor(2 val size: String,3 val cheese: Int,4 val olives: Int,5 val bacon: Int,6) {7 class Builder(private val size: String) {8 private var cheese: Int = 09 private var olives: Int = 010 private var bacon: Int = 011 12 fun setCheese(value: Int): Builder = apply {13 cheese = value14 }15 fun setOlives(value: Int): Builder = apply {16 olives = value17 }18 fun setBacon(value: Int): Builder = apply {19 bacon = value20 }21 22 fun build() = Pizza(size, chesses, olives, bacon)23 }24}25// usage26val myFavorite = Pizza.Builder("L").setOlives(3).build()27val villagePizza = Pizza.Builder("L") .setCheese(1)28 .setOlives(2)29 .setBacon(3)30 .build()
하지만 이러한 것들은 모두 named parameters 와 default arguments 를 사용하면 대체될 수 있고 더 많은 장점들을 가지고 있다.
build()
의 호출을 잊은 적이 있을 것이다.하지만 primary constructor 가 builder pattern 보다 항상 유리한 것은 아니다.
빌더는 여러 function 조합으로 사용하기 때문에 이점을 가지고 있다. ( 하나의 프로퍼티를 설정하기 위해 여러개의 파라미터를 받을 수 있다 )
1// builder2val dialog = AlertDialog.Builder(context) 3 .setMessage(R.string.fire_missiles)4 .setPositiveButton(R.string.fire, { d, id ->5 // FIRE MISSILES!6 })7 .setNegativeButton(R.string.cancel, { d, id ->8 // User cancelled the dialog9 }) 10 .create()11
12val router = Router.Builder()13 .addRoute(path = "/home", ::showHome) 14 .addRoute(path = "/users", ::showUsers) 15 .build()
constructor를 사용해서 위와 같이 달성하려고 한다면, 여러 데이터를 담는 다른 타입이 필요하다
1// constructor2val dialog = AlertDialog(3 context,4 message = R.string.fire_missiles,5 positiveButtonDescription =6 ButtonDescription(R.string.fire, { d, id ->7 // FIRE MISSILES!8 }),9 negativeButtonDescription =10 ButtonDescription(R.string.cancel, { d, id ->11 // User cancelled the dialog12 })13)14
15val router = Router( 16 routes = listOf(17 Route("/home", ::showHome),18 Route("/users", ::showUsers)19 )20)
하지만 위 두가지의 표기법들은 kotlin-community 에서는 환영받지 못한다. kotlin은 DSL을 지원하기 때문에 이것을 사용하는 것을 권장한다.
1val dialog = context.alert(R.string.fire_missiles) {2 positiveButton(R.string.fire) {3 // FIRE MISSILES!4 }5 negativeButton {6 // User cancelled the dialog7 }8}9
10val route = router {11 "/home" directsTo ::showHome12 "/users" directsTo ::showUsers13}
다음 아이템에서 이에 대한 자세한 설명을 하기 때문에 여기서는 간단히 알아보고 넘어가자.
DSL은 더 유연하고 더 명확하고 깔끔한 표기법을 지원하기 때문에 더 선호된다. builder를 구현하는 것도 어렵기는 하지만 DSL을 구현하는 것은 더 어려운 일이다.
하지만 이미 좀 더 시간을 투자하여 좋은 표기법을 선택하기로 했으면 단계를 높여서 더 이점이 많은 것을 구현하는 것이 좋은 개발자가 되는 길이지 않을까? ( DSL을 사용하자... )
builder pattern의 결론
Kotlin에서는 사용될 이유들이 거의 없지만 몇몇 경우에 사용할수 있다.