티스토리 뷰

반응형


| 3. 함수



Summary.

아무래도 가장 중요한 포인트는

함수는 "짧게", "한 가지 작업만", "서술적 이름으로"

조거문에 들어가는 블록은 한줄로

...



|| 작게 만들어라!



1
2
3
4
5
6
public static String renderPageWithSetupAndTeardowns (
    PageData pageData, boolean isSuite) throws Exception {
    if (isTestPage(pageData))
        includeSetupAndTeardownPages (pageData, isSuite);
    return pageData.getHtml();
}
cs


- if문/else문/while문 등에 들어가는 블록은 한 줄이어야 한다는 의미. (대개 이곳에서 함수를 호출)

- 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서는 안 된다.

* 그래야 함수는 읽고 이해하기 쉬워진다!



|| 한 가지만 해라!


-

* "함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다."


- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.

- 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위함이다는 것!


TO RenderPageWithSetupAndTeardowns : 페이지가 테스트 페이지인지 확인(isTestPage())한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다.(includeSetupAndTeardownPages()) 테스트 페이지든 아니든 페이지를 HTML로 렌더링한다.(getHtml())


* 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.



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


- 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

 


|| 내려가기 규칙


-

- 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

- 일련의 TO 문단을 읽듯이 프로그램이 읽혀야 한다

- 핵심은 "짧으면서도 '한 가지'만 하는 함수"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class SetupTeardownIncluder {
  private PageData pageData;
  private boolean isSuite;
  private WikiPage testPage;
  private StringBuffer newPageContent;
  private PageCrawler pageCrawler;
 
  public static String render(PageData pageData) throws Exception {
    return render(pageData, false);
  }
 
  public static String render(PageData pageData, boolean isSuite)
    throws Exception {
    return new SetupTeardownIncluder(pageData).render(isSuite);
  }
 
  private SetupTeardownIncluder(PageData pageData) {
    this.pageData = pageData;
    testPage = pageData.getWikiPage();
    pageCrawler = testPage.getPageCrawler();
    newPageContent = new StringBuffer();
  }
 
  private String render(boolean isSuite) throws Exception {
    this.isSuite = isSuite;
    if (isTestPage())
      includeSetupAndTeardownPages();
    return pageData.getHtml();
  }
 
  private boolean isTestPage() throws Exception {
    return pageData.hasAttribute("Test");
  }
 
  private void includeSetupAndTeardownPages() throws Exception {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
  }
  private void includeSetupPages() throws Exception {
    if (isSuite)
      includeSuiteSetupPage();
    includeSetupPage();
  }
 
  private void includeSuiteSetupPage() throws Exception {
    include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
  }
 
  private void includeSetupPage() throws Exception {
    include("SetUp""-setup");
  }
 
  private void includePageContent() throws Exception {
    newPageContent.append(pageData.getContent());
  }
 
  private void includeTeardownPages() throws Exception {
    includeTeardownPage();
    if (isSuite)
      includeSuiteTeardownPage();
  }
 
  private void includeTeardownPage() throws Exception {
    include("TearDown""-teardown");
  }
 
  private void includeSuiteTeardownPage() throws Exception {
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
  }
 
  // ...
cs
 

|| Switch 문


-

- switch 문은 작게 만들기 어렵다 (if/else 포함)

- 본질적으로 switch 문은 N가지를 처리


> 문제의 코드

- 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.

- '한 가지' 작업만 수행하지 않는다.

- SRP(Single-responsibility principle, 단일 책임 원칙) 위반

- OCP(Open–closed principle, 개방-폐쇄 원칙)를 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
  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);
      }
  }
cs


> 해결 코드

- switch 문을 추상 팩토리에 숨겨 보여주지 말자.

- 적절한 Employee 파생 클래스의 인스턴스를 생성

각 switch 문을 저차원 클래스에 숨기고 반복하지 않는 방법, 다형성을 사용하는 방법이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract clas 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 SalariedEmployee(r);
        default:
            throw new InvalidEmployeeType(r.type);
    }
}
cs



|| 서술적인 이름을 사용


-

- "코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다." - 워드 커닝햄

- 길고 서술적인 이름이 짧고 어려운 이름보다 좋고 길고 서술적인 주석보다 좋다.

ex. 함수 이름, SetupTeardownIncluder ..

    private 함수, isTestable, includesetupAndTeardownPages ..

- 일관성있게 이름을 붙이자. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용

ex. includeSetupAndTeardownPages,

    includeSetupPages,

includeSuiteSetupPage,

includeSetupPage ...



|| 함수 인수


-

- 함수에서 이상적인 인수 개수는 0개(무항), 다음이 1개(단항) -> 2개(이항)


> 단항 함수

- 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는

1. 인수에 질문을 던지는 경우. 

2. 인수를 뭔가로 변환해 결과를 반환하는 경우.

- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려주자.

StringBuffer transform(StringBuffer in)


> 플래그 인수

- 플래그 인수는 함수가 한꺼번에 여러 가지 일을 한다고 대놓고 공표하는 셈.


> 이항 항수

- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.

- 이항 함수를 만들어야하는 불가피한 경우가 생길 수 있지만 그만큼 위험이 따른다는 사실을 이해하고

  가능하면 단항 함수 함수로 바꾸도록 하자.


> 인수 객체

- 인수가 2~3개 필요하다면 독자적인 클래스 변수를 선언하는 방법도 좋다.

Circle makeCircle(Point center, double radius);


> 인수 목록

- 인수 개수가 가변적인 함수일 경우 String.format 메서드가 좋은 예

public String format(String format, Object... args)

