## 목차 ##



## 들어가면서 ##

  • 함수는 모든 프로그램의 기본 단위이다.

  • 의도를 분명히 포함하며 처음읽는 사람이 직관적으로 파악하기 쉬운 함수를 만들어라.



## 작게 만들어라! ##

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

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

<bad\>

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문의 몸통 부분을 'includeSetUpAndTeardownPages()' 함수로 따로 빼어 아래와 같이 더 작게 만들 수 있다.



<good\>

public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 
   if (isTestPage(pageData)) 
       includeSetupAndTeardownPages(pageData, isSuite); 
   return pageData.getHtml();
}


  • 함수 하나가 이야기 하나를 표현해야한다.

  • if문, else문, while문의 블럭은 1줄이어야한다.

    • 장황하게 늘어놓기보단 함수를 여러번 호출하는 것이 낫다는 의미이다.
    • 그 함수 이름을 적절히 짓는다면 코드를 이해하는 것이 쉬워진다.
    • 중첩구조가 생길만큼 함수가 커져서는 안된다.
    • 함수에서 들여쓰기 수준은 1단, 최대 2단을 넘어서면 안된다.



## 한 가지만 해라! ##

  • 함수는 한 가지를 해야하며, 그 한 가지를 잘해야하고, 한 가지만을 해야한다.

  • 추상화 수준이 하나인 단계만 수행하는것이라면 그 함수는 '한 가지'만 한다고 할 수 있다.

public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 
   if (isTestPage(pageData)) 
       includeSetupAndTeardownPages(pageData, isSuite); 
   return pageData.getHtml();
}

아까 봤던 함수를 다시 가져왔다. 이 함수가 수행하는 내용은 아래의 세 단계이다.

1. 페이지가 테스트 페이지인지 판단한다.
2. 그렇다면 설정 페이지와 해제 페이지를 넣는다.
3. 페이지를 HTML로 렌더링한다.

위 세 단계는 추상화 수준이 하나다. 테스트페이지인지 확인후 맞다면 설정 및 해제 페이지를 넣는다. 테스트 페이지이든 아니든 HTML로 렌더링한다. 위 함수는 의미를 가지면서 더 작게 조개기란 불가능하다.

  • 함수 내부의 코드 특정 부분을 의미있는 이름을 가지는 다른 함수로 추출할 수 있다면 그 함수는 여러 작업을 하고있는것이므로 쪼개라.



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

  • 함수 내부에 여러 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.

    • 핵심 개념인지 세부적인 사항인지 헷갈리기 때문이다.
    • 근본사항과 세부사항이 섞이기 시작하면 깨어진 창문처럼 사람들이 세부사항을 점점 더 추가하게 되고 더욱 복잡해진다.
  • 위에서 아래로 코드 읽기: 내려가기 규칙

    • 코드는 위에서 아래로 이야기처럼 읽히는것이 좋다.
    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
    • 위에서 읽으면 추상화 수준이 한 단계씩 낮아진다.

아래 코드는 추상화 수준이 가장 높은 함수부터 낮은 함수까지 내려가기 때문에 이야기처럼 읽힐 것이다.

public class WorkABC {
    public void doABC() {
        doA();
        doB();
        doC();
    }

    public void doA() {
        do_Afront();
        do_Aback();
    }

    public void doB() {
        do_Bfront();
        do_Bback();
    }

    public void doC() {
        do_Cfront();
        do_Cback();
    }

    public void do_Afront(){...}

    public void do_Aback(){...}

    public void do_Bfront(){...}

    public void do_Bback(){...}

    public void do_Cfront(){...}

    public void do_Cback(){...}
}



## Switch 문 ##

  • case분기가 두 개인 switch문도 저자의 성향에는 길다.

    • 이 책의 저자는 단일 블록이나 함수를 선호한다.
  • switch문은 본질적으로 여러가지를 처리하기에 한 가지 작업만하는 switch문을 만들기 어렵다.

  • 본질적으로 switch문을 피할 방법은 없다.
    + 그러나 switch문을 저차원 클래스에서 한 번만 만들고 캡슐화와 다형성을 사용하여 숨겨 반복하지 않는 방법이 있다.


