관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Clean Code(클린 코드) - 12. 냄새와 휴리스틱 (2) (일반) 본문

Clean Code (클린 코드)

[iOS - swift] Clean Code(클린 코드) - 12. 냄새와 휴리스틱 (2) (일반)

jake-kim 2021. 11. 28. 23:35

* 냄새와 휴리스틱: 파틴 파울러의 "Refactoring"에서 "코드 냄새"를 표현하여 주의해야하는 코드 사항들에 대해 나열한 것에 더하여 저자 로버트 C.마틴의 생각을 더한 것

일반


  • 이름과 기능이 일치하는 함수
    • 이름만으로 분명하지 않은 함수를 사용하면 매번 문서를 뒤적여야 하므로 처음부터 함수 이름과 기능을 확인할 것

WRONG

- 5초인지, 5주인지, 5일인지 알 수 없는 상태이므로 더욱 애매한 함수

let newDate = date.add(5)

RIGHT

- 함수의 이름이 명확한 경우

let newDate1 = date.addDaysTo(5)

  • 역할 분담에 대한 의존성을 제대로 할 것

ex) 임직원들의 report를 만드는데, page가 지정된 크기 pageSize만큼 커지면 페이지를 초기화 시키는 함수

WRONG

- pageSize는 HourlyReporter가 알필요 없이, page에 관한 처리를 하는 HourlyReportFormatter가 알아야 자연스럽기 때문에, 역할분담에 대한 의존성이 잘 정의되지 않은 코드

- 만약 HourlyReportFormatter 클래스가 페이지 크기 55를 제대로 처리하지 못하면 오류가 생기는 코드

class HourlyReporter {
    private let formatter: HourlyReportFormatter
    private let pageSize = 55
    
    func generateReport(employees: [HourlyEmployee]) {
        employees.forEach {
            addLineItemToPage($0)
            if page.count == pageSize {
                printAndClearItemList()
            }
        }
        if page.count > 0 {
            printAndClearItemList()
        }
    }
    
    private func printAndClearItemList() {
        formatter.format(page)
        page.clear()
    }
}

RIGHT

- HourlyReportFormatter의 메소드에 getMaxPageSize() 함수를 추가하여 역할 분담을 제대로 수행하는 방법

class HourlyReporter {
    private let formatter: HourlyReportFormatter
    private var pageSize { return formatter.getMaxPageSize() }
    ...
}

  • 매직 넘버는 가독성을 위하여 명명된 상수로 교체할것

ex) 텍스트필드의 입력값이 10보다 큰 경우는 blue화면으로 변하고, 작아지면 시스템 background 색상으로 변경하는 코드

WRONG

- 해당 코드를 봤을 때 10이라는 숫자를 보고 예측해야하는 상황이므로 안좋은 코드

func setBackgroundColor() {
    if myTextField.text.count > 10 {
        view.backgroundColor = .blue
    } else {
        view.backgroundColor = .systemBackground
    }
}

RIGHT

- 매직 넘버를 명명된 상수로 사용함으로서 더욱 가독성 높은 코드

let inputThresholdCount = 10

func setBackgroundColor() {
    if myTextField.text.count > inputThresholdCount {
        view.backgroundColor = .blue
    } else {
        view.backgroundColor = .systemBackground
    }
}

  • 분기문에 들어가는 조건식은 나열하지 말고 캡슐화 할 것

WRONG

- 조건식이 여러개이므로 읽을때 가독성이 떨어지는 코드

if (timer.hasExpired() && !timer.isRecurrent()) { ... }

RIGHT

- 조건식이 캡슐화 되어 있어서 의도가 분명히 밝혀져, 가독성이 좋은 코드

if shouldBeDeleted(timer) { ... }
  • '!'와 같은 부정 조건은 한번에 이해하기 힘드기 때문에 가능하면 긍정 조건을 사용

WRONG

- 부정조건인 경우 한번에 이해하기 힘든 형태

if !buffer.shouldNotCompact() { ... }

