가로 150자를 넘어서는 안 된다. 함수는 100줄을 넘어서는 안 된다. 아니 20줄도 길다
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다
//목록 3-2, from p.42
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-3, from p.43
public static String renderPageWithSetupsAndTeardowns{
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
...(중략)
목록 3-2도 추상화 수준이 둘이다. 그래서 목록 3-3으로 축소가 가능했다. 하지만 의미를 유지하면서 목록 3-3을 더 이상 줄이기란 불가능하다.
페이지가 테스트 페이지인지 확인 한 후
테스트 페이지라면
if문과 관계 없이, 페이지를 html로 렌더링하여 반환합니다.
이렇게 보면, a ~ f에 해당하는 내용을 함수로 추상화해야 한다는 것을 확실히 느낄 수 있습니다.
public Money calculatePay(Employee e) throws InvalidEmployeeType{
switch (e.type) {
case COMISSIONED:
return calculateComissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
무슨 문제가 있는 것일까요?
1. 함수가 길다
→ 20줄이 넘어가는 수준은 아니지만, 짧을 수록 좋다는 책의 맥락에서 보면 길다고도 볼 수 있을 듯 합니다.
2. ‘한 가지' 작업만 수행하지 않는다.
→ case 안에 이미 세 개의 다른 메서드가 포함된 것을 확인할 수 있습니다.
→ 이 부분은 switch 문의 태생이 그렇기 때문에 어쩔 수 없는 것이 아니냐고 하실 수도 있습니다. 저자도 이 부분에 대해서 인지하고 있기에, 책의 이어지는 페이지에서 다음과 같이 말합니다.
*일반적으로 나는 switch 문을 단 한 번만 참아준다. 다형성 객체를 생성하는 코드 안에서다.
...(중략)
물론 불가피한 상황도 생긴다. 나 자신도 이 규칙을 위반한 경험이 여러 번 있다.
저자까지 이렇게 말 할 정도면, 정말 어쩔 수 없는 부분이긴 한 것 같습니다 :(
3. SRP를 위반한다. 코드를 변경할 이유가 여럿이기 때문이다.
→ 조금 어렵습니다. 코드를 변경할 이유가 왜 여럿일까요?
→ 만약 계산 할 때, 다른 여러 조건을 고려해야 하게 된다면 어떻게 될까요? 예를 들어 Employee의 근무 일수를 따져서 계산해야 할 수가 있습니다. 이런 경우에 위 코드에서는 calculatePay() 메서드의 변경이 불가피하겠죠.
→ 코드가 calculateXXXPay() 라는 구체적 메서드에 의존하고 있기 때문에 이러한 현상이 발생합니다. 이는 이어지는 OCP 관련 내용과도 연관이 있습니다.
4. OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다
→ 직관적으로 이해 가능한 문제입니다. 동시에, 해결할 수 없는 문제이기도 합니다.
5. 가장 심각한 문제는, 위 함수와 구조가 동일한 함수가 무한정 존재한다는 것.
→ isPayday(Employee e, Date date), deliverPay(Employee e, Money pay) 등의 함수를 써야 하는 케이스가 발생한다면, 이 함수들 내에 동일한 switch 문을 작성해야 합니다. 이는 개발자에게 악몽과도 같은 일이죠...ㅎ;
해결책? 추상화와 다형성!
calculatePay() 함수의 가장 큰 문제점은 switch 문을 통해 직원 유형을 파악한 뒤, 구체적인 메서드를 실행한다는 점입니다. 이에 따라 변화에 유연하게 대응하지 못하는 코드가 되는 것입니다.
이럴 때 Java 진영에서는 추상화를 통해 문제를 해결합니다.
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 InvaludEmployeeType;
}
//
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMISSIONED:
return new ComissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return SalariedEmployee(r);
default:
throw new InvalidEmployeeType(e.type);
}
이렇게 하면, makeEmployee를 통해 생성받은 객체에서 isPayday(), calculatedPay() 등의 메서드를 실행하면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다!