public Money calculatePay(Employee e) throws InvalidEmployeeType {
    switch (e.Type) {
        case COMMINSSIONED: 
            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 Money calculatePay();
    public abstract boolean isPayDay();
    public abstract void deliveryPay(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);
        }
    }
}
  • switch문을 한 번만 사용한다.
    • switch문을 다형적 객체를 생성하는 추상팩토리에서만 작성했다.
    • 이후에 객체(Employee) 종류별로 추상클래스의 메소드를 구현하면 된다.



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


워드 커닝햄(Ward Cunningham) - Wiki 창시자

"코드를 읽으며 짐작한 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드이다."

  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉽다.

  • 이름이 길어도 괜찮다. 서술적인 주석보다 훨씬 낫다.



## 함수 인수 ##

  • 이상적인 함수 인수(매개변수)의 수는 0개다.

  • 그 다음으로는 1개(단항)가 좋다.

  • 그 다음이 2개(이항)이다.

  • 3개(삼항)는 가능한 피하는 편이 좋다.

  • 4개 이상에는 특별한 이유가 필요하다.



(1) 많이 쓰는 단항 형식

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

    • boolean fileExists(File file)
  • 인수를 뭔그로 반환해 결과를 반환하는 경우

    • InputStream fileOpen(File file)
  • 이벤트

    • 입력 인수만 있고 출력 인수는 없다.
    • 함수 호출시 인수로 시스템 상태를 바꾼다.
    • passwordAttemptFailedNtimes(int attempts) 같은 함수

위에서 말한 세 가지 경우 이외의 단항 함수는 가급적 피한다.



(2) 플래그 인수

  • 플래그(flag) 인수는 추하다.
    • 함부로 불리언을 넘기는 것은 끔직하다.
    • 한 번에 여러가지를 처리한다고 광고하는 것이다.(참이면 이걸하고 거짓이면 저걸한다는 말이다.)
    • render(boolean isSuite) 같은 함수는 renderForSuite()renderForSingleTest()로 나누어야 마땅하다.



(3) 이항함수

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

    • OutputStream 클래스를 따로 만들고 거기에 writeField(name) 메소드를 포함시킨 후 outputStream.writeField(name)의 형태로 호출할수도 있다.
  • 이항함수가 적절한 경우도 있다.

    • 직교좌표계의 점 Point p = new Point(x,y) 처럼 2개의 요소가 하나의 값을 표현하는 경우
    • 두 인수간에 자연적인 순서가 있는 경우도 이항함수를 사용해도 괜찮다.
    • 반면 outputStream과 name에는 한 값을 표현하지도, 자연적인 순서가 있지도 않다.



(4) 삼항함수

  • 일반적으로 삼항 함수는 매우 헷갈리므로 불가피할 경우에만 신중하게 작성한다.
    • 인수의 순서에도 신경을 써야한다.
    • 메소드 이름에 인수를 순서대로 표현하는 방법이 있다.



(5) 인수 객체

  • 인수가 2, 3개 이상 필요하다면 인수들 중 일부를 따로 클래스로 만들어 뺄 수 있는지 살펴본다.
    • Circle makeCircle(double x, double y, double radius)
    • Circle makeCircle(Point center, double radius) 위 함수의 x, y를 묶어 하나의 점으로 표현했다.



(6) 인수 목록

  • 단항 함수는 함수이름과 인수가 동사/명사 쌍을 이룬다.

    • write(name)은 이해하기 쉽다.
    • writeField(name)은 의미가 더욱 분명하다.
  • 함수 이름에 키워드를 추가하면 인수 순서를 기억할 필요가 없다.

    • assertEquals(expected, actual)보다 assertExpectedEqualsActual(expected, actual)이 더 좋다.



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

  • 부수효과는 함수에서 하기로 하지 않은 일을 몰래 하는것이나 마찬가지다.
    • 예상치 못한 클래스 변수 수정, 시스템 변수 수정 등
    • 많은 경우 시간적인 결합 혹은 순서 종속성을 초래한다.
