클린 코드 - 3장 함수



Intro



초서-독서 한 내용을 그대로 적는 곳이기 때문에 책을 읽지 않은 분들이 보기에 맥락이 애매할 수 있습니다.

초서 : 책을 읽는데 그치는 것이 아닌 손을 이용해 책의 중요한 내용을 옮겨 적음으로써 능동적으로 책의 내용을 수용하고 판단하여 새로운 지식을 재창조하는 과정. 메타인지 학습법




Index





3장 함수


” 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까 ? “


작게 만들어라!

” 함수를 만드는 첫째 규칙은 ‘작게’, 두 번째 규칙은 ‘더 작게!’ “

  • 각 함수가 이야기 하나를 표현한다. (쓰임이 명백해야 한다)
  • if 문 / else 문 / while 문 등에 들어가는 블록은 한 줄이어야 한다.
    • 대게 여기서 함수를 호출한다. (바깥을 감싸는 함수가 작아져 가독성이 높아진다)
    • 즉 중첩 구조가 생길만큼 함수가 커져서는 안 된다.


한 가지만 해라!

” 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다. “

  • 지정된 함수 이름 아래에서 ‘추상화 수준이 하나인 단계만 수행’ 한다면 그 함수는 한 가지 작업만 한다.
  • 단일 책임 원칙 (SRP) 과 같은 말.


함수 당 추상화 수준은 하나로!

  • 함수 내 모든 문장의 ‘추상화 수준이 동일’ 해야 함.
  • 한 함수 내에 여러 추상화 수준이 섞여있으면 읽는 사람이 헷갈림.
    • 추상화 수준이 높은 코드 getHtml()
    • 추상화 수준이 낮은 코드 .append("\n")
  • 위에서 아래로 ‘이야기’ 처럼 읽혀야 좋다.
    • 위에서 아래로 한 번 내려갈 수록, 추상화 수준도 한 단계씩 낮아져야 함.


Switch 문

  • 다형성을 이용하여 저차원 클래스에 숨기고 반복을 피해야 한다.

SRP 를 위반한 Switch 사용 함수 예시

public Money calculatePay(Employee e) throw InvalidEmployeeType {
    switch (e.type) {
        case COMMISSIOND:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}

문제점

  • 함수가 길다. (새 직원 유형을 추가하면 더 길어짐)
  • 한 가지 작업만 수행하지 않는다.
  • SRP를 위반한다. (코드를 변경할 이유가 여럿이기 때문)
  • OCP를 위반한다. (새 직원을 추가할 때마다 코드를 변경해야 하기 때문)
  • 제일 심각한 문제는 위 함수와 구조가 동일한 함수가 무한정 생성될 수 있다.
    • isPayDay(Employee e, Date date)
    • deliverDay(Employee e, Money pay)


추상 팩토리 사용하여 해결

public abstract class Employee {
    public abstract boolean isPayDay();
    public abstract Money calculatePay();
    public abstract void deliverDay(Money pay);
}

---

public interface EmployeeFactory {
    Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

---

public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIOND:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}
  • 팩토리는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성.
  • calculatePay, isPayDay, deliverDay 와 같은 함수는 Employee 인스턴스를 거쳐 호출.
  • 다형성으로 인해 실제 파생 클래스의 함수가 실행됨.


서술적인 이름을 사용하라!

  • 함수가 하는 일을 잘 표현해야 한다.
  • private 함수 또한 서술적인 이름을 지어야 한다.

길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

  • 이름을 붙일 때는 일관성이 있어야 한다.
  • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
    • includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage 등이 좋은 예다.
    • “includeTearDownPages, includeSuiteTeardownPage 도 있나요?” 당연하다. ‘짐작하는 대로’다.


함수 인수

” 함수에서 이상적인 인수 개수는 0개(무항)다. “

  • 인수(매개변수)는 개념을 이해하기 어렵게 만든다.
  • 함수 이름과 인수 사이에 추상화 수준이 같아야 한다.
  • 인수가 3개를 넘어가면 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 상당이 부담스러워 짐.


” 출력 인수는 입력 인수보다 이해하기 어렵다. “

  • 출력 인수는 매개변수로 받은 출력기능을 하는 객체이다.
    • appendFooter(StringBuffer report)StringBuffer 와 같은.
  • 인수로 입력을 넘기고 반환값으로 출력을 받는다는 개념이 일반적이다.
  • 대개 함수에서 인수로 결과를 받으리라 기대하지 않는다. (코드를 재차 확인하게 만든다)
  • 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다.
    • 출력 인수로 사용하라고 설꼐한 변수가 바로 this 이기 때문
    • public void appendFooter(StringBuffer report) 보다는 report.appendFooter() 더 낫다.
    • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.


많이 쓰는 단항 형식

  • 인수에 질문을 던지는 형식
    • boolean fileExists("myFileName")
  • 인수를 뭔가로 변환해 결과로 반환하는 형식
    • InputStream fileOpen("myFileName")
  • 이벤트 형식 (입력 인수로 시스템 상태를 변경)
    • passwordAttemptFailedNtimes(int attempts)

주의 해야할 단항 형식

  • 위에서 설명한 경우가 아니라면 단항 함수는 가급적 피한다.
  • 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
    • void includeSetupPageInto(StringBuffer pageText)
  • 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.
    • StringBuffer transform(StringBuffer in)
  • 플래그 인수는 추하다.
    • 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈.
  • 인수가 2~3개 필요하다면 독자적인 클래스 변수로 선언할 가능성을 짚어본다.

동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
    • writeField(name)
  • 함수 이름에 키워드를 추가하는 형식 (인수 순서에 따라)
    • assertEquals 보다는 assertExpectedEqualsActual(expected, actual) 이 더 좋음.


부수 효과를 일으키지 마라!

” 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 하지 마라. “

  • 부수 효과는 시간적인 결합이나 순서 종속성을 초래한다.
    • 시간과 결합한다는 의미는 동시성을 띄지 못함을 의미(시간에 종속되어).
    • cf) 실용주의 프로그래머


