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)를 설계할 때는 오버로딩 규칙을 무시할 수 있습니다. 

// 1. 연산자 오버로딩 
operator fun Int.times(operator: () -> Unit): Unit = {repeat(this) {operator()}}

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

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

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

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

13. Unit?을 리턴하지 않기 

Unit?를 반환하는 것은 오해의 소지가 있으며, 예측하기 어려운 오류를 발생시킬 수 있습니다. 

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

변수의 타입을 명확하게 지정할 때 코드의 가독성이 향상됩니다. 
단, 무조건 타입을 명시해야하는 것은 아니며, 상황에 맞게 사용하는 것이 중요합니다. 

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

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

  • 스코프 내부에 두 개 이상의 리시버가 있는 경우, 리시버를 명시적으로 나타내는게 좋습니다. (ex: apply, with, run을 사용할 때)
  • 외부에 있는 리시버를 사용하려면 레이블(@로 표현)을 사용해야 합니다.
  • 코틀린 DSL을 사용할 때는 여러 리시버가 중첩되어도 리시버를 명시적으로 표시할 필요가 없습니다. (원래 그렇게 설계됨)
    DSL에서 외부 스코프의 리시버를 명시적으로 적으려면 @DslMaker 어노테이션을 사용합니다. 

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

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

// 자바 필드
String name = null;
  • 프로퍼티는 상태를 나타내거나 결정을 위한 목적으로 사용하는 바람직하며, 계산식을 포함하지 않습니다. 
  • 프로퍼티는 필드와 달리 사용자 정의 Setter/Getter를 갖습니다.
    프로퍼티의 field 식별자는 프로퍼티의 데이터를 저장하는 백킹 필드(baccking field)에 대한 레퍼런스이며, Setter와 Getter의 디폴트 구현에 사용됩니다.
  • val 로 읽기 전용 프로퍼티를 만들 때는 field가 생성되지 않습니다. 
    var 로 만든 프로퍼티는 Setter/Getter 정의가 가능하며, '파생 프로퍼티'라고 불립니다. 
// 아래의 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