가독성을 목표로 설계하라
인지 부하 감소
가독성은 사람에 따라 다르게 느낄 수 있다.
// Type:A
if (person != null && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}
// Type:B
person?.takeIf { it.isAdult }
?.let(view::showPerson)
?: view.showError()
가독성은 뇌가 얼마나 많은 관용구에 익숙해졌냐에 따라 다르다. Type:A
의 경우 kotlin을 사용해보지 않은 개발자라도 빠르게 이해할 수 있다. 하지만, Type:B
의 경우 kotlin에서 일반적으로 사용되는 패턴으로 익숙하지 않아 어려울 수 있다.
그리고, 사실 Kotlin을 오래 하지 않으면 Type:B
에 익숙해지기 쉽지 않다.
추가로, Type:A
는 수정하기도 쉽다. if 블록에 새로운 작업이 생긴다면 하단에 한 줄 더 추가하면 된다. 하지만, Type:B
는 함수에서 앨비스 연산자 끝에 추가 작업이 필요하다면 run
등의 함수를 사용해아 한다. 그리고, 디버깅 시스템도 전자를 더 잘 인식한다. Type:B
가 항상 나쁜 것은 아니지만 가독성을 높이기 위해선 자주 사용하는 관용구를 써야 된다.
Unit?을 리턴하지 말 것
Unit?을 리턴하게 된다면, 어떤 경우일까? 다음을 보자.
fun keyIsCorrect(key: String): Boolean = //...
if (!keyIsCorrect(key)) return
다음과 같이 사용할 수도 있다.
fun verifyKey(key: String): Unit? = //...
verifyKey(key) ?: return
이는 사용할 수도 있는 것이지만, 가독성 측면에서 최악이다.
변수 타입이 명확하지 않은 경우 확실하게 지정하라
타입 추론을 활용하면 개발 시간 및 효율성을 향상 시킬 수 있다. 하지만, 이전 포스팅에서 소개했듯 안정성이 떨어진다. 더불어 가독성 측면에서도 좋지 않다. 해당 변수가 어떤 타입인지 모르는 것은 개발자가 타입을 추론해야되고, 이는 코드 해석에 더 많은 시간을 쓴다는 것을 의미한다.
리시버를 명시적으로 참조하라
무언가를 더 자세하게 설명하기 위해, 명시적으로 긴 코드를 작성할 때가 있다. 대표적인 예시가 this
이다. 이를 사용하면, 출처가 어느 곳인지를 명확하게 밝힐 수 있다.
class User: Person() {
private var beersDrunk: Int = 0
fun drinkBeers(num: Int) {
this.beersDrunk += num
}
}
조금 더 복잡한 예시를 보자
class Node(val name: String) {
fun create(name: String): Node? = Node(name)
fun makeChild(childName: String) = create("$name.$childName")
.apply { print("Created ${name}") }
}
fun main() {
val node = Node("parent")
node.makeChild("child")
}
위는 "Create parent.child"가 출력될 것 같이 보이지만, "Create parent'가 출력된다.
다음과 같이 개선할 수 있다.
class Node(val name: String) {
fun create(name: String): Node? = Node(name)
fun makeChild(childName: String) = create("$name.$childName")
.apply { print("Created ${this?.name} in ${this@Node.name}") }
}
fun main() {
val node = Node("parent")
node.makeChild("child")
}
위에서 apply 내부에서 this?
를 사용한 이유는 create의 결과가 Node?
이기 때문이다. 그리고, this@Node
로 부모를 명시적으로 처리하고 있다. 이렇게 리시버를 명확히하면 안정성도 향상되고 가독성도 뛰어나다.
프로퍼티는 동작이 아니라 상태를 나타내야 한다.
코틀린의 프로퍼티는 자바의 필드와 비슷해보이지만, 전혀 다른 개념이다. 같은 점은 둘 다 데이터를 저장한다는 점이다. 하지만 프로퍼티에는 더 많은 기능이 있다. 프로퍼티는 사용자 정의 getter/setter를 가질 수 있다.
var name: String? = null
get() = field?.toUpperCase()
set(value) {
if (!value.isNullOrBlank()) {
field = value;
}
}
프로퍼티는 위와 같이 사용할 수도 있지만, 프로퍼티 위임을 통해 확장 프로퍼티를 만들 수도 있다. 프로퍼티의 본질은 결국 함수이기 때문이다.
하지만, 원칙적으로 프로퍼티는 상태로만 활용해야 한다. 프로퍼티를 사용하는 것 대신 명시적으로 함수를 사용해야될 경우는 다음과 같다.
- 복잡도가 O(1) 보다 크거나 연산 비용이 크다.
- 비즈니스 로직
- 결과값이 고정되지 않은 경우
- 변환을 수행하는 경우
- 상태 변화가 발생하는 경우
반대로 상태를 추출/설정할 때는 프로퍼티를 적극 활용하라. 다음과 같은 코드를 절대 작성하지 마라.
var name: String = ""
fun getName() = name;
fun setName(value: String) {
this.name = value;
}
결국 프로퍼티는 상태를 나타내고, 함수는 행동을 나타내야 한다.
Named Argument를 사용하라.
코드에서 인자의 의미가 명확하지 않은 경우가 있다.
val text = (1..10).joinToString("|")
위에서 "|"는 무엇을 의미할까? 다음과 같이 작성해보자.
val text = (1..10).joinToString(seperator="|")
혹은 변수를 사용해도 괜찮다.
val seperator = "|"
val text = (1..10).joinToString(seperator)
'안드로이드 > Kotlin' 카테고리의 다른 글
6. Effective Kotlin - 함수를 파라미터로 갖는 함수에 inline을 사용하라 (0) | 2022.03.20 |
---|---|
5. Effective Kotlin - 효율성 (0) | 2022.03.20 |
3. Effective Kotlin - 결과 부족 시 null & Failure를 사용하라 (0) | 2022.03.17 |
2. Effective Kotlin - Inferred, Exception (0) | 2022.03.17 |
1. Effective Kotlin - Destructuring declaration (0) | 2022.03.17 |