명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만.
  • 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 (둘 중 하나!)
    • public boolean set(String attribute, String value) 이러한 함수는
      if (set("username", "unclebob")) 과 같은 괴상한 코드가 나온다.
      • 코드만 봐서는 의미가 모호함. (set 단어가 명령인지 조회인지…)
      • username 을 unclebob으로 설정(set)하는 것인지, 설정되어 있다면 인지…
    • 해결책은 명령과 조회를 분리해 혼란을 애초에 뿌리뽑는 방법이다.
      • if(attributeExists("username"))setAttribute("username", "unclebob") 으로 나눠야함.


오류 코드보다 예외를 사용하라!

  • 명령 함수에서 오류 코드를 if 표현식으로 중첩하여 반환하지 마라.
    • 호출자는 오류 코드를 곧바로 처리해야 함.
  • 오류 코드 대신 예외를 사용하라.
  • try / catch 블록은 코드 구조에 혼란을 준다. (별도로 뽑아내는 것이 좋음)
  • 오류 처리도 ‘한 가지’ 작업에 속한다. (오류를 처리하는 함수는 오류만 처리해야 마땅하다)

Error.java 의존성 자석
오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수(enum)든, 어디선가 오류 코드를 정의한다는 뜻

public enum Error {
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    ...
}

위와 같은 코드는 ‘의존성 자석’ 이다.
다른 클래스에서 Error enum을 import해 사용해야 하므로, 이 에러코드를 사용하는 클래스 전부를 다시 컴파일 하고 다시 배치해야 한다.


반복하지 마라!

  • 중복을 없애면 모듈 가독성이 크게 높아진다.


구조적 프로그래밍

” 데이크스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구 하나만 존재해야 한다고 말했다. “

함수를 어떻게 짤까?

” 글짓기와 비슷하다. (생각을 기록한 후 읽기 좋게 다듬는다)
처음에는 길고 복잡하다.
그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
그런다음 코드를 가다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
메서드를 줄이고 순서를 바꾼다. 때로는 클래스를 쪼개기도 한다.
이 와중에도 코드는 항상 단위 테스트를 통과한다.