- 가변 인수를 취하는 모든 함수에 같은 원리가 적용 (단항, 이항, 삼항 함수로 취급)

void monad(Integer... args);

void dyad(String name, Integer... args);

void triad(String name, int count, Integer... args);


> 동사와 키워드

- 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수

- 함수와 인수가 동사/명사 쌍을 이뤄야 한다.

writeField(name) : 'field'인 'name'이 무엇이든 'write' 한다.

- 이름이 키워드를 추가해보자. (함수 이름에 인수 이름을 넣자)

assertExpectedEqualsActual(expected, actual)



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


-

- 함수에서 한 가지를 하겠다고 했지만 남몰래 다른 행동도 하게 될 수 있다.

그럴 경우에는 함수 이름에 분명히 명시해주자. 단, 함수가 '한 가지'만 한다는 규칙을 위반하지만..

checkPassword -> checkPasswordAndInitializeSession


> 출력 인수

- 일반적으로 인수를 함수 입력으로 해석

- 일반적으로 출력 인수는 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하자.

public void appendFooter(StringBuffer report) -> report.appendFooter()



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


-

- 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 수행

> 모호한 의미의 코드

username이 unclebob으로 설정되어 있는지 확인하는 코드인가..?

1
if (set("username""unclebob"))..
cs


> 명령과 조회를 분리한 코드

사실 username을 unclebob으로 설정하는 코드이다.

1
2
3
4
if (attributeExists("username")) {
    setAttribute("username""unclebob");
    ...
}
cs



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


-

- 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 위반

- 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 하는 문제 발생

- 오류 코드 대신 예외(ex. Try/Catch)를 사용하면 오류 처리 코드가 원래 코드에서 분리되어 코드가 깔끔해짐


> Try/Catch 블록 뽑아내기

- try/catch 블록은 코드 구조에 혼란을 일으키고 정상 동작과 오류 처리 동작을 뒤석으므로 별도 함수로 뽑아내는 편이 좋음

- 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워짐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) {
    logger.log(e.getMessage());
}
cs


> 오류 처리도 한 가지 작업이다.    

- 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch/finally 문으로 끝나야 한다.



|| 반복하지 마라!


-



|| 구조적 프로그래밍


- 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.

- 하지만 함수가 아주 커지면 return문은 하나여야 상당한 이익을 제공한다.



|| 함수는 어떻게 짜죠?


-

- 처음에는 길고 복잡하고 중복되고.. 하더라도 작성해보자.

  그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고 순서를 바꿔보자.



Result.

* "master programmer는 시스템을 (구현할) 프로그램이 아닌 (풀어갈) 이야기로 여간다."

> 규칙을 잘 따른 코드 (https://github.com/ludwiggj/CleanCode/blob/master/src/clean/code/chapter03/SetupTeardownIncluder.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class SetupTeardownIncluder {
  private PageData pageData;
  private boolean isSuite;
  private WikiPage testPage;
  private StringBuffer newPageContent;
  private PageCrawler pageCrawler;
 
  public static String render(PageData pageData) throws Exception {
    return render(pageData, false);
  }
 
  public static String render(PageData pageData, boolean isSuite)
    throws Exception {
    return new SetupTeardownIncluder(pageData).render(isSuite);
  }
 
  private SetupTeardownIncluder(PageData pageData) {
    this.pageData = pageData;
    testPage = pageData.getWikiPage();
    pageCrawler = testPage.getPageCrawler();
    newPageContent = new StringBuffer();
  }
 
  private String render(boolean isSuite) throws Exception {
    this.isSuite = isSuite;
    if (isTestPage())
      includeSetupAndTeardownPages();
    return pageData.getHtml();
  }
 
  private boolean isTestPage() throws Exception {
    return pageData.hasAttribute("Test");
  }
 
  private void includeSetupAndTeardownPages() throws Exception {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
  }
  private void includeSetupPages() throws Exception {
    if (isSuite)
      includeSuiteSetupPage();
    includeSetupPage();
  }
 
  private void includeSuiteSetupPage() throws Exception {
    include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
  }
 
  private void includeSetupPage() throws Exception {
    include("SetUp""-setup");
  }
 
  private void includePageContent() throws Exception {
    newPageContent.append(pageData.getContent());
  }
 
  private void includeTeardownPages() throws Exception {
    includeTeardownPage();
    if (isSuite)
      includeSuiteTeardownPage();
  }
 
  private void includeTeardownPage() throws Exception {
    include("TearDown""-teardown");
  }
 
  private void includeSuiteTeardownPage() throws Exception {
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
  }
 
  private void updatePageContent() throws Exception {
    pageData.setContent(newPageContent.toString());
  }
 
  private void include(String pageName, String arg) throws Exception {
    WikiPage inheritedPage = findInheritedPage(pageName);
    if (inheritedPage != null) {
      String pagePathName = getPathNameForPage(inheritedPage);
      buildIncludeDirective(pagePathName, arg);
    }
  }
 
  private WikiPage findInheritedPage(String pageName) throws Exception {
    return PageCrawlerImpl.getInheritedPage(pageName, testPage);
  }
 
  private String getPathNameForPage(WikiPage page) throws Exception {
    WikiPagePath pagePath = pageCrawler.getFullPath(page);
    return PathParser.render(pagePath);
  }
 
  private void buildIncludeDirective(String pagePathName, String arg) {
    newPageContent
      .append("\n!include ")
      .append(arg)
      .append(" .")
      .append(pagePathName)
      .append("\n");
  }
}
cs





출처 : 클린 코드 (Robert C. Martin)




반응형
댓글
최근에 올라온 글
최근에 달린 댓글
링크
Total
Today
Yesterday