public class UserValidator {
    private Cryptographer cryptographer;
    public boolean checkPassword(String userName, String password) { 
        User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
            String codedPhrase = user.getPhraseEncodedByPassword(); 
            String phrase = cryptographer.decrypt(codedPhrase, password); 
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true; 
            }
        }
        return false; 
    }
}
  • Session.initialize()는 부수효과다. 세션이 초기화된다.

  • checkPassword 함수의 이름은 비밀번호 확인이다.

    • 함수 이름만보고 사용자가 호출하면 세션정보를 잃을 위기에 처한다.
    • 시간상으로 비밀번호가 확인되면 항상 세션 초기화는 뒤따라온다.(시간 결합 - temporal coupling)
  • 메소드 이름을 checkPasswordAndInitializeSession()으로 고치는 것이 낫다.

    • 함수가 하나의 일을 하라는 규칙을 위반하지만



## 명령과 조회를 분리하라! ##

  • 함수는 뭔가를 수행하거나, 뭔가에 답하거나 둘 중 하나만 해야한다.
    • 둘 다 하면 안된다.
    • 객체 상태 변경, 객체 정보 반환 둘 중 하나다.
public boolean set(String attribute, String value);

이 함수는 속성(attribute)를 찾아 값(value)로 값을 설정한다. 성공하면 true, 실패하면 false를 반환한다.

하지만 읽는 사람 입장에서는 파악하기가 어렵다. attribute가 value로 세팅되어있는지 확인하는 것으로 오해할 확률이 높다. 따라서 아래와 같이 분리하는 것이 좋다.

if(attributeExists(attribute)) {
    setAttribute(attribute, value);
}



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

  • 오류 코드를 반환해서 체크하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
    • if문에서 명령을 표현식으로 사용하기 쉽기 때문이다.
if (deletePage(page) == STATUS.OK) {
    if (registry.deleteReference(page.name) == STATUS.OK) {
        if (configKeys.deleteKey(page.name.makeKey()) == STATUS.OK) {
            logger.log("page deleted");
        } else {
            logger.log("configKey not deleted");
        }
    } else {
        logger.log("deleteReference from registry failed"); 
    } 
} else {
    logger.log("delete failed"); return E_ERROR;
}
  • 오류코드를 사용함으로써 여러 단계로 중첩되는 코드가 야기된다.

  • 블록을 별도 함수로 만들고 예외처리를 사용하면 코드가 깔끔해진다.


try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
    logError(e);
}
  • 사실 try/catch 블록도 보기 추하다.
    • 아래와 같이 try/catch문도 별도의 블록으로 뽑아내는 것이 더 좋다.
deletePage(page);

---------------------

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());
}
  • 위에서 delete 함수가 모든 오류를 처리하고 페이지를 실제로 삭제하는 함수는 deletePageAndAllReferences 함수이다.

    • 정상동작과 오류를 분리하면 코드를 이해하고 수정하기 쉽다.
  • 오류코드는 Enum이든 클래스에서든 정의되어 있다.

    • 위와같은 클래스는 의존성 자석이다.
    • Enum이 바뀌면 이를 사용하는 클래스들에서 재배치를 해야하므로 변경이 쉽지않다.(OCP 위배)
    • 예외처리를 사용하면 새 예외 클래스만 추가하면 된다.



## 반복하지 마라! ##

  • 중복은 소프트웨어 모든 악의 근원이다.
    • 많은 원칙과 기법들이 중복 제거 목적으로 나왔다.



## 함수를 어떻게 짜죠? ##

이 책의 저자는 아래와 같은 흐름대로 함수를 짠다고 한다.

  • 처음에는 길고 복잡하다.

    • 들여쓰기도 많고 중복 루프도 많다.
    • 인수 목록도 길다.
    • 이름도 즉흥적이다.
  • 이후에 하나씩 고친다.

    • 코드를 다듬고
    • 함수를 쪼개고
    • 이름을 고치고
    • 중복을 제거한다
    • 기타등등
  • 결국 최종적으로 이 장의 규칙을 따르는 함수가 얻어진다.

+ Recent posts