Clean Code With Kotlin 1

클린코드란 무엇인가?

클린 코드 저자 엉클 밥은 "클린코드는 가독성이다. 하나의 스토리를 말해준다" 라고 했으며 애자일 개발자의 원칙 저자는 "너의 의도를 reader에게 명확하게 표현하는 것이다. 가독성이 없는 코드는 영리하지 않다"라고 말했다.

그럼 우리는 어떻게 클린 코드를 측정할 수 있을까?

비공식적으로 분 당 얼마나 WTF을 외치냐로 측정한다고 말한다. wtf/min이 적으면 행복한 개발자 팀이 있고, wtf/min이 많으면 그다지 만족스럽지 않은 개발자 팀이 있다. 여기서 중요한 점은 WTF은 "What a terrible feature"의 줄임말이다 ^^ 오해하지 말기

의미있는 네임들 및 표현

하지만 어떻게 클린 코드를 적을까? 여기 적용 시킬 몇 개의 규칙이 있다.

우리가 패키지, 클래스, 함수, 변수들을 만들 때 코드를 작성한다. 그리고 그들은 이름들을 갖고 있고 우리는 이 컴포넌트의 의도를 표현하는 이름을 골라야 한다. 그게 단지 변수 혹은 클래스 이름일지라도.
우리가 가독성 있는 코드를 짜려고 찾아보면 3개의 질문을 하라고 한다.

  • Why it exists?
  • What does it do?
  • How it is used?

그리고 2개의 타입, 클린코드와 언클린 코드로 나누어서 살펴보자.

examples

1) split a file path and get the latest file and its directory

"unclean"

data class GetFile(val d: String, val n: String)

val pattern = Regex("(.+)/([^/]*)")

fun files(ph: String): PathParts {
	val match = pattern.matchEntire(ph) ?: return PathParts("". ph)
    return PathParts(match.groupValues[1], match.groupValues[2])
}

"clean"

data class PathParts(val directory: String, val fileName: String)

val pattern = Regex("(.+)/([^/]*)")

fun splitPath(path: String) = 
	PathParts(
		path.substringBeforeLast("/", ""),
        path.substringAfterLast("/"))

2) "if-null" checks를 피하고 evils(?:)와 throw를 같이 사용할 때

"unclean"

class Book(val title: String?, val publishYear: Int?)

fun displayBookDetails(book: Book) {
    val title = book.title
    if (title == null)
        throw IllegalArgumentException("Title required")
    val publishYear = book.publishYear
    if (publishYear == null) return

    println("$title: $publishYear")
}

"clean"

class Book(val title: String?, val publishYear: Int?)

fun displayBookDetails(book: Book) {
    val title = book.title ?: 
throw IllegalArgumentException("Title required")
    val publishYear = book.publishYear ?: return

    println("$title: $publishYear")
}

3) Warning: 어규먼트 네이밍을 직접 사용하고 it을 자주 쓰는 걸 피해라

"unclean"

users.filter{ it.job == Job.Developer }
    .map{ it.birthDate.dayOfMonth }
    .filter{ it <= 10 }
    .min()

"clean"

users.filter{ user -> user.job == Job.Developer }
    .map{ developer -> developer.birthDate.dayOfMonth }
    .filter { birthDay -> birthDay <= 10 }
    .min()

더군다나 우린 아래와 같은 이유들로 코틀린을 사용할수록 불변성을 더 선호한다고 말할 수 있다.

  • var보다는 val 사용하는 걸 선호
  • mutable properties보다 read-only properites를 선호
  • mutable collections보다 read-only collections를 선호
  • copy()를 제공하는 데이터 클래스 사용을 선호

함수

코틀린에서 함수를 쓸 때 규칙은 무엇일까?

  • 첫 번째 규칙: 함수는 작아야 한다.
  • 두 번째 규칙: 함수는 그것보다 작아야한다.

왜 긴 함수들은 괜찮지 않다고 하는 걸까?
왜냐면 긴 함수들은

  • 테스트하기 어려움
  • 읽기 엉려움
  • 재사용하기 어려움
  • 중복성을 야기함
  • 바꿔야할 많은 이유를 가짐 (왜냐면 코드가 많으니깐)
  • 디버그하기 어려움

하기 때문이다.

추가적으로 코틀린으로 함수 작성할 때 추가되는 규칙들이 있다 :

  • 많은 어규먼트들은 객체로 그룹화하거나 리스트를 사용한다(동일 유형일 경우)
  • 함수들은 사이드 이펙을 가지면 안된다 (사이드 이펙트들은 구라다)
  • 함수의 들여쓰기 수준은 최대 2 depth까지다
  • if/else/while 문에 한 줄 블록이 포함되어 있는 경우
  • 긴 설명 네임이 짧은 수수께기 같은 네임보다 낫다.
  • 코드를 쉽게 읽을 수 있게 바꾸고 재구축해야 한다.
  • 명령어 쿼리 분리 원칙을 적용한다.*

