한 가지만 해라!
함수는 한 가지만을 잘 해야한다. 여기서 함수가 한 가지만 하는지 판단하는 방법은 다음과 같다.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다.
- 단순히 다른 표현이 아니라 의미있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
함수당 추상화 수준은 하나로!
함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 왜냐하면, 한 함수 내에서 추상화 수준을 섞으면 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다. 또한 근본 개념과 세부사항을 뒤섞기 시작하면, 깨진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다. 다음 코드는 함수 내 추상화 수준이 동일하지 않은 코드다.
public class HtmlUtil {
public static String testableHtml(PageData pageData, boolean includeSuiteSetup) {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
// 반복
// 1번째 중복
if (includeSuiteSetup) {
WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(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");
}
}
// 2번째 중복
WikiPage setUp = PageCrawlerImpl.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")) {
// 3번째 중복
WikiPage teardown = PageCrawlerImpl.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");
}
// 4번째 중복
if (includeSuiteSetup) {
WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(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();
}
private static class PathParser {
public static String render(WikiPagePath pagePath) {
return pagePath.pathName;
}
}
}
이 코드를 보면,
- getHtml()은 추상화 수준이 아주 높다.
- PathParser.render(pagePath)는 추상화 수준이 중간이다.
- .append("\n")와 같은 코드는 추상화 수준이 아주 낮다.
따라서 다음 규칙을 적용하여 리팩토링할 수 있어야한다.
위에서 아래로 코드 읽기 : 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽을수록 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 결과적으로 각 함수는 다음 함수를 소개하고, 각 함수는 일정한 추상화 수준을 유지한다. 여기서 추상화 수준을 이해하기 위해서 다음 블로그 링크를 참고하였습니다.
https://ablue-1.tistory.com/81
https://onestone-dev.tistory.com/3
함수 인수
함수에서 이상적인 인수 개수는 0개이다. 차선은 입력 인수가 1개뿐인 경우이다. 코드를 읽는 사람에게 includesSetupPageInto(new PageContent)는 함수 이름과 인수 사이에 추상화 수준이 다르므로, 이 보다는 includesSetupPage()가 이해하기 더 쉽다. 또한 테스트 관점에서도 보면 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 작성한다고 생각하면 끔찍하다.
많이 쓰는 단항 형식
함수에 인수 1개를 넘기는 이유로 하나는 인수에 질문을 던지는 경우가 있고, 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우가 있다.
플래그 인수
이는 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이므로 지양해야 한다.
동사와 키워드
함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수이다. 여기서 단항 함수는 함수와 인수가 동사-명사 쌍을 이루어야한다. write(name)도 좋은 이름일 수 있지만, writeFile(name)이 조금 더 좋은 이름이 될 수 있다. 그러면 이름이 필드라는 사실이 분명히 드러난다. 또한 함수 이름에 인수 이름을 넣는 키워드 방식도 좋다. 예를 들어, Junit의 assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 그러면 인수 순서를 기억할 필요가 없어진다.
출력 인수
일반적으로 출력 인수는 피해야한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
즉, 다음 함수처럼의 사용은 지양하고,
public void appendFooter(StringBuffer report);
다음 함수처럼 변경해야 한다.
report.appendFooter()
이를 바탕으로 위의 함수를 리팩토링 하여 다음 클래스를 만들 수 있다.
package ch3;
public class SetupTeardownIncluder {
private final PageData pageData;
private boolean isSuite;
private final WikiPage testPage;
private final StringBuffer newPageContent;
private PageCrawler pageCrawler;
public SetupTeardownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
public static String render(PageData pageData) {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite) {
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private String render(boolean isSuite) {
this.isSuite = isSuite;
if (isTestPage()) {
includeSetupAndTeardownPages();
}
return pageData.getHtml();
}
private boolean isTestPage() {
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() {
includeSetupPages();
includePageContent();
includeTearDownPages();
updatePageContent();
}
private void includeSetupPages() {
if (isSuite) {
includeSuiteSetupPage();
}
includeSetupPage();
}
private void includeSuiteSetupPage() {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() {
include("SetUp", "-setup");
}
private void includePageContent() {
newPageContent.append(pageData.getContent());
}
private void includeTearDownPages() {
includeTearDownPage();
if (isSuite) {
includeSuiteTearDownPage();
}
}
private void includeTearDownPage() {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void includeSuiteTearDownPage() {
include("TearDown", "-teardown");
}
private void updatePageContent() {
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) {
WikiPage inheritedPage = getInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage getInheritedPage(String pageName) {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage inheritedPage) {
WikiPagePath pagePath = PageCrawler.getFullPath(inheritedPage);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
private static class PathParser {
public static String render(WikiPagePath pagePath) {
return pagePath.pathName;
}
}
}
'책 - 요약 정리 > 클린 코드' 카테고리의 다른 글
객체와 자료 구조 (0) | 2023.09.06 |
---|---|
클린 코드 (0) | 2023.09.05 |
2장. 의미 있는 이름 (0) | 2023.08.31 |