함수
기본 원칙
함수는 최대한 작을수록 좋다. 또한, 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야한다.
if/else/while 문 등에 들어가는 블록은 한 줄이어야 한다.
다만, '한 가지'가 무엇인지 알기 어렵다는 점은 존재한다. 예를 들어, 함수 내에서 다음 3가지 동작을 한다고 가정해보자:
- 페이지가 테스트 페이지인지 판단한다.
- 그렇다면 설정 페이지와 해제 페이지를 넣는다.
- 페이지를 HTML로 렌더링 한다.
목록만으로 판단했을 때, 이는 세 가지를 하는 것처럼 보인다. 하지만, 이는 어떻게 구현하냐에 따라 달라질 수 있다. 즉, 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면, 그 함수는 한 가지 작업만 하는 것이다:
function renderPageWithSetupsAndTeardowns(pageData, isSuite) {
if (isTestPage(pageData)) {
includeSetupAndTeardownPages(pageData, isSuite);
}
return pageData.getHtml();
}
함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 저자는 이것을 내려가기 규칙이라고 부른다.
Switch 문
switch 문은 작게 만들기 어렵다. 본질적으로 switch 문은 N가지를 처리한다.
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);
}
}
위 함수에는 몇 가지 문제가 있다. 첫째, 함수가 길다. 새 직원 유형을 추가하면 더 길어진다. 둘째, '한 가지' 작업만 수행하지 않는다. 셋째, SRP 를 위반한다. 넷째, OCP 를 위반한다.
게다가 비슷한 switch 문을 활용하는 여러 함수가 추가될 가능성이 무한하다. 즉, isPayday(Employee e, Date date); 혹은 delvierPay(Employee e, Money pay); 등이 추가될 때 모두 switch 문이 사용되어야 한다.
이를 어떻게 개선할 수 있을까? 정답은 추상 팩토리 를 통해 switch 문을 꽁꽁 숨기는 것이다.
abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
class EmployeeFactory {
public static 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);
}
}
}
class CommissionedEmployee extends Employee {
public CommissionedEmployee(EmployeeRecord r) { /* ... */ }
public boolean isPayday() { /* ... */ }
public Money calculatePay() { /* ... */ }
public void deliverPay(Money pay) { /* ... */ }
}
// Similar classes for HourlyEmployee and SalariedEmployee...
이런식으로 구현하게 되면, 처음 Employee 인스턴스 생성할 때를 제외하곤 switch 문을 사용할 필요가 없다. 저자는 일반적으로 이와 같이 다형적 객체를 생성하는 코드 내에서만 switch 문을 한 번만 참아준다고 말한다.
물론, 저자도 이 규칙을 위반하는 불가피한 상황이 생긴다고는 한다.
플래그 인수는 추하다.
함수로 부울 값을 넘기는 것은 끔찍하다. 함수가 한번에 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
서술적인 이름을 사용하라!
testableHtml 보다는 SetupTeardownIncluder.render() 와 같은 방식으로 함수를 사용하는 것이 해당 함수가 하는 일을 좀 더 잘 표현한다.
이름은 길어도 괜찮다. 겁먹을 필요없다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 이름을 정하느라 시간을 들여도 괜찮고, 이런저런 이름을 넣어 코드를 읽어보는 것도 좋다.
또한, 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용해야 한다.
함수 인수
함수에서 이상적인 인수 개수는 0개 (무항) 이며, 다음은 1개 (단항)이고, 그 다음은 2개(이항)이다. 3개(삼항) 이상은 가능한 피하는 편이 좋다.
인수가 적어야 코드를 읽는 사람의 가독성이 올라가고, 테스트 코드가 단순해 질 수 있다.
물론 이항 함수가 적절한 경우도 있다. Point p = new Point(0, 0) 이 좋은 예다. 직교 좌표계 점은 일반적으로 인수 2개를 취한다. 게다가 두 요소에는 자연적인 순서도 있다.
하지만, 그런 경우를 제외하고는 이항 함수도 피하는 편이 좋다. 아주 당연하게 여겨지는 이항 함수 assertEquals(expected, actual) 에도 문제가 있다. expected 인수에 actual 을 넣게 되는 경우가 많을 것이다.
인수가 2-3개 필요하다면, 일부를 독자적인 클래스 변수로 선언해서 사용하는 것이 적절할 수 있다. 예를 들면 다음과 같다:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Pint center, double radius);
두번째 함수가 개념적으로 더 풍부하고, 인수 전달 순서가 헷갈릴 여지가 적다.
부수 효과를 일으켜선 안된다.
함수는 한가지 일만을 해야한다는 원칙에 포함된다. 예를들어, userName 과 password 를 확인하는 checkPassword 메소드 내부에서 성공할 경우에 세션을 초기화 하게 된다면 이는 부수 효과를 일으키는 것이다. 함수명에는 이러한 정보가 전혀 드러나지 않는다. 이 함수를 사용하는 팀원은 함수 이름만 보고 사용자를 인증하면서 기존 세션 정보를 지워버릴 수 있다.
Try/Catch 블록 뽑아내기
try/catch 블록은 원래 추하다. 각각의 블록을 별도의 함수로 뽑아내는 편이 좋다.
private void delete(Page page) {
try {
deletePage(page);
} catch (Exception e) {
logError(e);
}
}