본문 바로가기

TDD, CleanCode

클린코더스 강의 리뷰 2. Function

https://github.com/msbaek/fitness-example

 

해당 예제를 이용하였다.

package function;

import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;

public class FitnessExample {
    public String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
        WikiPage wikiPage = pageData.getWikiPage();
        StringBuffer buffer = new StringBuffer();

        if (pageData.hasAttribute("Test")) {
            if (includeSuiteSetup) {
                WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
                if (suiteSetup != null) {
                    WikiPagePath pagePath = wikiPage.getPageCrawler().getFullPath(suiteSetup);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("!include -setup .").append(pagePathName).append("\n");
                }
            }
            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")) {
            WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
            if (teardown != null) {
                WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
                String tearDownPathName = PathParser.render(tearDownPath);
                buffer.append("!include -teardown .").append(tearDownPathName).append("\n");
            }
            if (includeSuiteSetup) {
                WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
                if (suiteTeardown != null) {
                    WikiPagePath pagePath = wikiPage.getPageCrawler().getFullPath(suiteTeardown);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("!include -teardown .").append(pagePathName).append("\n");
                }
            }
        }

        pageData.setContent(buffer.toString());
        return pageData.getHtml();
    }
}

다음 코드를 리팩토링 해보자

 

1. Extract Method Object

인텔리제이에서 extract method object라는 리팩토링 기능을 제공한다.

해당 창은 Double Shift를 하면 뭐든 검색할 수 있는 창이다.

 

ublic class FitnessExample {
    public String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
        return new TestableHtmlBuilder(pageData, includeSuiteSetup).invoke();
    }

    private class TestableHtmlBuilder {
        private PageData pageData;
        private boolean includeSuiteSetup;

        public TestableHtmlBuilder(PageData pageData, boolean includeSuiteSetup) {
            this.pageData = pageData;
            this.includeSuiteSetup = includeSuiteSetup;
        }

        public String invoke() throws Exception {
            WikiPage wikiPage = pageData.getWikiPage();
            StringBuffer buffer = new StringBuffer();

            if (pageData.hasAttribute("Test")) {
                if (includeSuiteSetup) {
                    WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
                    if (suiteSetup != null) {
                    ...중략

이를 통해서 TestableHtmlBuilder라는 클래스를 생성하고 파라미터들은 클래스 변수로 생성자에 의해 초기화 되고 그 결과를 invoke() 메소드를 이용해서 반환하게 되어졌다.

 

2. Field 변수로 추출

 

invoke를 살펴보면 wikiPage와 buffer가 중복되서 사용되는 것을 볼 수 있다.

이를 field로 넘긴다.

역시 dobuleshift이후 검색어 입력으로 값을 넘긴다.

이 후 다음과 같이 수정된다.

 

3. 중복된 로직 제거

해당 코드들은 모두 WikiPagePath ... 이후의 3줄의 코드가 모두 중복된다. 다만 다른점은 3번째 줄의 append하는 String이 teardown과 setup이라는 점이다. 그렇기 때문에 이를 변수로 생성한다.

이 때도 Variable(ctrl + alt + v)를 이용하여 변수를 쉽게 생성할 수 있다.

이제 중복된 메소드를 extract Method하면 다음과 같아진다.

        public String invoke() throws Exception {
            if (pageData.hasAttribute("Test")) {
                String mode = "setup";
                if (includeSuiteSetup) {
                    WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
                    if (suiteSetup != null) {
                        incluePage(mode, suiteSetup);
                    }
                }
                WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
                if (setup != null) {
                    incluePage(mode, setup);
                }
            }

            buffer.append(pageData.getContent());
            if (pageData.hasAttribute("Test")) {
                WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
                String mode = "teardown";
                if (teardown != null) {
                    incluePage(mode, teardown);
                }
                if (includeSuiteSetup) {
                    WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
                    if (suiteTeardown != null) {
                        incluePage(mode, suiteTeardown);
                    }
                }
            }

            pageData.setContent(buffer.toString());
            return pageData.getHtml();
        }

        private void incluePage(String mode, WikiPage suiteSetup) throws Exception {
            WikiPagePath pagePath = wikiPage.getPageCrawler().getFullPath(suiteSetup);
            String pagePathName = PathParser.render(pagePath);
            buffer.append("!include -" + mode + " .").append(pagePathName).append("\n");
        }
    }

이후에도 다음과 같은 중복을 또 발견할 수 있다.

수정 하면 다음과 같다.

다음과 같이 함수로 만든 후에 보니 굳이 변수명을 선언하여 파라미터로 넘겨줄 필요가 없다.

그렇기 때문에 inline(ctrl + alt + n)을 한다.

< inline 한 결과 >

이 후에 첫번째 if문 안의 로직 역시 메소드로 extract하면 다음과 같은 결과를 얻는다.

마지막으로 if문의 중복을 없애고 if문 안의 조건을 extract메소드하자

이러한 식으로 계속 잘게 extract하여 쪼갤 수 있다.

 

4. 리팩토링 어디까지?

과연 하나의 일만 하는 함수는 무엇일까? 여태까지 들었던 예시 중 가장 와닿는 예시였는데 로그인 기능이 있다고 가정하면, 로그인은 아이디를 입력받고 비밀번호를 입력받는 두가지 일을 한다. 하지만 아이디를 입력받는일과 비밀번호를 입력받는 일의 추상화 단계는 로그인을 하는 추상화 단계보다 한 단계 낮다. 그렇기 때문에, 로그인은 낮은 두 단계의 일을 가지는 하나의 일을 한다고 생각될 수 있다.

< 이런 느낌? >