어떤 프로그램이든 가장 기본적인 단위가 함수다. 이 장은 함수를 잘 만드는 법을 소개한다.
함수를 만드는 첫째 규칙은 '작게'다. 이 책의 저자는 함수가 적을 수록 좋다는 근거를 대기는 좀 어렵지만 40여년 동안 온갖 크기로 함수를 구현해봤을 때 작은 함수가 좋았다라고 확신한다고 한다.
public static String renderPageWithSetupsAndTeardowns(Page Date pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownpages(pageData, isSuite);
return pageData.getHtml();
}
if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미다. 대개 거기서 함수를 호출하고 그러면 바깥을 감싸는 함수가 작아질 뿐만 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.
기존의 수십줄의 코드는 여러 가지를 처리한다. (버퍼 생성, 페이지를 가져오고, 상속된 페이지를 검색하고, 경로를 렌더링, 불가사의한 문자열을 덧붙이고, HTML을 생성한다.)
반면 위의 개선된 코드는 설정 페이지와 해제 페이지를 테스트 페이지에 넣는다.
한 함수 내에서 여러가지 섹션으로 나눠진다면 여러가지 작업을 한다는 증거다. 한 가지 작업만 하는 함수는 섹션을 나누기가 어렵다.
함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
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):
}
}
위 코드는 몇가지 문제점이 있다.
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculcateDay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory{
@Override
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmplyee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type):
}
}
}
저자는 불가피한 상황도 생기지만 switch문을 다형성 객체를 생성해주는 코드에서 단 한번만 참아주는 것이 좋다고 주장한다.
testableHtml보다 SetupteardownIncluder.render와 같이 함수가 하는 일을 좀 더 잘 표현하는 이름이 좋다.
함수에서 인수는 적을 수록 이상적이다. 3개는 가능한 피하고 4개 이상은 특별한 이유가 필요하지만 이유가 있어도 사용하면 안된다고 주장한다.
boolean fileExists("Myfile")
InputStream fileOpen("MyFile")
passwordAttemptFailedNtimes(int attempts)
위 경우를 제외하고는 사용하지 단항 함수는 가급적 피해야한다.
void transform(StringBuffer out)와 같이 출력 인수를 변환 함수에서 사용하면 혼란을 일으킨다.
StringBuffer transform(StringBuffer in) 이 좀 더 낫다.
부울 값을 인수로 넘기는 것은 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
플래그 인수는 사용하지 않는 것이 좋다.
인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다. 좌표를 생성하는 Point p = new Point(0,0) 과 같은 코드는 물론 예외겠지만 인수가 많을 수록 혼란을 야기한다.
assertEquals(expected, actual)도 첫번째 인수가 expected고 두번째 인수가 actual 이라는 것을 인지하고 있어야한다. 될 수 있으면 단항 인수가 더 좋다.
인수가 두 개인 함수보다 더 이해하기 어렵다. 신중히 고려해야한다.
인수가 2-3개 필요하다면 일부를 묶어 클래스 변수로 만드는 것을 고려 해본다.
때로는 인수 개수가 가변적인 함수도 필요하다. (String.format())
가변 인수 전부를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다.
-> String.format()은 이항 함수
함수의 의도나 인수의 순서와 의도를 제대로 표현하기 위해 좋은 함수이름이 필수.
단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.
assertEquals보다 assertExpectedEqualsActual이 인수의 순서를 기억할 필요 없으므로 좋다.
부수 효과란 예상치 못하게 클래스 변수를 수정하거나 인수나 시스템 전역 변수를 수정하는 등의 행위를 말한다.
password를 체크하는 checkPassword() 함수에서 세션을 초기화 하는 등의 행동이 있다면 이것은 부수 효과라 할 수 있다. 함수 이름에서는 비밀번호가 일치하는지 확인하는 행동만을 예상할 수 있다. 그러므로 checkPasswordAndInitializeSession과 같은 함수 이름이 더 적합하다.
appendFooter(s);
일반적으로 우리는 s를 입력 인수로 생각하고 s를 바닥글로 첨부할 것이라고 생각하지만 만약에 함수 선언부가 public void appendFooter(StringBuffer report) 라면? 이것이 출력 인수라면? 인지적으로 굉장히 거슬린다.
함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
public boolean set(String attribute, String value);
위 함수는 attribute인 속성값을 찾아 value로 설정하고 성공하면 true, 실패하면 false를 반환하는 함수라 했을 때
if (set("username", "unclebob"))
위 구문은 굉장히 모호하다. 관점에 따라 다르게 해석될 수 있다.
아래와 같이 개선할 수 있다.
if (attributeExists("username")) {
setAttribute("username", "unclebob");
}
오류 코드를 사용하면 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오 코드를 곧바로 처리해야한다는 문제점에 부딪힌다.
if (deletePage(page) == OK)
{
if(registry.deleteReference(page.name) == OK)
{
if(configKeys.deleteKey(page.name.makeKey()) == OK)
{
...
}
}
}
반면 예외 처리는 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
try {
deletePage(page);
registry.deleteReference(page.name)
configKeys.deleteKey(page.name.makeKey())
}
catch(Exception e) {
...
}
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
private void deletePageAndAllReferences(Page page) throws Exception
{
deletePage(page);
registry.deleteReference(page.name)
configKeys.deleteKey(page.name.makeKey())
}
private void logError(Exception e)
{
...
}
함수는 한가지 작업만 해야 하고 오류 처리도 한 가지 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
오류 코드를 반환하다는 이야기는, 클래스든 열거형 변수든, 어디선가 오류 코드를 정의한다는것.
다른 클래스에서 Error enum을 import해야 하므로 의존성이 생긴다.
오류 코드 대신 예외를 사용하면 새 예외는 Exception에서 파생되므로 새 예외 클래스를 쉽게 추가할 수 있다.
코드의 중복은 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러군데에서 손봐야한다. 그에 따라 오류가 발생할 확률도 높다.
구조적 프로그램이, AOP, COP 모두 중복 제거 전략이다.
함수는 return문이 하나여야 한다. 루프 안에서 break, continue, goto 사용은 금물.
이 규칙은 함수가 클 때 큰 효과를 얻는다.
함수를 작게 만든다면 사용해도 괜찮다. 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
반면 goto는 작은 함수에서는 피해야한다. (그냥 안쓰는게 좋은듯)
길고 복잡한 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고, 순서를 바꾸는 등 점진적으로 수정해 나가며 규칙을 만족하는 함수를 만든다.