Hyeyeon blog

이펙티브 코틀린 - 2장 가독성 본문

개발/Android

이펙티브 코틀린 - 2장 가독성

Hyeyeon.P 2024. 3. 10. 01:58
반응형

2장 가독성

   11. 가독성을 목표로 설계하기
   12. 연산자 오버로드를 이름의 의미에 맞게 사용하기
   13. Unit?을 리턴하지 않기
   14. 변수 타입을 명확하게 지정하기
   15. 리시버를 명시적으로 참조하기
   16. 프로퍼티는 동작이 아닌 상태를 나타해야함
   17. 이름있는 아큐먼트를 사용하기
   18. 코딩 컨벤션 지키기


코틀린은 간결성이 아닌 가독성을 개선하는 데 목표를 두고 설계된 언어임.

간결성은 가독성을 목표로 두고 반복되는 코드를 짧게 쓸 수 있기 때문에 발생한 부가적인 효과임.

11. 가독성을 목표로 설계하기

인지 부하 감소, 즉 코드를 읽고 이해하는 속도를 줄이도록 작성해야함. 
인지 부하가 있더라도, 가치가 있는 경우라면 사용해도 괜찮음. 
이를 위해 어떤 구조들이 어떤 복잡성을 가져오는지 파악이 필요함. 
두 구조를 조합해서 사용하면 개별적인 복합성의 합보다 훨씬 커지기에 주의가 필요함. 

12. 연산자 오버로드를 이름의 의미에 맞게 사용하기

연산자 의미가 명확하지 않다면 일반 함수를 사용해야함.
꼭 연산자 형태로 사용하고싶다면 infix 확장함수 또는 최상위 레벨 함수를 활용.
* 최상위 레벨 함수: 객체, 클래스에 작성되지 않고 최상위 파일에 작성한 함수로 파일 어디서든 접근이 가능함.  
도메인 특화 언어(Domain Specific, Language, DSL)를 설계할땐 오버로딩 규칙을 무시해도됨.

operator fun Int.times(operator: () -> Unit): Unit = {repeat(this) {operator()}}

// case1
val tripleHello = 3 * {print("Helo")}
tripleHello()

// case2
3 * {print("Hello")}

// 함수를 세번 반복하는 새로운 함수를 만드는지, 함수를 세번 호출하는 지 혼란이 있어서 아래와 같이 변경.
// infix 활용하여 아래와 같이 사용
infix fun Int.timesRepeated(operator: () -> Unit): Unit = {repeat(this) {operator()}}
val tripleHello = 3.timesRepeated {print("Helo")}
tripleHello()

// 최상위 레벨 함수를 사용 > 아래의 내용으로 stdlib에 구현함 
repeat(3) {print("Hello")}

13. Unit?을 리턴하지 않기 

Unit?은 오해의 소지가 있으며 예측하기 어려운 오류를 만들 수 있음.

14. 변수 타입을 명확하게 지정하기

타입이 명확할 때 코드의 가독성이 향상됨. (무조건X, 상황에 맞게 사용)

val data = getSomeData() // 타입을 알 수 없음
val data : UserData = getSomeData() // 타입 명시로 가독성 향상

15. 리시버를 명시적으로 참조하기 

스코프 내부에 둘 이상의 리시버가 있는 경우, 리시버를 명시적으로 나타내는게 좋음. (apply, with, run 사용 시)
외부에 있는 리시버를 사용하려면 레이블(@로 표현)을 사용해야함. 

코틀린 DSL을 사용할 떄는 여러 리시버가 중첩되어도 리시버를 명시적으로 표시하지 않음. (원래 그렇게 설계됨)
DSL에서 외부 스코프의 리시버를 명시적으로 적으려면 DslMaker 어노테이션을 사용함. 

16. 프로퍼티는 동작이 아닌 상태를 나타해야함

// 코틀린 프로퍼티
var name: String? = null

// 자바 필드
String name = null;

프로퍼티는 필드와 달리 사용자 정의 세터/게터를 가짐
프로퍼티의 field 식별자는 프로퍼티의 데이터를 저장해두는 백킹 필드(baccking field)에 대한 레퍼런스이며, 세터와 게터의 디폴트 구현에 사용됨. 
val 로 읽기 전용 프로퍼티를 만들때는 field가 생성되지 않음. 
var 로 만든 프로퍼티는 게터/세터 정의가 가능하며, '파생 프로퍼티'라고 부름.

// 아래의 sum 프로퍼티는 모든 요소를 반복처리 하므로 많은 계산량이 필요함. 
var Tree<Int>.sum: Int
	get() = when(this){
    	is Leaf -> value
        is None -> left.sum + right.sum
    }
    
// 게터에 많은 계산량이 필요하다면 아래와 같이 함수로 구현해야함. 
fun Tree<Int>.sum(): Int = when(this){
	is Leaf -> value
	is None -> left.sum + right.sum
}

아래의 경우엔 프로퍼티 대신 함수를 사용하는 것이 좋음.
   - 연산 비용이 높거나, 복잡도가 O(1)보다 큰 경우: 함수를 사용해야 연산비용 예측이 쉽고 이를 기반으로 캐싱 등을 고려 가능
   - 비즈니스 로직을 포함하는 경우
   - 결정적이지 않은 경우: 같은 동작을 연속적으로 수행했는데 다른 값이 나올 수 있다는 함수를 사용.
   - 변환의 경우: Int.toDouble()과 같은 변환 함수를 프로퍼티로 만들면 오해를 불러일으킬 수 있음.
   - 게터에서 프로퍼티의 상태 변경이 일어나야 하는 경우 
일반적으로 프로퍼티는 상태 집합을 나타내고, 함수는 행동을 나타낸다고 생각함. (아래 예시)

// 잘못된 예시 
class UserIncorrect {
	private var name: String = ""
    fun getName() = name
    fun setName(name: String) { this.name = name }
}

// 좋은 예시 
class UserCorrect {
	var name: String = ""
}

17. 이름있는 아큐먼트를 사용하기

val text = (1..10).joinToString("|")
val text = (1..10).joinToString(separator = "|") // 아규먼트 명시

val separator = "|"
val text = (1..10).joinToString(separator = separator) // 변수를 사용할 때도 아규먼트 명시

이름있는 아규먼트를 사용하면 코드가 길어지나, 값이 나타내는 의미를 알 수 있고 파라미터 입력순서가 무관하여 안전함.
디폴트 아규먼트의 경우 / 같은 타입의 파라미터가 많은 경우 / 함수 타입 파라미터인 경우 에 이름있는 아규먼트를 사용해야함.
함수 타입 파라미터는 마지막 위치에 배치하는 것이 명확해져서 좋음. 

// 예시1. 
val view = linearLayout {
    text("Click")
    button({ /* 1 */ }, { /* 2 */ }) 

// 아래와 같이 명시 
val view = linearLayout {
    text("Click")
    button(onClick = { /* 1 */ }) { /* 2 */ }
    
// 예시2. 
// Java는 주석으로 표시함 
observable.getUsers()
	.subscribe((..) -> { // onNext 
    	// ...
    },

// 코틀린은 아규먼트를 활용함
observable.getUsers()
	.subscribeBy( onNext = { .. },

18. 코딩 컨벤션 지키기

https://kotlinlang.org/docs/coding-conventions.html

 

728x90
Comments