*통칭 Comman-Query Separation (CQS)으로 명령형 컴퓨터 프로그래밍의 원리이다. 모든 메서드는 액션을 실행하는 명령어 또는 발신자에게 데이터를 반환하는 쿼리 중 하나여야 하지만 둘 다 해서는 안 된다.

쿼리: 결과를 반환하고 시스템의 관찰 가능한 상태를 변경하지 않는다(사이드 이펙에서 자유롭다).
명령어: 시스템 상태를 변경하지만 값을 반환하지 않는다.

example

1) when절 사용할 때

"unclean"

fun parseProduct(response: Response?): Product? {
    if (response == null) {
        throw ClientException("Response is null")
    }
    val code: Int = response.code()
    if (code == 200 || code == 201) {
        return mapToDTO(response.body())
    }
    if (code >= 400 && code <= 499) {
        throw ClientException("Invalid request")
    }
    if (code >= 500 && code <= 599) {
        throw ClientException("Server error")
    }
    throw ClientException("Error $code")
}

"clean"

fun parseProduct(response: Response?) = when (response?.code()){
    null -> throw ClientException("Response is null")
    200, 201 -> mapToDTO(response.body())
    in 400..499 -> throw ClientException("Invalid request")
    in 500..599 -> throw ClientException("Server error")
    else -> throw ClientException("Error ${response.code()}")
}

2) 추상 레벨 구성을 유지하고 중첩 코드를 피해라

"unclean"

for (user in users) {
    if(user.subscriptions != null) {
        if (user.subscriptions.size > 0) {
            var isYoungerThan30 = user.isYoungerThan30()
            if (isYoungerThan30) {
                countUsers++
            }
        }
    }
}

"clean"

var countUsersYoungerThan30WithSubscriptions = 0
for (user in users) {
    if (user.isYoungerThan30WithSubscriptions) {
        countUsersYoungerThan30WithSubscriptions++;
    }
}

클래스

클래스는 small(소규모)보다 더 작아야 한다. 오직 하나의 책임과 변경될 단 하나의 이유를 가져야 한다.(SRP) 클래스는 몇 개의 다른 클래스와 협력해서 원하는 시스템 동작을 수행한다.

클래스 파일은 신문 기사와 같다 :

  • 이름은 단순하고 알기 쉬워야 한다.
  • parts from the top은 하이레벨(고수준) 개념과 알고리즘을 포함하고 있다. (기사로치면 헤더라인)
  • 아래로 이동하면 세부 정보가 표시된다.
  • 마지막으로 가장 낮은 레벨의 기능을 찾는다.

응집(Cohesion)

응집력은 클래스 혹은 모듈의 요소가 기능적으로 관련되어 있는 정도를 나타내는 척도이다.
클래스들이 낮은 응집력을 갖고 있을 때 분리시켜라

결합(Coupling)

결합은 모듈 간의 상호의존성 정도를 나타내는 척도이다.
isolation은 각 시스템의 요소를 보다 쉽게 이해할 수 있게 만든다.

베스트는 높은 응집도와 낮은 결합도를 갖는 것이다.

이 베스트 시나리오를 완성하려면 아래 원칙을 유의해서 클래스를 작성하면 된다.

  • DRY
Don't Repeat Yourself
코드를 복사/붙일 때마다 적용 가능
개발 비용을 절감
  • KISS
Keep It Simple and Stupid
모든 것을 실행하는 방법을 구현하고 싶을 때, 간단한 것이 실패가 적다.
단순은 익숙한 것과 다르다: for 루프는 익숙하지만 단순할 필요는 없다.
  • YAGNI
You Ain’t Gonna Need It
아직 필요하지 않은 코드를 작성하지 말자.
구현 비용을 평가하자: 지금 당장 실행하는 것은 비용이 많이 들기 때문에 정말 필요할 때 코드 작성을 하자.
  • SOLID
SRP - 클래스 또는 모듈에 변경 이유가 1개만 있는 것
OCP - 클래스, 모듈, 기능 등의 소프트웨어 엔티티는 확장에 대해 개방되어 있지만 변경에는 닫혀 있는 것
LSP - 프로그램 내의 오브젝트는 해당 프로그램의 동작을 변경하지 않고 서브타입의 인스턴스로 대체할 수 있는 것
ISP - 인터페이스를 구현하는 클래스는 사용하지 않는 메서드를 강제로 구현해서는 안 된다
DIP - 상위 레벨의 모듈은 하위 레벨의 모듈에 의존하지 말고 둘 다 추상화에 의존해야 하는 것. 추상화는 디테일에 의존해서는 안 된다. 상세한 것은 추상화에 의존해야 한다.

Reference

https://magdamiu.com/2021/08/23/clean-code-with-kotlin-2/

좋은 웹페이지 즐겨찾기