본문 바로가기

Kotlin

Kotlin Effectvie 번역 - 2. Readability

개인적으로 코틀린을 사용하면서 이게 가독성이 좋은 코드일까? 동료가 보기 좋은 코드일까?에 대한 고민을 많이 하였다. 그러던 중에 Kotlin Effectvie에서 가독성이라는 챕터를 발견하였고 아직 한국에 나오지 않은 책을 번역해보려 한다

 

Item 11. Design for readability

앞부분에는 모두가 아는 코드를 짜는 시간보다 읽는 시간이 절대적으로 많다는 설명과 함께 클린코드의 중요성을 상기시켜주고 있다.

 

A와 B 코드는 어떤게 더 깔끔한 코드일까?

//Implementation A
if( person != null && person.isAdult ) {
  view.showPerson(person)
} else {
  view.showError()
}

//Implementation B
person?.takeIf{it.isAdult} 
   ?.let(view::showPerson) 
   ?: view.showError()
  • 더 짧은 B가 낫다? 딱히 합당한 이유는 아니다
  • 동업 개발자가 코틀린에 익숙한가?
    • 익숙하지 않다면 A가 낫다고 판단 할 수도 있다. B가 더 코틀린스럽지만...
    • 거기다가 다른 언어를 사용하는 개발자가 읽게 된다면??? 당연 A가 낫다....
  • 변경하기 쉬운가? 디버깅하기 쉬운가? - A의 승리
  • 만약 아래 showPerson이 null 을 반환한다면?
    • A는 명백하게 showError가 동작하지 않지만
    • B는 let에서 null을 반환하기 때문에 showError가 동작하게 된다....
// A
if(person!=null&&person.isAdult){ 
	view.showPerson(person) 
    view.hideProgressWithSuccess()
}else{ 
	view.showError() 
    view.hideProgress()
}
// B
person?.takeIf{it.isAdult} ?.let {
  view.showPerson(it)
  view.hideProgressWithSuccess() } 
?: run {
  view.showError()
  view.hideProgress() }

책에서 내린 결론은 결국 우리와 친근한 구조를 선호해야 한다고 하고 있다. ( 그럼 let 을 개발해준 코틀린 사람들은 바보인가? 쓰지 말란건가?? 라고 혼자 생각한 순간 바로 아니라고 밑에 설명이 나온다... ㅎㅎ )

 

Do not get extreme

아래와 같이 명확한 경우는 

class Perosn(val name: String)
var person: Person? = null

fun print() {
  person?.let {
    print(it.name)
  }
}

Item 12. Operator 의미는 함수이름과 함께 간결하게 표현되어야 한다

아래의 예제는 함수이름이 명확하지 않는 케이스이다.

operator fun Int.times(operation: () -> Unit) : () -> Unit = 
	{ repeat(this) { operator() } }
    
val tripledHello = 3 * { print("Hello") }

tripledHello() // Prints: HelloHelloHello

위의 함수는 아래와 같이 좀더 명확한 이름을 가지는게 좋다

operator fun Int.timesRepeated(operation: () -> Unit) : () -> Unit = 
	{ repeat(this) { operator() } }
    
val tripledHello = 3 timesRepeated { print("Hello") }

tripledHello() // Prints: HelloHelloHello

Item 13 : Unit?을 사용해서 return, operator 하지말라

이건 읽지 않아도 Unit? 을 반환하게 되면 함수의 의도가 불명확하게 된다. 그냥 안쓰면 된다...

 

Item 14 : 변수 타입이 명확하지 않다면 구체화하라

아래 코드는 return type이 명시되지 않아 추론하기 쉽지 않다. 구현체로 넘어가야 한다 결국...

val data = getSomeData()

val data : UserData = getSomeData() // better readability

Item 15. Receiver를 명시적으로 참조하라

class Node(val name: String) {
	fun makeChild(childName: String ) =
    	creeate("$name.$childName")
        	.apply { print("Created ${name}") }
            
    fun create(name: String): Node? = Node(name)
}


fun main(){
  val node = Node("parent") 
  node.makeChild("child") // Created parent가 출력됨
}

예상 했던 결과가 나오지 않았다.

class Node(val name: String) {
	fun makeChild(childName: String ) =
    	creeate("$name.$childName")
        	.apply { print("Created ${this?.name}") }
            
    fun create(name: String): Node? = Node(name)
}


fun main(){
  val node = Node("parent") 
  node.makeChild("child") // Created parent.child 출력
}

