- 추상화를 잘하면 세부 사항을 잘 알지 못하더라도 변경에 자유로워짐
상수
- 리터럴을 상수 프로퍼티로 변경하게 되면 해당 값을 이름을 붙일 수 있고 값을 변경할때에 훨씬 쉽고 안전하게 변경이 가능하게됨
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
fun Context.toast(
message: String,
duration: Int = Toast.LENGTH_LONG
) {
Toast.makeText(this, message, duration).show()
}
- 여기서 스낵바라는 다른 형태의 방식으로 출력해야하는 요구사항 발생
fun Context.snackbar(
message: String,
duration: Int = Snackbar.LENGTH_LONG
) {
//
}
- 위와 같이 스낵바를 출력하는 확장함수를 만들고 기존의 Context.toast를 Context.snackbar로 변경
- 그러나 위와 같으 방법은 다른 모듈이 해당 함수를 의존하고 있을때 영향을 줄 수 있기 때문에 좋지 않음
After
fun Context.showMessage(
message: String,
duration: MessageLength = MessageLength.LONG,
) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Length.LENGTH_LONG
MessageLength.LONG -> Length.LENGTH_LONG
}
Toast.makeText(this, message, toastDuration).show()
}
- 위 요구사항의 경우 메시지 출력하고 싶다는 의도 자체가 중요하기 duration을 enum으로 관리 및 입력을 받고 함수명을 메시지 출력만 하는 역할로써 높은 레벨의 함수로 옮김
클래스
- 클래스는 상태를 가질 수 있고 많은 함수를 가질 수 있기 때문에 함수보다 더욱 다양한 기능의 추상화 도구로 사용할 수 있음
class MessageDisplay(
val context: Context
) {
fun show(
message: String,
duration: MessageLength = MessageLength.LONG
) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Length.LENGTH_LONG
MessageLength.LONG -> Length.LENGTH_LONG
}
Toast.makeText(this.context, message, toastDuration).show()
}
}
fun main() {
val messageDisplay = MessageDisplay(Context())
messageDisplay.show("message")
}
- 의존성 주입 프레임 워크를 사용해 클래스 생성 위임할 수 있음
@Inject lateinit var messageDisplay: MessageDisplay
- mock 객체를 활용해 해당 클래스에 의존하는 다른 클래스의 기능을 테스트 할 수 있음
val messageDisplay: MessageDisplay = mockk()
messageDisplay.setChristmasMode(true)
- 하지만 더 많은 확장성을 얻기 위해서는 인터페이스를 활용하는 것이 좋음
인터페이스
- 라이브러리의 경우 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 노출하는 코드를 많이 사용
- 이렇게 하면 사용자가 클래스를 직접 사용하지 못해 라이브러리를 만드는 사람은 인터페이스만 유지하면 되기 때문에 사용자 측과의 결합을 줄일 수 있음
- 참고로 코틀린은 멀티 플랫폼 언어라 환경에 따라 다른 리스트를 리턴 및 플랫폼에 최적화를 시키고 있음
interface MessageDisplay {
fun show(
message: String,
duration: MessageLength = MessageLength.LONG
)
}
class ToastDisplay(
val context: Context,
): MessageDisplay {
override fun show(message: String, duration: MessageLength) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Length.LENGTH_LONG
MessageLength.LONG -> Length.LENGTH_LONG
}
Toast.makeText(this.context, message, toastDuration).show()
}
}
- 인터페이스로 구성하면 해당 인터페이스의 의도만 제공하고 다양한 플랫폼에서 의도에 맞는 다양한 기능 직접 구현해 사용할 수 있음
- 추가로 테스트시에 인터페이스 faking이 클래스 mocking보다 간단하기 때문에 별도의 mocking 라이브러리를 사용하지 않아도됨
- 추가로 선언과 사용이 분리되어 있기 때문에 ToastDisplay 등의 실제 클래스를 자유롭게 변경할 수 있음
추상화가 주는 자유
- 아래와 같이 추상화하는 방법을 통해 추상화를 진행하게 되면 변화에 대한 자유를 제공함
- 상수로 추출
- 동작을 함수로 래핑
- 함수를 클래스로 래핑
- 인터페이스 뒤에 클래스를 숨김
- 보편적인 객체를 특수한 객체로 래핑
추상화의 문제
- 모든 것을 추상화하게 되면 불필요하게 너무 많은 것을 숨기게 되고 간단한 의도나 동작을 이해하기 더욱 어려워질 수 있음
- 그래서 팀의 크기나 도메인 복잡도 등을 고려해 적절한 추상화를 진행해야함
- 적절한 균형을 찾는 것은 거의 예술에 가까운 것이기 때문에 많은 경험이 필요한 영역
정리
- 추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것이 아니고 코드를 변경해야할때 자유를 주는 것
- 하지만 너무 과한 추상화는 오히려 이해하기 힘든 구조로 변질될 수 있음
'Reading Record > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item 31 - 32. 문서 규약과 추상화 규약 (0) | 2022.05.29 |
---|---|
[이펙티브 코틀린] Item 28 ~30. API 안정성 및 가시성 (0) | 2022.05.29 |
[이펙티브 코틀린] Item 26. 함수 내부의 추상화 레벨을 통일하라 (0) | 2022.05.29 |
[이펙티브 코틀린] Item25. 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라 (0) | 2022.05.02 |
[이펙티브 코틀린] Item24. 제네릭 타입과 variance 한정자를 활용하라 (0) | 2022.05.02 |