작게 만들어라!
프로그램의 기본적인 단위는 함수이다. 함수를 만드는 첫째 규직은 '작게'다. 함수를 만드는 둘째 규칙은 '더 작게'다. 80년대에는 함수가 한 화면을 넘어가면 안된다고 말했다.
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();
}
위 코드도 길다 되도록 3~5줄 이내로 줄이는 것을 권장한다.
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
블록과 들여쓰기
다시 말해, if / else / while 문 등에 들어가는 블록은 한줄이여야 한다는 의미이다. 대개 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수 (enclosing function)가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.
한가지만 해라!
"함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야한다." 여기서 문제라면 그 한 가지가 무엇인지 알기가 어렵다는 점이다. 위에 제시된 코드에서 한 가지만 하고 있는가? 세 가지를 한다고 주장할 수도 있다.
- 페이지가 테스트 페이지인지 확인한다.
- 그렇다면 설정 페이지와 해제 페이지를 넣는다.
- 페이지를 HTML로 렌더링한다.
위에서 언급된 세 단계는 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 하는 것이다.
-> renderPageWithSetupsAndTeardowns
함수가 한 가지 이상의 일을 하는지 확인할 수 있는 좋은 방법은, 했던 말을 거의 다시 되뇌이지 않는 단계로 더 추상화할 수 있는지 확인하는 것이다.
함수 내 섹션
함수가 '한 가지'만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미 잇는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다. 한 가지 작업만 하는 함수는 자연스럽게 센션으로 나누기 힘들다.
함수 당 추상화 수준은 하나로!
- 함수가 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
- 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
- 코드는 위에서 아래로 이야기 처럼 읽혀야 좋다.
Switch 문
switch문은 작게 만들기 어렵다. case 분기가 단 두 개인 switch 문도 너무 길며, 또한 '한 가지' 작업만 하는 switch 문도 만들기 어렵다. 본질적으로 swtch문은 N가지 처리를한다. 불행하게도 switch 문을 완전히 피할 방법은 없다. 하지만 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다. 다형성을 이용한다.
ublic 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);
}
}
위 함수에는 몇 가지 문제가 있다. 첫째, 함수가 길다. 새 직원 유형을 추가하면 더 길어진다. 둘째, '한 가지'
작업만 수행하지 않는다. 세째, SRP(단일책임원칙)를 위반한다. 네째, OCP(개방폐쇄원칙)를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다. 하지만 가장 심각한 문제는 위 함수와 구조가 동일한 함수가 무한정 존재한다는 사실이다. 예를 들어, 다음과 같은 함수가 계속 중복될 것이다.
isPayDay();
calculatePay()
deliveryPay(Money pay);
이 문제를 해결한 코드가 아래 코드이다. switch문을 추상 팩토리 ( abstract factory )에 꽁꽁 숨긴다. 아무에게도 보여주지 않는다. 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다. 위 중복되는 함수는 Employee 인터페이스를 통해 가져오도록 한다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
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 SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
switch 문을 사용할 때에는 다형적 객체를 생성하는 코드 안에서 사용하도록 하자!
서술적 이름을 사용하라!
"크드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다."
이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 이름을 정하느라 시간을 들여도 괜찮다.
이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage 등이 좋은 예다. 문체가 비슷하면 이야기를 순차적으로 풀어가기도 쉬워진다. 방금 열거함 함수를 살펴보자. includeTeardownPage 있나요?
함수 인수