이렇게 수정하면 의도된 결과가 나오지만 이는 apply를 잘못 사용한 예이다. also는 함수의 receiver를 명시적으로 사용하도록 강제한다. 

 

만약에 outer receiver도 같이 명시해주고 싶은 경우는 어떻게 해야 될까?

class Node(val name: String) {
	fun makeChild(childName: String ) =
    	creeate("$name.$childName")
        	.apply { print("Created ${this?.name} in ${this@Node.name}") }
            
    fun create(name: String): Node? = Node(name)
}


fun main(){
  val node = Node("parent") 
  node.makeChild("child") // Created parent.child in parent 출력
}

Item 16. property 는 행동을 나타내는게 아니라 상태를 나타내야 한다

//Kotlin property
var name:String?=null 

//Java field
String name=null;

우리는 프로퍼티들이 custom getter와 setter를 가질 수 있다는 걸 기억해야한다.

var name: String? = null
	get() = field?.toUpperCase()
    set(value) {
    	if(!value.isNullorBlank()) {
        	field = value
        }
    }
}

// Read-only property
val fullName:String
    get() = "$name $surname"

backing field는 우리가 선언하지 않아도 일단 기본 getter, setter 때문에 생성된다. 

 

우리는 Getter, setter를 정의함으로써 property를 만들 수 있다. 이러한 property 들을 derived property라고 부른다. 예를 들어, date 라는 프로터피의 타입이 바뀐 경우, 프로젝트에서 이 프로터피를 참고 하는 있는 코드에 문제가 생긴다. 이때 아래와 같이 사용할 수 있다.

var date: Date
  get() = Date(millis)
  set(value) {
     millis = value.time
  }

 

property 는 필드를 가지지 않아도 된다. 그렇기 때문에, accessor를 개념적으로 나타낼 수 있다.

interface Person {
	val name: String
}	

open classSupercomputer{
   open val theAnswer: Long = 42
}

class AppleComputer:Supercomputer(){
   override val theAnswer: Long = 1_800_275_2273 7
}
val db:Database by lazy{connectToDb()}

아래와 같이 프로퍼티들은 accessor를 나타내는 것이지, 필드를 나타내는 것이 아니다. 이러한 코드를 짤 때 알고리즘의 특성을 띠는 행위를 해서는 안된다.

val Context.preferences : SharedPreferences
	get() = PreferenceManager.getDefaultSharedPreferences(this)

잘못된 예제이다. 이 예제는 프로퍼티의 성격이 아니라 함수이다.

val Tree<Int>.sum: Int
	get() = when(this) {
    	is Leaf -> value
        is Node -> left.sum + right.sum
    }

그렇기 때문에 아래와 같이 수정하여야 한다.

val Tree<Int>.sum(): Int
	get() = when(this) {
    	is Leaf -> value
        is Node -> left.sum() + right.sum()
    }

일반적인 규칙은 state를 나타내거나 셋할때만 사용되어야 하고, 다른 로직은 포함돼서는 안된다. 그래도 애매해서 어떤걸 선택할지 모르겠다고? 그렇다면 답은 프로퍼티를 안쓰는것이 더 좋다이다.

  • deterministic하지 않다 : member를 2번 호출했는데 다른 결과가 나올 수 있다.
  • 이러한 로직은 conversion이다. ( Int.toDouble() ) : property를 사용하면 안의 일부를 reference 하는 것처럼 보인다. 하지만, conversion을 함수로하는 것은 하나의 관습이다.
  • 비즈니스 로직을 담고 있다. : 우리는 일반적으로 코드를 읽을 때, property에서 어떠한 비즈니스 행위를 하는 것을 기대하지 않는다.
  • operation이 O(1)보다 복잡성을 뛴다면? : 개발자들은 property를 보고 복잡성을 생각하진 않을 것이다. 함수를 본다면 복잡성을 고려해 이걸 캐싱할지 등등의 조치를 취할 것이다.

Item 17. 인자에 이름 붙이는걸 고려하라

아래와 같은 코드를 보면 | 가 의미하는게 명확하지 않다.

val text=(1..10).joinToString("|")

그래서 아래와 같이 수정할 수 있다.

val text=(1..10).joinToString(separator="|")

val separator="|"
val text=(1..10).joinToString(separator)
  • 장점 1 : 넘기는 파라미터가 명확해짐
  • 장점 2 : 개발자가 실수로 다른 인자를 넘기지 않게 됨