티스토리 뷰
| 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 |
'Books' 카테고리의 다른 글
[클린 코드: Clean Code] 5. 형식 맞추기(Formatting) (0) | 2021.01.11 |
---|---|
[클린 코드: Clean Code] 4. 주석(comment) (0) | 2021.01.11 |
[클린 코드: Clean Code] 2. 의미 있는 이름(Meaningful name) (0) | 2021.01.10 |
[JPA] 엔티티 비교, 프록시, 성능 최적화 (4) | 2020.12.29 |
[JPA] 엔티티 그래프 (Entity Graph) (0) | 2020.12.29 |