3장 함수
개요
-
아래의 두 코드를 비교해 볼 것
// 리펙터링 전 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 = 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"); } } 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("\n") .append("!include -teardown .") .append(tearDownPathName) .append("\n"); } 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(); }
// 리펙터링 후 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(); }
-
리펙터링한 코드는 코드를 이해하기 쉬워짐
작게 만들어라
- 함수는 작으면 작을수록 좋음 (저자의 경험에 바탕)
- 들여쓰기는 2단을 넘지 말 것
한가지만 해라!
- 개요에서 제시한 리팩터링 전 코드는 한 함수에서 여러가지 역할을 수행함
- 버퍼 생성, 페이지 로드, 페이지 검색, 경로 랜더링 등등….
- 함수는 한가지만 수행하고, 그 한가지만을 잘 해야함
함수 당 추상화 수준은 하나로!
- 함수가 확실히 한가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 함
- 내려가기 규칙
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋음
- 한 함수의 다음에는 추상화 수준이 한 단계 낮은 함수가 옴
- 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아짐
switch 문
- Switch 문은 작게 만들기 어렵고, 한 가지 작업만 하는 switch 문을 만들기도 어려
-
Switch 문을 완전히 피할 수는 없지만, 저차원 클래스에 숨기는 방법이 있음
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); } }
- 위 함수의 문제점
- 함수가 길다. 새 직원 유형이 추가되면 더 길어짐
- 한가지 작업만 수행하지 않음
- SRP(Single Responsibility Principle)를 위반함
- OCP(Open Closed Principle)를 위반함. 새 직원 유형을 추가할 때 마다 코드를 변경하기 때문
- 이 문제를 해결한 코드
- Switch 문은 추상 팩토리에 숨김
- 팩토리는 Switch 문을 사용해 적절한
Empolyee
파생 클래스의 인스턴스를 생성
public abstract class 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);
}
}
}
서술적인 이름을 사용하라!
- 함수가 작고 단순할 수록 서술적인 이름을 고르기 쉬워짐
- 이름이 길더라도 서술적인게 긴 주석보다 좋음
- 서술적인 이름을 사용하면 설계가 뚜렷해지므로 코드를 개선하기 쉬워짐
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사 사용이 좋음
함수 인수
- 함수에서 이상적인 인수 개수는 0 (무항) 임
- 그 다음은 1개, 그 다음은 2개이며, 3항 이상은 피하는게 좋음. 4항은 무슨 이유라도 안됨
- 코드를 읽는 사람에게는
includeSetupPageInto(new PageContent)
보다includeSetupPage()
가 이해하기 더 쉬 - 많이 쓰는 단항 형식
boolean fileExists(“MyFile”)
과 같이 질문을 던지는 형식InputStream fileOpen(“MyFile”)
과 같이 특정 대상에 대한 결과를 받는 형식passwordAttemptFailedNtimes(int attempts)
와 같이 return 없이 이벤트를 처리하는 형식- 위와 같은 형식이 아니면 단항 함수는 가급적 피할 것
- 플래그 인수
- 특히
bool
형식을 받는 함수는 좋지 않음- 함수 안에서 두가지 역할을 한다는 뜻이므로.
- 함수를 각각 나누는 것이 더 좋음
- 특히
- 이항 함수
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어려움.
- 예시로
writeField(name)
는writeField(outputStream, name)
보다 이해하기 쉬움
- 예시로
- 이항 함수가 적절한 경우도 있음
Point p = new Point(0, 0)
와 같은 좌표 입력assertEquals(expected, actual)
과 같은 테스트 검증 함
- 이항 함수가 나쁜 것은 아니지만, 필요한 상황이 아니면 단항으로 바꾸려는 노력을 해봐야 함
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어려움.
- 삼항 함수
- 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 이해하기 어려움
- 삼항 함수 만들때는 신중히 고려할 필요가 있음
- 인수 객체
- 인수가 2~3개 필요하다면 독자적인 클래스 변수로 선언할 수 있을지 검토해 볼 것
부수 효과를 일으키지 말 것
- 한 함수에서 한가지를 하기로 하고 다른 행위를 하는 것은 좋지 않음
- 함수로 넘어온 인수나 전역 변수를 수정하는 행위
- 아래의 코드에서 부수 효과를 찾아보자.
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, 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();
의 호출임checkPassword
함수는 패스워드 체크만 해야 함- 함수 이름만 보고 패스워드 체크를 위해 위 함수를 사용하다가 세션이 초기화 될 수 있
- 출력 인수
- 일반적으로 인수는 입력으로 해석함
appendFooter(s)
는 s를 바닥글로 넣을까? 아니면 s 에 바닥글을 넣어 줄까?public void appendFooter(StringBuffer report)
라는 선언부를 봐야 분명해짐- 일반적으로는 출력 인수는 피해야 함
명령과 조회를 분리하라
- 함수는 뭔가를 수행하거나, 뭔가에 답하거나 둘 중에 하나만 해야 함
public boolean set(String attribute, String value);
이 함수는 attribute의 속성을 찾아 value로 바꾸고 성공 여부를 반환함if (set("username", "unclebob"))...
은 어떻게 해석해야 하는가?username
이unclebob
인지 확인하는 코드?- 또는
username
을unclebob
으로 설정하는 코드? - 이는 set 이 동사인지 형용사인지 분간하기 어려운 탓
- set 을 동사로 의도했지만, if 문에 들어가면 형용사처럼 느껴지게 됨
- 아래와 같이 명확하게 바꿔주는 것이 좋음
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
오류코드보다는 예외를 사용할 것
- 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반함
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
- 반면 예외를 사용하면 오류처리 코드가 분리되어짐
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
반복하지 마라!
- 중복 코드가 늘어나면 해당 부분 수정시 범위가 늘어나게 됨
- 중복 코드는 최대한 제거하려는 노력을 해야 함
- 구조적 프로그래밍
- 에츠허르 데이크스트(Edsger Dijkstra) 의 구조적 프로그래밍 원칙
- 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 함
- 즉, 함수는
return
문이 하나여야 하며,break
나contiune
를 사용하면 안되고,goto
는 절대 사용하지 않음 - 이는 함수가 작을 때 보다 함수가 클 때 상당한 이익을 제공함
- 함수가 작을때는
return
,break
,continue
를 써도 좋음
- 함수를 어떻게 짜죠?
- 처음에는 길고 복잡하더라도 동작하도록 작성함
- 이후 작성된 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 작성함
- 이후 코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거하는 등의 리팩터링을 진행한 뒤 단위 테스트를 통과하도록 함
Leave a comment