의도를 분명히 표현하는 함수를 어떻게 만들 수 있을까?
함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있을까?
작게 만들어라!
• 함수를 만드는 첫번째 원칙 => 작게!
• 함수를 만드는 두번째 원칙 => 더 작게!
무조건 작은 함수가 좋다.
각 함수가 명백하게 하나의 이야기를 표현해야 한다.
얼마나 짧아야 할까?
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();
}
위의 함수는 설정(setup) 페이지와 해제(teardown) 페이지를 테스트 페이지에 넣은 후 해당 테스트 페이지를 HTML로 렌더링하는 함수다. 이 함수를,
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 testableHtml(PageData pageData, boolean includeSuiteSetup)
throws Exception {
Wikipage wikipage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup = PageCrawlerlmpl.getlnheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage);
if (suiteSetup != null) {
wikiPagePath pagePath =
suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup =
PageCrawlerlmpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown =
pageCrawlerlmpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName)
.append("\n");
}
if (includeSuiteSetup) {
WikiPage suiteTeardown = PageCrawlerlmpl.getlnheritedPage(
SuiteResponder.SUITE_TEARDOWN_NAME,
wikiPage
);
if (suiteTeardown != null) {
Wikipagepath pagePath =
suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
여기서 getHtml()은 추상화 수준이 아주 높으며,
String pagePathName = PathParser.render(pagepath);는 추상화 중간이 중간이고,
.append("\n");와 같은 코드는 추상화 수준이 아주 낮은 것이다.
함수 당 '추상화 수준'은 하나로!
함수가 확실히 '한 가지' 작업만 하려면, 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
한 함수 내에 추상화 수준을 섞으면, 코드를 읽는 사람이 헷갈린다.
위에서 아래로 코드 읽기: 내려가기 규칙
• 코드는 위에서 아래로 이야기처럼 읽혀야 한다.
• 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
• => 내려가기 규칙
이를테면 이런 것이다.
TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고,
해제 페이지를 포함한다.
TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 `SuiteSetUp` 페이지를 찾아 include 문과
페이지 경로를 추가한다.
TO 부모 계층을 검색하려면, ......
• 위에서 아래로 TO 문단을 읽어내려 가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.
Switch 문
TBD
서술적인 이름을 사용하라!
• 코드를 읽으면서 짐작되는 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드다.
• 함수가 작고 단순할 수록 서술적인 이름을 고르기도 쉬워진다.
• 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
• 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
• 이름을 붙일 때는 일관성이 있어야 한다.
• 한 모듈 내에서의 함수 이름은 같은 문구, 명사, 동사를 사용한다.
함수 인수
함수에서 이상적인 인수 개수는?
0개(무항)다.
3개(삼항)이상은 가능한 피하는 편이 좋다.
• 인수는 개념을 이해하기 어렵게 만든다.
• 테스트 관점에서 보면 인수는 더 어렵다. 갖가지 인수들의 조합으로 함수를 검증하는 테스트 코드를 크작성하려면 복잡하기 때문이다.
• 인수가 2~3개 필요하다면, 그 중 일부를 묶어 하나의 클래스 변수로 선언할 수도 있다. 예를 들면,
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
이렇게 x와 y를 묶어 하나의 클래스 변수로 넘기려면 결국 이름을 지어야만 한다. 자연스럽게 개념을 표현할 수 있게 된다.
• 단항 함수는 함수와 인수가 동사/명사 쌍을 이루도록 한다. 예를 들면 write(name) 혹은 writeField(name)과 같은 식이다.
부수 효과를 일으키지 마라!
어떤 함수에서 한 가지 일을 하기로 해놓고, 사실 내부적으로는 뭔가 전혀 다른 일을 처리한다면 매우 혼란스러울 것이다.
이를 '부수 효과(Side Effect)'라고 한다.
다음 함수(checkPassword())는 내부적으로 부수 효과를 일으킨다.
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String usenName, 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();다.
함수 이름만 봐서는 세션을 초기화한다는 사실이 드러나있지 않다. 그래서 함수 이름만 보고 함수를 호출했다가는 의도치 않게 세션 정보를 지워버릴 수도 있는 것이다.
이런 부수효과는 시간적인 결합(Temporal Coupling)을 초래한다. 즉 이 경우에는, 세션을 초기화해도 괜찮은 경우에만 이 함수를 호출할 수 있다.
만약 반드시 시간적인 결합이 필요한 경우라면, 함수 이름에 그것을 분명히 명시해야만 한다.
명령과 조회를 분리하라!
• 함수는 뭔가를 수행하거나, 혹은 뭔가에 답하거나. 둘 중 하나만 해야 한다.
• 즉, 객체 상태를 변환하거나, 아니면 객체 정보를 반환하거나. 둘 중 하나다.
댓글