확장 함수를 추가할 때 어떤 클래스의 내부로 추가하는 것이 좋지 않다. (즉, 멤버로 추가하는 것이 좋지 않다.)
예를 들면 다음과 같은 함수는
fun String.isPhoneNumber(): Boolean = length == 7 && all { it.isDigit() }
컴파일되면, 다음과 같이 변합니다.
fun isPhoneNumber('$this': String): Boolean = '$this'.length == 7 && '$this'.all { it.isDigit() }
이렇게 단순하게 변환되는 것이므로, 확장 함수를 클래스 멤버로 정의할 수도 있고 인터페이스 내부에 정의할 수도 있다.
interface PhoneBook {
fun String.isPhoneNumber(): Boolean
}
class Fizz: PhoneBook {
override fun String.isPhoneNumber(): Boolean = length == 7 && all { it.isDigit() }
}
이런 코드도 가능하지만 DSL 제외하곤 이를 사용하지 않는 것이 좋다. 특히 가시성 제한을 위해 확장 함수를 멤버로 정의하는 것은 굉장히 좋지 않다.
// bad habbit
class PhoneBookIncorrect {
// ...
fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }
}
한 가지 큰 이유는 가시성을 제한하지 못한다. 확장함수 사용하는 형태를 어렵게 만들 뿐이다. 그래서 위 확장함수를 쓰면 다음과 같이 사용해야 한다.
PhoneBookIncorrect().apply { "1234567890".test() }
확장 함수의 가시성을 제한하고 싶다면 멤버로 만들지 말고 가시성 한정자를 붙여주면 된다.
// 이런 형태로 확장 함수의 가시성을 제한한다.
class PhoneBookCorrect {
//..
}
private fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }
멤버 확장을 피해야하는 몇 가지 타당한 이유를 정리해 보면 다음과 같다.
val ref = String::isPhoneNumber
val str = "123456"
val boundedRef = str::isPhoneNumber
val refX = PhoneBookIncorrect::isPhoneNumber // error
val book = PhoneBookIncorrect()
val boundedRefX = book::isPhoneNumber // error
class A {
val a = 10
}
class B {
val a = 20
val b = 30
fun A.test() = a + b // 40? 50?
}
class A {
// ...
}
class B {
// ...
fun A.update() = ... // A와 B 중에서 어떤 것을 업데이트할까?
}
정리하자면,
멤버 확장 함수를 사용하는 것이 의미가 있는 경우에는 사용해도 괜찮다!
하지만 일반적으로 단점을 인지하고 사용하지 않는 것이 좋다.
가시성을 제한하려면 가시성과 관련된 한정자를 사용하자. 클래스 내부에 확장 함수를 배치한다고 외부에서 해당 함수를 사용하지 못하게 제한되는 것이 아니다.