변화로부터 코드를 보호하려면 추상화를 사용하라
//Before
fun isPasswordValid(text: String): Boolean {
if (text.length < 7) return false
// ...
}
//After
const val MIN_PASSWORD_LENGTH = 7
fun isPasswordValid(text: String): Boolean {
if (text.length < MIN_PASSWORD_LENGTH) return false
// ...
}
매직넘버를 상수로 추출하면
//Before
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
//After
fun Context.toast(
message: String,
duration: Int = Toast.LENGTH_SHORT
) {
Toast.makeText(this, message, duration).show()
}
context.toast(message)
자주 쓰이는 부분을 확장 함수로 빼서 사용할 수 있다.
만약 toast가 아닌 snackbar로 출력해야 한다면 어떻게 하면 좋을까?
1) 함수 변경
위와 같이 snackbar를 이용해 확장함수로 만들고 기존의 Context.toast()
를 Context.snackbar()
로 일괄 변경한다. (이름을 직접 바꾸는 건 위험할 수 있다.)
2) 더 높은 레벨의 함수로 옮긴다.
fun Context.showMessage(
message: String,
duration: MessageLength = MessageLength.LONG
) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Toast.LENGTH_SHORT
MessageLength.LONG -> Toast.LENGTH_LONG
}
Toast.makeText(this, message, toastDuration).show()
}
enum class MessageLength { SHORT, LONG }
toast, snackbar 등 구체적인 명칭을 사용하지 않고 추상적인 함수명을 사용할 수 있다.
구현을 추상화할 수 있는 더 강력한 방법으로는 클래스가 있다.
enum class MessageLength { SHORT, LONG }
class MessageDisplay(val context: Context) {
fun show(
message: String,
duration: MessageLength = MessageLength.SHORT
) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Toast.LENGTH_SHORT
MessageLength.LONG -> Toast.LENGTH_LONG
}
Toast.makeText(context, message, toastDuration).show()
}
}
//사용
val messageDisplay = MessageDisplay(context)
massageDisplay.show("message")
클래스는 상태를 가지고 있어, 많은 함수를 가질 수 있다. context는 의존성 주입이 될 수 있으며, mock을 이용해 해당 클래스에 의존하는 다른 클래스의 기능을 테스트할 수 있다.
하지만 여전히 한계가 있으며, open을 사용해 서브클래스를 제공하거나 인터페이스를 사용하는 것이 더 자유를 제공할 수 있다.
인터페이스 뒤에 객체를 숨겨 실질적인 구현을 추상화하고 사용자가 추상화된 것에 의존하도록 하여 유연하게 사용할 수 있도록 한다.
enum class MessageLength { SHORT, LONG }
interface MessageDisplay {
fun show(message: String, duration: MessageLength = MessageLength.SHORT)
}
class ToastDisplay(val context: Context): MessageDisplay {
override fun show(message: String, duration: MessageLength) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Toast.LENGTH_SHORT
MessageLength.LONG -> Toast.LENGTH_LONG
}
Toast.makeText(context, message, toastDuration).show()
}
}
var nextId: Int = 0
// 사용
val newId = nextId++
프로젝트에서 고유 id를 생성하는 코드를 작성할 때, 위와 같이 작성한다면
위와 같은 문제가 발생할 수 있다. 하지만 이 방법을 채택해야 한다면, 코드를 보호할 수 있게 함수와 클래스로 감싸는 것이 좋다.
data class Id(private val id: Int)
private var nextId: Int = 0
fun getNextId(): Id = Id(nextId++)
// 사용
val newId = getNextId()
지금껏 내용을 정리하면 다음과 같다.
또한 이를 구현할 때 다음과 같은 도구를 활용할 수 있다.
하지만 단점도 존재한다.
수많은 추상화를 적용해 코드를 복잡하게 만들 수 있다.
추상화를 잘 이해하려면 단위 테스트, 예제 등 잘 살펴보아야 한다....!