코틀린 DSL 관련 내용이 많이 나오는데, 이부분은 생략하고 추후에 따로 공부해야할 듯 하다.
아이템 11. 가독성을 목표로 설계하라
아래의 코드를 보면 일반적으로 A의 코드가 B보다 읽기 쉽다.
B의 코드는 코틀린에서 자주 사용하는 구문들을 사용했음에도, 코틀린 숙련자들도 읽기 쉽지 않다.
// 1. 읽기 쉽다
if (person !- null && person.isAdult) {
view.showPerson(person)
} else {
view.sohwError()
}
// 2. 읽기 어렵다
person?.takeIf { it.isAdult }
?.let(view::showPerson)
?: view.showError()
A의 구문은 B보다 아래와 같은 장점이 있다.
- 읽기 쉽다.
- 디버깅이 용이하다.
- IDE같은 디버깅 도구도 A코드에서 더 도움을 줄 확률이 높다.
그렇다고 코틀린의 let과 같은 구문을 쓰면 안될까?
당연히 적절한 상황에 적절하게 쓰는 것이 중요하다.
fun printName() {
person?.let {
println(it.name)
}
}
일반적으로 위와 같은 상황에 많이 사용되는데, 이런 관용구는 많은 사람들에게 쉽게 이해될 수 있다.
또한 let을 자주 사용하는 예시로 아래 두가지가 있다.
1. 연산을 아규먼트 처리 후로 이동시킬 때
print(students.filter{}.joinToString{}) // 변경 전
students.filter{}.joinToString{}.let(::print) // 변경 후
students
.filter( it.result >= 50 )
.joinToString(separator = "\n") {
"${it.name} ${it.surname}, ${it.result}"
}
.let(::print)
2. 데코레이터를 사용해서 객체를 랩할 때
var obj = FileInputStream("/file.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.let(::ObjectInputStream)
.readObject() as SomeObject
위의 코드는 경험이 적은 코틀린 개발자는 이해하기 어려울 수 도 있지만, 그 정도의 비용은 지불할만한 가치가 있을 정도로 유용하기 때문에 사용한다.
어떤 코드가 비용을 지불하면서라도 사용할만한 코드인지는 항상 고민해봐야 될 것같다.
아이템 12. 연산자 오버로드를 할 때는 의미에 맞게 사용하라
코틀린에서는 연산자를 오버로드 할 수 있다.
val money1 = Money(10)
val money2 = Money(10)
println(money1 + money2) // 20
단, 이렇게 연산자를 오버로드할 경우에는 항상 의미에 맞게 오버로드를 해야한다.
만약 위 예제에서 + 로 연산했는데, 실제 동작은 빼기(-) 나 곱하기(*)와 같은 동작을 수행하면 안되겠지?!
명확한 의미를 나타내고 싶다면 infix를 활용해 명확한 이름 명명하도록 확장함수 혹은 톱레벨 함 사용을 권장한다고 한다.
아이템 13. Unit? 을 리턴하지 말자
Unit? 리턴을 지양해야 하는 이유는 가독성 때문이다.
getData()?.let{ view.showData(it) } ?: view.showError()
결과의 null인지 아닌지에 따라 해석하기가 어렵다.(뇌로 직관적으로 해석하기 어려움)
그래서 아이템11 의 조언처럼 아래로 표한하는 것이 가독성이 좋다
if (person != null && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}
아이템 14. 변수타입이 명확하지 않은 경우 확실하게 지정하라
아래 타입은 타입을 지정하지 않아도 어떤 타입인지 유추하기 쉽다.
val ids = listOf(1, 2, 3)
그러나 아래의 타입같은 경우에는 함수를 통해 반환받고 있고, 어떤타입인지 유추하기 어렵다.
이러한 경우에는 오히려 타입을 명시해주는 것이 좋을 수 있다.
val data = getData() // 어떤 데이터 타입인지 모르겠음
물론 IDE등의 기능을 통해 타입을 쉽게 확인할 수 있지만, 깃허브와 같은 공간에서 코드를 확인한다면 타입을 추론하기 어려울 것이다.
val data: UserData = getData()
<타입을 써야하는 이유 등에 대한 파트>
- 1장 아이템 3 : 플랫폼 타입을 최대한 사용하지 말라
- 1장 아이템 4: inferred 타입으로 리턴하지 말라
아이템 15. 리시버를 명시적으로 참조하라
리시버(ex this, it)가 여러개 있는 경우 헷갈릴 수 있기 때문에 명확히 명시해주자.
마냥 짧게 적을 수 있다고 가독성에 좋은 것은 아님.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.apply { println("Created ${name}") }
fun create(name: String): Node? = Node(name)
}
위 코드를 보면 name이 어디의 name인지 굉장히 헷갈리는 코드이다.
그래서 this를 붙여서 사용하는 방식으로 개선해보면 아래와 같다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.apply { println("Created ${this.name}") } // 컴파일 에러
fun create(name: String): Node? = Node(name)
}
하지만 막상 바꿔보니 this.name에서 컴파일 에러가 발생한다.
그 이유는 실제 this가 가르키는 타입은 Node? 이기 때문에 nullable한 처리를 해줘야 한다.
그래서 this.name이 아니라 this?.name 이렇게 사용해야한다.
하지만 this를 이렇게 사용해버리니까 이 this가 apply라는 스코프함수에 적용된 this인지 class내부에서 필드를 가르키는 this인지 또 헷갈린다.
그래서 also, let 을 활용하면 it라는 리시버를 명시적으로 사용하기 때문에 nullable 처리하는데 유용할 수 있다.
apply를 사용해버리면 this가 어디에 this인지, nullable한지 체크할 때 컴파일 에러가 발생할 수 있다.
중요한 점은 리시버가 여러개이고 복잡한 경우에는 명확하게, 읽는사람이 읽기 쉽도록 작성하란 것이다.
아이템 16. 프로퍼티는 동작이 아니라 상태를 나타내야한다.
코틀린에서 프로퍼티라는 개념은 자바의 필드와는 다르다.
프로퍼티 자체가 사용자 정의 getter()와 setter()를 정의하여 함수와 같은 동작을 수행할 수 있기 때문이다.
var name: String? = null
get() = field?.toUpperCase() // 사용자 정의 getter()
set(value) { // 사용자 정의 setter()
if(!value.isNullOrBlank()) {
field = value
}
}
요점원칙적으로 프로퍼티는 상태를 나타내거나 설정할 목적으로만 사용하는것이 좋고 다른 로직은 포함하지 않아야 된다.
아래와 같은 경우에는 프로퍼티가 아니라 함수로 정의하는 것이 좋다.
- 연산 비용이 높거나 복잡도가 O(1)보다 큰 경우
- 사용하는 사람이 getter를 호출했는데 시간복잡도가 클거라곤 예상하지 못할 수 있음
- 비즈니스 로직(애플리케이션 동작)을 포함하는 경우
- 의미있는 동작이므로 함수를 통해 명명하여 나타내는것이 좋음
- 결정적이지 않은 경우
- 같은 동작을 여러번 수행했는데 서로 다른 결과가 나오는 경우는 함수로 만들자
- 변환의 경우
- 변환은 관습적으로 Int.toDouble() 이렇게 함수를 사용하므로 관례에 따라 함수로 만들자
- 게터에서 프로퍼티의 상태 변경이 일어나야 하는 경우
- getter에서 상태 변화 동작을 수행하지 말고, 의미있는 명명을 통해 함수로 만들자
아이템 17. 이름있는 아규먼트를 사용하라
코틀린은 아래와 같이 파라미터에 이름을 명시해줄 수 있다.
createUser(age = 25, name = "Alice", email = "alice@example.com")
파라미터를 명확히 전달해줄 수 있다는 장점이 있는데, 아래와 같은 경우에 더 사용하면 좋다.
1. 클래스의 프로퍼티에 디폴트 아규먼트가 있는경우에는 헷갈리지 않도록 사용하는것이 좋다.
class Rectangle(
val width: Int = 10,
val height: Int = 5,
val color: String = "blue"
) { . . .}
2. 파라미터의 타입이 같은 경우에는 헷갈릴 수 있으니 사용하는 것이 좋다.
// sendEmail("홍길동", "hongildong@naver.com")
// sendEmail("hongildong@naver.com", "홍길동") -> 어떤 값이 먼저인지 헷갈림
sendEmail(to = "홍길동", email = "hongildong@naver.com")
3. 함수 파라미터의 경우 일반적으로 마지막에 위치하는데, 이때도 이름있는 파라미터를 사용하면 의미 전달에 유용하다.
아이템 18. 코딩 컨벤션을 지켜라
코딩 컨벤션을 무조건 지켜야 되는 것은 아니지만, 지키면 아래와 같은 이점들이 있다.
1. 코딩 컨벤션을 지키는 다른 프로젝트를 봐도 쉽게 이해할 수 있다.
2. 코드의 작동 방식을 쉽게 추측할 수 있다.
3. 코드를 병합거나, 한 코드를 다른 프로젝트의 코드로 옮기는 것이 쉽다.
한가지 예를 들면 코틀린에서는 아래와 같은 방식을 추천한다.
class Person(
val id: Int = 0,
val name: String = "",
val surname: String = ""
) : Human(id, name) {
// 본문
}
결국 함께하는 프로젝트에서는 컨벤션을 준수해야 가독성, 협업 등에서 좋다.
>> 아래는 코틀린 공식 문서의 컨베션이다.
https://kotlinlang.org/docs/coding-conventions.html
[스터디 교재]