## 2장 가독성
### 아이템11 가독성을 목표로 설계 하라
```kotlin
if (person != null && person.isAdult) {
view.showPerson(person)
view.hideProgressWithSuccess()
} else {
view.showError()
view.hiseProgress()
}
person?.takeIf { it.isAdult }
?.let {
view.showPerson(person)
view.hideProgressWithSuccess()
} ?: run {
view.showError()
view.hideProgress()
}
```
A/B 코드 중에 A 코드가 명확하고, 이해하기가 더 쉽다. B 코드는 코틀린 스러워 보이지만, 오히려 디버깅을 어렵게 만든다. 가독성을 떨어트리기 때문이다.
기본적으로 인지 부하를 줄이는 방향으로 코드를 작성 해야 한다.
let으로 인해서 예상하지 못한 결과가 나올 수 있다고 하여, let을 쓰지 말아야 하는 것은 아닙니다.
let을 써야 하는 경우
1. 연산을 argument 처리 후로 이동시킬 때
2. 데코레이터를 사용해서 객체를 wrapping 할 때
```kotlin
stduents
.filter { it.result >= 50}
.joinToString(separator = "\n") {
"${it.name} ${it.surname} ${it.result}"
}
.let(::print)
val obj = FileInputStream("/file.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.let(::ObejctInputStream)
.readObject() as SomeObject
```
## 아이템 12 연산자 오버로드를 할 때는 의미에 맞게 사용하라
연산자 오버로딩은 정확한 표현식으로 아주 제한적으로 사용하는 것이 좋다.
| 연산자 | 대응 함수 |
| ------ | ------------------ |
| +a | a.unaryPlus() |
| -a | a.unaryMinus() |
| !a | a.not() |
| ++a | a.inc() |
| --a | a.dec() |
| a+b | a.plus(b) |
| a-b | a.minus(b) |
| `a*b` | a.times(b) |
| `a/b` | a.div(b) |
| a..b | a.rangeTo(b) |
| a in b | a.contains(b) |
| a+=b | a.plusAssign(b) |
| a-=b | a.minusAssign(b) |
| a*=b | a.timesAssign(b) |
| a/=b | a.divAssign(b) |
| `a==b` | a.equals(b) |
| a>b | a.compareTo(b) > 0 |
| a<b | a.compareTo(b) < 0 |
| a>=b | a.compareTo(b) >= 0 |
| a<=b | a.compareTo(b) <= 0 |
코틀린에서 각 연산자의 의미는 항상 같게 유지 된다. 스칼라와 같은 일부 프로그래밍 언어는 무제한 연산자 오버로딩을 지원한다. 이는 해당 기능을 오용하도록 만든다.
```kotlin
val abc = "A" { "B" } and "C" // 최악의 코드
operator fun String.invoke(f: () -> String): String = this + f()
infix fun String.and(s: String) = this + s
```
```kotlin
operator fun Int.times(operation: () -> Unit) {
repeat(this) { operation() }
}
```
의미가 명확하지 않다면, infix를 활용한 확장 함수를 사용하는 것이 좋다.
```kotlin
infix fun Int.timesRepated(operation: ()-> Unit) {
repeat(this) { operation() }
}
```
그러나, stdlib에 구현되어 있는 함수들이 있는지 먼저 찾아보고 해당 함수를 사용하는 것이 좋다.
## 아이템13 Unit?을 리턴하지 말라
함수에서 Unit?을 리턴하지 않는 것이 좋고, 이유는 Unit?의 의미가 Unit 이거나 null이라는 의미인데, 이런 경우 이렇게 하지 않고 Boolean 타입을 사용하는 것이 가독성에 더 유리하다.
```kotlin
fun verifyKey(key: String): Unit?
verifyKey(key) ?: return
```
unit 보다는 차라리 verifyKey 함수를 Boolean으로 변경하여 사용하는 것이 혼동을 줄일 수 있다.
## 아이템 14 변수 타입이 명확하지 않은 경우 확실하게 지정하라
코틀린은 기본적으로 타입 추론을 굉장히 높은 수준으로 제공한다. 하지만, 유형이 명확하지 않을때에는 의도하지 않은 타입으로 추론을 하기 때문에 남용하지 않고, 직접 명확하게 명시하는 것이 좋습니다.
```kotlin
val data = getReviewData() // 어떤 타입인지 명확하게 추론 불가
```
위 코드를 보면, 해당 data 변수에는 어떤 타입인지 명확하게 알 수 없기 때문에 해당 함수의 명세를 확인 해야 하므로 가독성을 떨어트리게 된다.
## 아이템 15 리시버를 명시적으로 참조하라
```kotlin
fun <T: Comparable<T>> List<T>.quickSort(): List<T> {
if (size < 2) return this
val pivot = first()
val (smaller, bigger) = drop(1).partition { it < pivot }
return smaller.quickSort() + pivot + bigger.quickSort()
}
```
```kotlin
fun <T: Comparable<T>> List<T>.quickSort(): List<T> {
if (size < 2) return this
val pivot = this.first()
val (smaller, bigger) = this.drop(1).partition { it < pivot }
return smaller.quickSort() + pivot + bigger.quickSort()
}
```
대표적으로 함수와 프로퍼티를 지역 또는 탑 레벨 변수가 아닌 다른 리시버로부터 가져온다는 것을 나타낼때 명시적으로 표현하는 것이 좋다.
리시버가 명확하지 않다면, 명시적으로 리시버를 적어서 이를 명확하게 해주는게 좋고, 레이블 없이 리시버를 사용하면, 가장 가까운 리시버를 의미한다. 외부에 있는 리시버를 사용하려면, 레이블을 사용해야 한다.
```kotlin
class Node(val name: String) {
fun makeChild(childName: String) = create("$name.$childName").apply {
println("Created ${this?.name} in ${this@Node.name}")
}
fun create(name: String): Node? = Node(name)
}
fun main() {
val node = Node("parent")
node.makeChild("child")
}
```
DSL에서 외부 스코프에 있는 리시버를 명시적으로 적게 강제하고 싶다면, `DslMarker` 메타 어노테이션을 사용한다.
```kotlin
table {
tr {
td { +"Column 1" }
td { +"Column 2" }
tr { // 컴파일 오류를 발생 시켜야 함.
td { +"value 1" }
td { +"value 2" }
}
}
}
@DslMarker
annotation class HtmlDsl
fun table(f: TableDsl.() -> Unit) {}
@HtmlDsl
class TableDsl {}
table {
tr {
td { +"Column 1" }
td { +"Column 2" }
this@table.tr { // 컴파일 오류
td { +"value 1" }
td { +"value 2" }
}
}
}
```
## 아이템 16 프로퍼티는 동작이 아니라 상태를 나타내야 한다.
코틀린의 프로퍼티와 자바의 필드는 비슷하지만, 개념이 다르다. 둘 다 모두 데이터를 저장한다는 것은 같지만, 프로퍼티에는 더 많은 기능이 있다. 프로퍼티는 사용자 정의 setter, getter를 가질 수 있다.
```kotlin
var name: String? = null
get() = field?.toUpperCase()
set(value) {
if (!value.isNullOrBlank()) {
field = value
}
}
```
프로퍼티에 데이터를 저장해두는 backing field에 대한 레퍼런스가 바로 `field`이다. 이런 백킹 필드는 기본적으로 생성되고, val을 사용하는 경우 `field`라는 백킹 필드가 만들어지지 않는다.
var을 사용해서 만든 경우, 해당 프로퍼티를 derived property (파생 프로퍼티)라고 부른다.
개념적으로 val의 경우는 getter, var의 경우 setter, getter를 나타낸다. 그래서 코틀린은 인터페이스에도 프로퍼티를 정의할 수 있다.
원칙적으로 프로퍼티는 상태를 나타내거나 설정하기 위한 목적으로만 사용하는 것이 좋고, 다른 로직등을 포함하지 않아야 한다.
### 프로퍼티 대신 함수를 사용하는 것이 좋은 경우
- 연산 비용이 높거나, 복잡도가 O(1)보다 큰 경우
- 비즈니스 로직을 포함 하는 경우
- 깂이 결정적이지 않은 경우
- 값을 변환하는 경우
- getter에서 프로퍼티의 상태 변경이 일어나야 하는 경우
상태를 추출/설정 할 때는 프로퍼티를 활용해야 한다. 특별한 이유가 아닌데 함수를 사용하면 안 된다.
```kotlin
// Don't Use
class UserIncorrect {
private var name: String = ""
fun getName() = name
fun setName(value: String) {
this.name = value
}
}
```
## 아이템 17 이름 있는 argument를 사용하라
```kotlin
// Bad
val text = (1..10).joinToString("|")
// Good
val text = (1..10).joinToString(separator = "|")
```
이름 있는 argument를 사용하면, 명확하게 구분 된다는 점에서 가독성이 향상된다.
### Named argument를 언제 사용해야 할까?
- 이름 기반으로 값이 무엇을 나타내는지 알 수 있음.
- 파라미터 입력 순서와 상관 없으므로 안전함.
다만, 코드가 길어지는 단점이 있다.
#### default argument의 경우
```kotlin
fun calc(a: Int = 0, b: Int = 0) {}
```
#### 같은 타입의 파라미터가 많은 경우
```kotlin
fun sendEmail(to: String, message: String, ref: String) {}
```
#### 함수 타입의 파라미터가 있는 경우
일반적으로 함수 타입 파라미터는 마지막 위치에 배치하는 것이 좋다.
```kotlin
fun calc(leftValue: Int, rightValue: Int, f: () -> Unit) {}
```
## 아이템 18 코딩 컨벤션을 지켜라
코틀린 문서의 `Coding Convensions`을 보면, 코틀린은 굉장히 잘 정리 된 코딩 컨벤션을 가지고 있다.
코딩 컨벤션을 지키면 이점은 아래와 같다.
- 어떤 프로젝트이더라도 쉽게 이해할 수 있다.
- 다른 외부 개발자와 프로젝트의 코드를 쉽게 이해 가능
- 다른 개발자도 코드의 작동 방식을 쉽게 추측
- 코드를 병합하고, 한 프로젝트의 코드 일부를 다른 코드로 이동하는 것이 쉽다.
'프로그래밍 > Kotlin' 카테고리의 다른 글
kotlin + junit의 생성자 주입시 오류 (0) | 2023.07.21 |
---|---|
kotest 생성자 bean 주입 오류 - Specs must have a public zero-arg constructor (0) | 2023.07.18 |
Kotest 기본만 알아보자 (0) | 2023.04.17 |
이펙티브 코틀린 3장 재사용성 (0) | 2022.10.23 |
이펙티브 코틀린 1장 안정성 (2) | 2022.09.28 |