3장: 함수

작게 만들어라!

  • 함수를 만드는 첫째 규칙은 '작게!', 둘째 규칙은 '더 작게!' 이다.

  • 아래 코드는 추상화 수준이 다양하고, 코드도 너무 길다. 두 겹으로 중첩된 if 문은 이상한 플래그를 확인하고, 이상한 문자열을 사용하며, 이상한 함수를 호출한다.

    // Listing 3-1
    public static String testableHtml(
      PageData pageData,
      boolean includeSuiteSetup
    ) throws Exception {
      WikiPage wikiPage = pageData.getWikiPage();
      StringBuffer buffer = new StringBuffer();
      if (pageData.hasAttribute("Test")) {
        if (includeSuiteSetup) {
          WikiPage suiteSetup =
            PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
          if (suiteSetup != null) {
            WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
            String pagePathName = PathParser.render(pagePath);
              buffer.append("!include -setup .")
                    .append(pagePathName)
                    .append("\n");
          }
        }
        WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
        if (setup != null) {
          WikiPagePath setupPath =
            wikiPage.getPageCrawler().getFullPath(setup);
          String setupPathName = PathParser.render(setupPath);
          buffer.append("!include -setup .")
                .append(setupPathName).append("\n");
        }
      }
      buffer.append(pageData.getContent());
      if (pageData.hasAttribute("Test")) {
        WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
        if (teardown != null) {
          WikiPagePath tearDownPath =
            wikiPage.getPageCrawler().getFullPath(teardown);
          String tearDownPathName = PathParser.render(tearDownPath);
          buffer.append("\n")
                .append("!include -teardown .")
                .append(tearDownPathName)
                .append("\n");
          if (includeSuiteSetup) {
            WikiPage suiteTeardown =
              PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
            if (suiteTeardown != null) {
              WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
              String pagePathName = PathParser.render(pagePath);
              buffer.append("!include -teardown .")
                    .append(pagePathName)
                    .append("\n");
            }
          }
        }
        pageData.setContent(buffer.toString());
        return pageData.getHtml();
      }
    }
  • 해당 코드는 이렇게 바꿀 수 있다. 위와 달리 아래쪽은 "설정 페이지와 해제 페이지를 테스트 페이지에 넣은 후 해당 테스트 페이지를 HTML로 렌더링하는" 기능임을 읽어낼 수 있다.

    // Listing 3-2
    public static String renderPageWithSetupsAndTeardowns(
      PageData pageData, boolean isSuite
    ) throws Exception {
      boolean isTestPage = pageData.hasAttribute("Test");
      if (isTestPage) {
        WikiPage testPage = pageData.getWikiPage();
        StringBuffer newPageContent = new StringBuffer();
        includeSetupPages(testPage, newPageContent, isSuite);
        newPageContent.append(pageData.getContent());
        includeTeardownPages(testPage, newPageContent, isSuite);
        pageData.setContent(newPageContent.toString());
      }
      return pageData.getHtml();
    }
  • if문, else문, while문 등에 들어가는 블록은 한 줄이어야 하며, 대개 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수가 작아질 뿐만 아니라 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다.

한 가지만 해라!

  • "함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야한다. 그 한 가지만을 해야 한다."

  • 한 가지가 무엇일까?

    • 위의 코드는 세 가지를 한다고 주장할 수도 있다.

      1. 페이지가 테스트 페이지인지 판단

      2. 그렇다면 설정 페이지와 해제 페이지를 넣는다.

      3. 페이지를 HTML로 렌더링한다.

  • 위에서 언급하는 세 단계는 지정된 함수 이름 아래에서 추상화 수준이 하나다. 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.

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

  • 함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

    • 3-1은 이 규칙을 확실히 위밚나다. getHtml()은 추상화 수준이 높고, String pagePathName = PathParser.render(pagePath);는 중간이며, .append("\n")와 같은 코드는 추상화 수준이 아주 낮다.

  • 내려가기 규칙(The Stepdown Rule): 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아진다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다!

Switch문

  • 본질적으로 switch문은 N가지를 처리하기 때문에 작게 만들기가 어렵다.

// Listing 3-4
public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED:
      return calculateCommissionedPay(e);
    case HOURLY:
      return calculateHourlyPay(e);
    case SALARIED:
      return calculateSalariedPay(e);
    default:
    throw new InvalidEmployeeType(e.type);
  }
}
  • 위의 함수에는 몇 가지 문제가 있다.

    1. 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.

    2. 한 가지 작업안 수행하지 않는다.

    3. SPR를 위반한다. 코드를 변경할 이유가 여럿이기 때문이다.

    4. OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다.

