Clean Code 3장

3장 함수

작게 만들어라!

  • 함수를 만드는 첫번째 규칙은 ‘작게’, 두번째 규칙은 ‘더 작게’다.
  • 각 함수는 명백하고 하나의 이야기를 표현한다.

블록과 들여쓰기

  • if/else/while문 등에 들어가는 블록은 한 줄이어야 한다.
    • 블록 안에서 함수를 호출한다.
      이때, 호출하는 함수 이름을 적절하게 짓는다면, 이해하기 쉬워진다.
  • 중첩 구조가 생길만큼 함수가 커져서는 안 된다.

한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다.
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다.
    • 함수는 큰 개념을 추상화 수준에서 여러 단계로 나눠 수행하기 위함이다.
  • 만약 의미 있는 이름으로 다른 함수를 추출할 수 있다면, 해당 함수는 여러 작업을 하는 것이다.

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

  • 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

위에서 아래로 코드 읽기: 내려가기 규칙

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
  • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
    즉, 위에서 아래로 읽으면 함수 추상화 수준이 한 단계씩 낮아진다.

Switch문

  • switch문은 작게 만들기 어렵다.
  • 본질적으로 switch문은 N가지를 처리한다.
switch(e.type) {
    case MARKETING:
        return calculateMarketingPay(e);
    case HR:
        return calculateHRPay(e);
    case PROGRAMMER:
        return calculateProgrammerPay(e);
    deault:
        throw new InvalidEmployeeType(e.type);
}

위와 같은 switch문에는 문제점이 있다.

  1. 함수가 길다.
  2. 한 가지 작업만 수행하지 않는다.
  3. SRP(Single Responsibility Principle)을 위반한다.
    • SRP란?
      • 한 클래스는 하나의 책임만 가져야 한다.
      • 어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다.
    • 해당 코드는 코드를 변경할 이유가 여럿이다.
  4. OCP(Open Closed Principle)를 위반한다.
    • OCP란?
      • 확장에는 열려있지만 변경에는 닫혀 있어야 한다.
      • 기존 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계해야 한다.
    • 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다.
public abstract class Employee {
    public abstract Money calculatePay();
}

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

public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch(r.type) {
            case MARKETING:
                return new MarketingEmployee(r);
            case HR:
                return new HREmployee(r);
            case PROGRAMMER:
                return new ProgrammerEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

switch문을 추상 팩토리에 숨기면서 문제를 해결할 수 있다.
팩토리는 switch문을 통해 적절한 파생 클래스의 인스턴스를 생성한다. calculatePay와 같은 함수는 Employee 인터페이스를 거쳐 호출된다.
=> 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

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

  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.
  • 길고 서술적인 이름이 짧고 어려움 이름이나 길고 서술적인 주석보다 좋다.
  • 이름을 붙일 때는 일관성이 있어야 한다.

함수 인수

  • 이상적인 인수 개수는 0개다.
  • 가능한 3개는 피하는 편이 좋고 4개 이상은 특별한 이유가 필요하지만, 이유가 있어도 사용하면 안 된다.
  • 코드를 읽는 사람이 현 시점에서 별로 중요하지 않은 세부사항을 알아야 하는 경우가 생긴다.
  • 테스트 관점에서 유효한 값으로 모든 조합을 구성해 테스트하기가 상당히 부담스러워진다.

많이 쓰는 단항 형식

  1. 인수에 질문을 던지는 경우
  2. 변환 함수(인수를 뭔가로 변환해 결과를 반환하는 경우)
    • 출력 인수를 사용하면, 혼란을 일으킨다.
    • 변환 결과는 반환값으로 돌려줘야 한다.
  3. 이벤트 함수
    • 입력 인수만 있고 출력 인수는 없다.
    • 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.

플래그 인수

  • 함수로 부울 값을 넘기는 관례는 좋지 않다.
    • 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈

인수 객체

  • 인수가 2~3개 필요하다면, 일부를 독자적인 클래스 변수로 선언할 가능성이 높다.
    • 변수를 묶어서 넘기려면 이름을 붙여야하는데 이때, 개념을 표현하게 된다.

동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.
    • ex) write(name)
  • 함수 이름에 키워드(인수 이름)를 추가한다.
    • ex) assertExpectedEqualsActual(expected, actual)

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

  • 한 가지를 하겠다고 약속하고 다른 일을 하는 경우
  • 예상치 못하게 클래스 변수를 수정하는 경우
  • 함수로 넘어온 인수나 시스템 전역 변수를 수정하는 경우
  • 시간적인 결합이 필요한 경우에는 함수 이름에 분명히 명시해야 한다.

출력 인수

  • 함수의 선언부를 찾아보는 행위 = 코드를 보다가 주춤하는 행위
  • this를 통해 출력 인수를 대체할 수 있다.
  • 함수에서 상태를 변경해야 한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
    • 오류 코드를 반환하면, 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.
  • 오류 코드 대신 예외를 사용하면, 오류 처리 코드가 원래 코드에서 분리된다.

Try/Catch 블록 뽑아내기

  • 코드 구조에 혼란을 일의며, 정상 동작과 오류 처리 동작을 뒤섞는다.
    그래서 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.
  • 정상 동작과 오류 처리 동작을 분리하면, 코드를 이해하고 수정하기 쉬워진다.

오류 처리도 한 가지 작업이다.

  • 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
  • 함수에 try 키워드가 있다면, try로 시작해 catch/finally 문으로 끝나야 한다.

반복하지 마라!

  • 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 반복되는 곳을 모두 손봐야 한다.
  • 중복을 줄이면, 모듈 가독성이 크게 높아진다.
  • 객체지향 프로그래밍에서는 부모 클래스로 몰아 중복을 없앤다.
  • 구조적 프로그래밍, AOP, COP 모두 어떤 면에서 중복 제거 전략이다.

구조적 프로그래밍

  • 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다.
    즉, return 문이 하나여야 한다. - 루프 안에서 break 또는 continue를 사용해서는 안되며 goto는 절대로 안 된다.
  • 함수가 아주 클 때만 상당한 이익을 제공한다.
    함수를 작게 만든다면, 단일 입/출구 규칙보다 return, break, continue가 의도를 표현하기 쉬워지는 경우도 있다.

함수를 어떻게 짜죠?

  • 처음에는 길고 복잡하다.
    • 들여쓰기 단계도 많다.
    • 중복된 루프도 많다.
    • 인수 목록도 아주 길다.
    • 이름은 즉흥적이다.
  • 코드를 다듬는다.
    • 함수를 만든다.
    • 이름을 바꾼다.
    • 중복을 제거한다.
    • 메서드를 줄인다.
    • 순서를 바꾼다.
  • 이렇게 수정하면서도 항상 단위 테스트를 통과한다.