RIGHT

- 긍정조건은 한번에 이해하기 쉬운 형태

if buffer.shouldCompact() { ... }

  • 함수는 한 가지만 수행할 것 (SRP)
    • 한 가지만 수행하는 좀 더 작은 함수 여럿으로 나눌 것

WRONG

- 하나의 함수에서 세 가지 임무를 수행하는 형태

func pay() {
    employees.forEach {
        if $0.isPayday() {
            let pay = $0.calculatePay()
            $0.deliverPay(pay)
        }
    }
}

RIGHT

- 여러가지 일을 하던 하나의 함수를 한가지의 일만 하는 함수 여러가지로 나눈 형태

func pay() {
    employees.forEach {
        payIfNecessary($0)
    }
}

private func payIfNecessary(_ employee: Employee) {
    if employee.isPayday() {
        calculateAndDeliverPay(employee)
    }
}

private func calculateAndDeliverPay(_ employe: Employee) {
    let pay = employee.calculatePay()
    employee.deliverPay(pay)
}

  • 중복되는 코드들은 변수로 만들어서 사용할 것

WRONG

- 코드 여기저기에 +1에 관한 코드가 중복으로 사용되는 코드

if level + 1 < tags.length {
    let parts = Parse(body, tags, level + 1, offset + endTag)
    body = nil
}

RIGHT

- 여기저기서 사용되던 코드를 변수로 만들어서 깔끔하게 표현한 코드

let nextLevel = level + 1
if nextLevel < tags.length {
    let parts = Parse(body, tags, nextLevel, offset + endTag)
    body = nil
}

  • 함수는 추상화 수준이 동일하거나 한 단계만 내려가도록 할 것
    • 하나의 함수에서 추상화 개념 차이가 많이 벌어질 수록 처음보는 사람이 이해하기 어려운 코드가 되는점을 주의

ex) 페이지를 가로질러 수평자를 만드는 HTML 태그 생성하는 코드

WRONG

- 추상화 수준이 두 개가 섞여있는 코드

  - 수평선에 크기의 개념이 나타난 것 (html을 모르는 사람이 보면은 이해하지 못하는 코드)

  - HR 테그 자체의 문법 (html을 모르는 사람이 보면은 이해하지 못하는 코드)

func render() {
    let html = "<hr"
    if extraDashes > 0 {
        hr.addAttribute("size", hrSize(extraDa))
    }
}

RIGHT

- 추상화 수준을 제거한 코드

  - 수평선에 크기의 개념이 나타난 것 -> size는 변수 이름은 목적을 반영하게 변경해으며, 변수 목적은 추가된 대시 개수를 저장, 대시(-) 개수가 연달아 4개가 나오는 경우 대시를 감지해 HR 태그로 변환하는 로직

  - HR 태그 자체의 문법 -> HtmlTag라는 모델을 만들어서 HTML 문법은 HtmlTag 모듈이 알아서 처리하는 형태

func render() -> String {
    let hr = HtmlTag("hr")
    if extraDashes > 0 {
        hr.addAttribute("size", hrSize(extraDashes))
    }
    return hr.html()
}

private func hrSize(_ hright: Int) -> String {
    let hrSize = height + 1
    return String(hrSize)
}

  • 추이적 탐색을 피할 것
    • A가 B를 사용하고 B가 C를 사용한다 하더라도 A가 C를 알아야 할 필요는 없다는 의미
    • ex) a.getB().getC()라는 형태를 사용한다면 설계와 아키텍처를 바꿔 B와 C사이에 Q를 넣기가 쉽지 않음
      -> 만약 사이에 넣을려고 하는 경우, a.getB().getC()를 모두 찾아 a.getB().getQ().getC()로 바꾸어야 함
    • 내가 사용하는 모듈이 내게 필요한 서비스를 대상으로 모두 제공하도록 설계하여, 객체 그래프를 따라 시스템을 탐색할 필요가 없도록 할 것

* 참고: Clean Code (로버트 C. 마틴)

Comments