// Listing 3-5
public abstract class Employee {
  public abstract boolean isPayday();
  public abstract Money calculatePay();
  public abstract void deliverPay(Money pay);
}
-----------------
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 COMMISSIONED:
        return new CommissionedEmployee(r);
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmployee(r);
      default:
        throw new InvalidEmployeeType(r.type);
    }
  }
}
  • 위는 3-4의 문제를 해결한 코드이다.

    • switch문을 추상 팩토리에 꽁꽁 숨긴다. 즉, 아무에게도 보여주지 않는다.

    • 팩토리는 switch문을 사용하여 적절한 Employee 파생 클래스의 인스턴스를 생성한다.

    • calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

  • 물론 불가피하게 이 규칙을 위반하는 상황도 생긴다.

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

  • SetupTeardownIncluder.render, isTestable, includeSetupAndTeardownPages 등 길고 서술적인 이름을 택하라. 길고 서술적인 이름이 짧고 어려운 이름, 그리고 주석보다 좋다.

  • 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그런 다음 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.

  • 서술적이 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

  • 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개이다. 차선은 입력 인수가 1개뿐인 경우이다.

  • 3개 이상은 가능한 피하고, 4개 이상은 특별한 이유가 있어도 사용하지 않는다.

많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우

    boolean fileExists("MyFile")
  • 인수를 뭔가로 변환해 결과를 반환하는 경우

    InputStream fileOpen("MyFile")
  • 함수 이름을 지을 때는 두 경우를 분명히 구분한다. 또한 언제나 일관적인 방식으로 두 형식을 사용한다.

  • 드물게 사용하지만 유용한 단항 함수 형식: 이벤트

    • 입력 인수만 있고 출력 인수는 없다.

    • 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.

    • 이벤트라는 사실이 코드에 명확히 드러나야 한다.

  • 위의 경우가 아니라면 단항 함수는 가급적 피한다.

플래그 인수

  • 플래그 인수는 추하다. 함수로 bool 값을 넘기는 관례는 끔찍하다!

  • 플래그 인수는 그 자체만으로도 함수가 여러가지를 처리한다고 공표하는 셈이다.

이항 함수

  • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.

    • writeField(name)writeField(outputStream, name)보다 이해하기 쉽다.

  • 이항 함수가 적절한 경우

    • 직교 좌표계 표현 Point p = new Point(0, 0) 같이 한 값을 표현하는 두 요소가 있는 경우

  • 이항 함수는 불가피한 경우 사용해야 하지만, 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.

삼항 함수

  • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다. 신중히 고려해야 한다.

인수 객체

  • 인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.

    • 예시: Circle makeCircle(double x, double y, double radius)에서 x, y 값을 Point 객체로 묶어 Circle makeCircle(Point center, double radius) 꼴로 만들기

인수 목록

  • 인수 개수가 가변적인 함수도 필요하다.

    • 대표적으로 String.format(String format, Object... args)가 있음.

    • 첫 번째 인수를 제외하면 가변 인수 전부를 동등하게 취급하여 List 하나로 볼 수 있음. 이렇게 하면 사실상 이항 함수이다.

  • 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있다. 그러나 이를 넘어서는 인수를 사용할 경우 문제가 생긴다.

동사와 키워드

  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이루어야 한다.

    • write(name): name이 무엇을 뜻하던 쓴다(write)는 뜻이다.

    • writeField(name): name이 field라는 사실을 분명히 드러낸다.

  • 함수 이름에 키워드를 추가하는 형식도 있다. assertEquals(expected, actual) -> assertExpectedEqualsActual(expected, actual)

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

  • 부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓도 하니까.

  • 예상치 못하게 클래스 변수를 수정하거나, 함수로 넘어온 인수나 시스템 전역 변수를 수정하기도 한다.

  • 많은 경우 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래한다.

출력 인수

  • 일반적으로 우리는 인수를 함수 입력으로 해석한다.

  • 출력 인수의 경우 최대한 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다. (this)

명령과 조회를 분리하라!

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

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

  • 명령에서 명령의 결과로 오류 코드를 반환하는 것은 명령/조회 분리 규칙을 미묘하게 위반한다.

  • 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되어 더욱 깔끔해진다.

Try/Catch 블록 뽑아내기

  • try/catch 블록은 원래 추하다. 코드 구조에 혼란을 일으키며, 정상 동작과 오류처리 동작을 뒤섞는다.

  • try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.

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

Last updated