[Effective Kotlin] 아이템 33. 생성자 대신 팩토리 함수를 사용하라

0

클라이언트가 클래스의 인스턴스를 만들게 하는 가장 일반적인 방법은 기본 생성자(Primary constructor)를 사용하는 방법이다.

class MyLinkedList<T>{
	val head : T
    val tail : MyLinkedList<T>?
}

val list = MyLinkedList(1, MyLinkedList(2, null))

하지만 생성자가 객체를 만들수 있는 유일한 방법은 아니다.

헬퍼 클래스를 생각해보자.

fun <T> myLinkedListOf(

): MyLinkedList<T>? {
 if(elements.isEmpty()) return null
 val head = elements.first()
 val elementsTail = elements.
 copyOfRange(1, elements.size)
 val tail = myLinkedListOf(*elementsTail)
 return MyLinkedList(head, tail)
}

val list = myLinkedListOf(1, 2)

생성자의 역할을 대신 해주는 함수를 팩토리 함수라고 부른다.

생성자 대신 팩토리 함수를 사용하면 생기는 장점들이다.

  • 생성자와 다르게 함수에 이름을 붙일 수 있다.
    예를들어 ArrayList(3) 일때, 무엇을 의미하는지 알수 없는데 ArrayList.withSize(3)이라는 이름이 붙어있다면 훨씬 유용할 것이다.

  • 함수가 원하는 형태의 타입을 리턴할 수 있다.
    인터페이스 뒤에 실체 객체 구현을 숨길때 유용하게 사용할 수 있다.
    listOf는 List 인터페이스를 리턴하는데, 플랫폼에 따라 다른 빌트인 컬렉션으로 만들어진다.
    또한 인터페이스를 리턴하기때문에, 인터페이스를 지켜서 만들어진다면 어떤 클래스라도 잘동작할 수 있다.

  • 새 객체를 만들 필요가 없다.
    싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나, 캐싱 메커니즘을 사용할수도 있다.
    또한 객체를 만들수 없을 경우, null을 리턴하게 만들수도 있다.
    예를 들어 Connction.createOrNull()은 연결이 생성될수 없을때 null을 리턴한다.

  • 아직 존재하지 않는 객체를 리턴할 수 있다.
    프로젝트를 빌드하지않고도, 앞으로 만들어질 객체를 사용하거나 프록시를 통해 만들어지는 객체를 사용할 수도 있다.

  • 객체 외부에 팩토리 함수를 만들면, 그 가시성을 원하는대로 제어할 수 있다.
    같은 모듈에서만 접근하게 할수 있다던가..

  • 팩토리 함수는 인라인으로 만들 수 있으며, 파라미터들을 refied로 만들수 있다.

  • 팩토리 함수는 생성자로 복잡한 객체도 만들어 낼수 있다.

  • 생성자는 즉시 슈퍼클래스나, 기본생성자를 호출해야하지만, 팩토리 함수는 원하는때에 생성자를 호출할 수 있다.


fun makeListView(config : Config) : ListView{
val items = ... // config로부터 요소를 읽어 들인다.

return ListView(items) // 진짜 생성자를 호출한다.

}

다만 서브 클래스 생성에는 슈퍼클래스의 생성자가 필요하기 때문에, 서브 클래스를 만들어 낼수 없다.


class IntLinkedList :MyLinkedList<Int>(){
 
 construce(vararg ints : Int) : myLinkedListOf(*ints)
 //오류 발생 
 
}

하지만, 팩토리 함수로 슈퍼 클래스를 만들기로 햇다면, 서브클래스에도 팩토리 함수를 만들면된다.

class MyLinkedIntList(head : Int , tail : MyLinkedIntList?) : MyLinkedList<Int>(head, tail)

fun myLinkedIntListOf(vararg elements : Int) : MyLinkedIntList? {
if(elements.is Empty()) return null

val head = elements.first()

val elemenstTail = elemnts.copyOfRange(1, elements.size)

val tail = myLinkedIntListOf(*elementsTail)

return MyLinkedIntList(head, tail)


}

이전 생성자보다 길지만, 유연성, 클래스 독립성, nullable을 리턴하는 다양한 특징 갖는다.

팩토리 함수는 굉장히 강력한 객체 생성방법중 하나이지만, 기본생성자를 사용하지 말라는 얘기가 아니다.

팩토리 함수는 기본생성자가 아닌 추가적인 생성자(secondary constructor)와 경쟁관계이다.

팩토리 함수에는 어떤것들이 있는지 알아보자.

1. Compaion 객체 팩토리 함수

팩토리 함수를 정의하는 가장 일반적인 방법은 Companion 객체를 사용하는것이다.

 class MyLinkedList<T>(
 	val head : T,
    val tail : MyLinkedList<T>?
 ){
 	companion object{
     fun <T> of(vararg elements : T) : MyLinkedList<T>? {
     
     }
    }
 }
 
 val list = MyLinkedList.of(1,2)

다른 프로그래밍은 이와같은 방식은 이름을 가진 생성자 라고 표현한다.

코틀린에서는 인터페이스에서도 이러반 접근방법을 구현할수 있다.

 class MyLinkedList<T>(
 	val head : T,
    val tail : MyLinkedList<T>?
 ){
 
 }
interface MyList<T>{

 	companion object{
     fun <T> of(vararg elements : T) : MyLinkedList<T>? {}
   }
 }
 
 val list = MyLinkedList.of(1,2)

이외에도 다음과 같은 이름을 많이 사용한다.

  • from
    파라미터를 하나 받고, 같은 타입의 인스턴스를 리턴하는 타입변환 함수
    val data: Data = Date.from(instant)

  • of
    파라미터를 여러개 받고, 인스턴스를 만들어 주는 함수
    val faceCards : Set<Rank> = EnumSet.of(JACK , QUEEN , KING)

  • valueOf
    of와 비슷한 기능을 하면서도 의미를 쉽게 읽을수 있게 이름을 붙인 함수
    val prime :BigInteger = BigInteger.valueOf(Integer.MAX_VALUE)

  • instacne 또는 getInstance
    싱글턴으로 인스턴스 하나를 리턴하는 함수이다.
    val StackWalker = StackWalker.getInsetance(option)

  • createInstance 또는 newInstance
    함수를 호출할때마다, 새로운 인스턴스를 반환한다.
    val newArray.newInstance(classObject , arrayLeen)

  • getType
    getInstance처럼 작동하지만, 팩토리함수가 다른 클래스에 있을때 사용한다.
    타입은 팩토리 함수에서 리턴하는 타입이다.
    val fs : FileStore = File.getFilesStore(path)

  • newType
    newInstance처럼 작동하지만, 팩토리함수가 다른 클래스에 있을때 사용한다.
    타입은 팩토리 함수에서 리턴하는 타입이다.
    val br : BuffedReader = File.newBufferedReader(path)

경험이 없는 코틀린 개발들은 companion 개체를 단순한 정적 멤버처리 처럼 다루는경우가 많으나,
companion 객체는 인터페이스를 구현할 수 있고, 클래스를 상속받을 수 있다.

abstract class ActivityFactor{
	abstract fun getIntent(context: Cotnext) : Intent
	
    fun start(context : Context)[
    	val intent = getIntent(context)
        context.startActivity(intent)
    }
   
}

class MainActivity : xxx{

companion object : ActivityFactory(){
	overide fun getIntext(cotnext: Context) Intent =
    Intent(context ,MainActivity::class.java
	}
}

//사용

val intent = MainActivity.getIntent(context)
MainActivity.start(context)
MainAcitivity.startForResult(activity, requestCode)

또한 compainon 객체 팩토리는 값을 가질수 있어서 캐싱을 구현하거나, 가짜 객체를 생성할 수 있다.

확장 팩토리 함수

compainon 객체가 존재할때, 다른 파일에서 해당 객체의 companion 객체를 만들면 어떻게 해야할까?

다음과 같이 Tool 인터페이스가 있다.

interface Tool{
	compainon obejct{}
} 

아래와 같이 companion 객체를 활용해서 확장함수를 정의할 수 있다.

fun Tool.Compainon.createBigTool : BigTool{

}

다만 compainon객체를 확장하려면, 적어도 비어있는 컴패니언 객체가 필요하다.

가짜 생성자

코틀린의 생성자는 톱레벨 함수와 같은 형태로 사용된다.

class A
val a = A()

그래서 다음과 같이 톱 레벨 함수처럼 참조될 수 있다.

일반적으로 대문자로 시작하는지 아닌지는 생성자와 함수를 구분하는 기준이다.

만약 쓰인다면 다른 용도로 사용된다.

List와 MutableList는 인터페이스이기에 생성자를 가질수 없지만, List를 생성자처럼 사용하는 코드를 보았을것이다.

public inline fun<T> MutableList(
 size: Int,
 init: (index: Int) -> T
) : MutavleList<T> {
	val list = ArrayList<T>(size)
    repeat(size) {index -> list.add(init(index)}
    retirm list
}

이러한 톱레벨 함수는 생성자처럼 보이고 생성자 처럼 작동한다.
하지만 팩토리함수와 같은 모든 장점을 갖는다.

많은 개발자가

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글