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();
}
위 코드의 문제점은 다음과 같다
- 추상화 수준이 다양
- 하나의 함수 안에 너무 긴 코드 삽입
- 두 겹 이상 중첩 된 if 문
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로 렌더링 한다는 사실을 짐작할 수는 있다
작게 만들어라
함수를 만드는 첫번 째 규칙은 '작게' 만드는 것이고
두 번째 규칙은 '더 작게' 만드는 것이다
!! 함수는 20줄도 길다 !!
일반적인 함수는 바로 위의 코드보다도 짧아야 한다 !!
public static String renderPageWithSetupsAndTearDowns(PageData pageData, boolean isSuite) throws Exception {
if (isTest(pageData)) {
includeSetupAndTeardownPages(pageData, isSuite);
}
return pageData.getHtml();
}
이정도로 !!
블록과 들여쓰기
다시말해 if 문, else 문, while 문등에 들어가는 블록은 한 줄이어야 한다는 의미이다
바깥을 감싸는 함수 (enclosing function)이 작아질 뿐 만 아니라 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 가독성 역시 좋아진다
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다
위 코드는
1. 페이지가 테스트 페이지인지 판단
2. 맞다면 설정 페이지와 해제 페이즈를 삽입
3. 페이지를 HTML 렌더링
세 단계를 가진다
이 세 가지 단계는 지정된 함수 이름 아래 추상화 수준이 하나이다
우리가 함수를 만드는 이유는 큰 개념(함수 이름)을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다
함수 당 추상화 수준은 하나로
- getHtml() 은 추상화 수준이 매우 높다
- String page pagePathName = PathParser.render(pagepath); 는 추상화 수준이 중간이다
- .append('\n) 은 추상화 수준이 아주 낮다
한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다
특정 표현이 근본 개념인지, 세부사항인지 구분하기 어렵기 때문
위에서 아래로 코드 읽기 : 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다
한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다
즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다 >> 내려가기 규칙
TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지 내용을 포함한다.
TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetUp" 페이지를 찾아
include 문과 페이지 경로를 추가한다.
TO 부모 계층을 검색하려면....
여기서 각 TO 문단은 현재 추상화 수준을 설명하며 이어지는 아래 단계 TO 문단을 참고한다
핵심은 짧으면서도 한 가지만 하는 함수
TO 문단을 읽어내려 가듯 코드를 구현하면 추상화 수준을 일관되게 유지하기 쉬워진다
Switch 문
switch 문은 작게 만들기 어렵다
본질적으로 switch 문은 N가지를 처리한다
>> 각 switch 문을 저차원 클래스에 숨기고 절대 반복하지 않는 방법 (다형성 이용)
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return cacualteSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
직원 유형에 따라 다른 값을 계산해 반환하는 함수
위 함수의 문제점
1. 함수가 길다 : 새 직원 유형을 추가하면 더 길어진다
2. 한 가지 작업만 수행하지 않는다
3. SRP(Single Responsibility Principal) 를 위반한다 : 코드를 변경해야 되는 이유가 다양하다
4. OCP(Open Closed Principal) 을 위반한다 : 새 직원 유형을 추가할 때 마다 코드를 변경해야 하기 때문이다
5. 위 함수와 구조가 동일한 함수가 무한정 존재한다 : isPayday(Employee e, Date date) 등등..
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{
@Override
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)
}
}
}
위 함수는 switch 문을 추상 팩토리에 숨기고 보여주지 않는다
팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다
calculatePay, isPayday, deliverPay 등의 함수는 Employee 인터페이스를 거쳐 호출된다
그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다
서술적인 이름을 사용하라
길고 서술적인 이름이 짧고 어려운 이름보다 좋다
길고 서술적인 이름이 길고 서술적인 주석보다 좋다
함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다
그 다음, 여러 단어를 사용해 그 함수의 기능을 가장 잘 표현하는 이름을 선택한다
includeSetupAndTeardownPages
includeSetupPages
includeSuiteSetupPage
includeSetupPage
등이 좋은 예
문체가 비슷하면 이야기를 순차적으로 풀어가기도 쉬워진다
함수 인수
함수에서 가장 이상적인 인수 개수는 0개
다음은 1개, 다음은 2개
3항 이상은 피하는 편이 좋다
4개 이상은 특별한 이유가 필요
단항
함수에 인수 1개를 넘기는 가장 흔한 경우
1. 인수에게 질문을 던지는 경우
boolean fileExists("fileName")
파일이 존재하는지 여부를 반환
2. 인수를 뭔가로 변환해 결과를 반환하는 경우
InputStream fileOpen("fileName")
String 형의 파일 이름을 InputStream 으로 변환
드물게 사용하지만 아주 유용한 단항 형식이 바로 이벤트
이벤트 함수는 입력 인수만 있고 출력 인수는 없다
passwordAttemptFailedNtimes(int attempts)
이벤트 함수의 좋은 예
위의 경우가 아니라면 단항 함수는 가급적 피하는게 좋다
예를 들어
StringBuffer transform(StringBuffer in)
이 코드가
void transform(StringBuffer out)
보다 좋다
StringBuffer transform(StringBuffer in)
이 함수가 입력 인수를 그대로 돌려주는 함수라 할지라도 변환함수 형식을 따르는 편이 좋다
적어도 변환 형태는 유지되기 때문
플래그 인수
플래그 인수는 사용하지 않는 것이 좋다
함수는 한 가지 동작을 해야하지만, 플래그 인수를 사용하게 되면 참일 땐 이것, 거짓일 땐 저것 등 여러 동작을 포함하기 때문이다
render(true) // render(boolean isSuite)
라는 코드 보다는
renderForSuite() 와 renderForSingleTest()
두 개의 함수로 나누는 것이 적절하다
이항
인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다
writeField(name) 은 writeField(outputStream, name) 보다 이해하기 쉽다
writeField 메서드를 ouputstream 클래스의 구성원으로 만들어
outputStream.writeField(name) 형태로 호출하는 것이 적절
삼항
인수가 3개인 함수는 2개인 함수보다 더 이해하기 어렵다
assertEquals(message, expected, actual)
이 함수는 한 번에 이해하기 어렵다
반면
assertEquals(1.0, amount, .001)
이 함수는 삼항이 충분히 납득 가는 함수
** assertEquals 함수는 Junit4 에서 사용되는, 두 객체의 값이 같은지 여부를 판단하는 함수
assertEquals(message, expected, actual)
actual 의 값이 expected 의 값과 동일한지 비교하고
테스트가 실패할 시, message 를 출력한다
인수 객체
인수가 2개, 3개가 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 고려해보자
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
아래줄처럼 x, y 좌표를 center 하나의 객체로 묶어 넘기려면 객체의 이름 (center) 이 필요하므로 결국은 개념을 표현하게 되므로 보다 직관적
인수 목록
때로는 인수의 개수가 가변적인 함수도 필요하다
String.format("%s worked %.2f hours", name, hours);
위 예제처럼 가변 인수 전부를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다
>> String.format 은 사실상 이항함수
public String format(String format, Ojbect... args)
선언부 살펴보면 이항함수인 것을 확인 가능
하지만 3개를 넘어가는 인수를 사용하는 경우는 자제하자
동시와 키워드
함수의 의도나 인수의 순서, 의도를 제대로 표현하려면 좋은 함수 이름이 필수다
단항함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다
write(name)
같은 경우 '이름' 을 '쓰다' 라는 의미가 명확하다
더 정확하게는
writeField(name)
이렇게 표현한다면
name 이 filed 라는 사실이 분명히 드러난다
함수 이름에 인수 이름을 넣는 방법을 활용할 수도 있다
assertEquals 보다
assertExpectedEqualsActual(expected, actual);
이렇게 표현하는 것이 좋다
인수의 순서를 기억할 필요가 없기 때문
부수 효과를 일으키지 마라
부수효과 >> 거짓말
함수에서 한 가지를 하겠다고 약속해놓고선 몰래 다른짓도 하니까!!
많은 경우 시간적인 결함 (temporal coupling) 이나 순서 중복성 (order dependency) 를 초래한다
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;
}
}
표준 알고리즘을 사용해 userName 과 password 를 확인한다
두 인수가 올바르면 true 를 반환하고 아니면 false 반환
But 함수는 부수 효과를 일으킨다 >> Session.initialize() 호출 !!
checkPassword 함수는 이름 그대로 암호를 확인하는 함수
이름만 봐서는 세션을 초기화 하는 사실을 알 수 없다
함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처할 수 있다
이런 부수 효과가 시간적인 결합을 초래하게 된다
즉, checkPassword 함수는 특정 상황, 세션을 초기화 해도 괜찮은 경우에만 호출이 가능
시간적인 결합이 필요하다면 함수 이름에 이를 분명하게 명시해야 한다
따라서 checkPasswordAndInitializeSession 이라는 이름이 위의 함수에 더 적합하다
출력인수
일반적으로 우리는 인수를 함수 입력으로 해석한다
appendFooter(s);
위 같은 함수를 보았을 때 s를 footer로 첨부한다는 뜻일까? 아니면 footer를 s에 첨부한다는 뜻일까?
의미가 모호하다
public void appendFooter(StringBuffer report);
함수 선언부를 찾아봐야지만 인수 s가 출력 인수라고 알 수 있다
** 입력 인수 : 일반적인 의미의 인수, 함수에게 전달되는 인수
** 출력 인수 : 함수에서 결과를 돌려받는 인수
void sum(int *a){
a = a + 1;
}
포인터 형태로 매개변수를 전달받고 값을 변경
a 가 참조하는 값이 sum 함수의 동작 결과로 반영된다
void sum(VO vo){
vo.setValue = vo.getValue + 1;
}
Java 에서는 위와 같이 객체도 출력 인수로 활용 가능
객체지향 개념에서 출력 인수로 사용하라고 설계한 변수 : this
report.appendFooter()
이렇게 호출하는 방식이 적합하다
일반적으로 출력 인수는 피하고, 함수에서 굳이 상태 변경을 해야한다면 함수가 속한 객체 상태를 변경하는 방식을 택하는 것이 맞다
명령과 조회를 분리하라
함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다
객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나만, 둘 다 하면 혼란을 초래하게 된다
pulbic boolean set(String attribute, String value);
이 함수는 이름이 attribute 인 속성값을 찾아 값을 value 로 설정한 후 성공하면 true, 실패하면 false 를 반환한ㄴ 함수
if (set("username", "unclebob"))
따라서 위와 같은 괴상한 코드가 생긴다
함수를 모르는 사람은 위 코드를 보고 username 이 unclebob 으로 설정되있는지 확인하는 코드인가?
username 을 unclebob 으로 설정하는 코드인가?
의미가 모호할 수 있다
set이라는 단어가 동사인지 형용사인지 구분하기 어렵기 때문이다
위 함수를 구현한 개발자의 의도는 동사 set이다
하지만 우리들은 if 문을 읽을 때
"username 이 unclebob 으로 설정되어 있다면..."
이라고 해석하기 쉽다
코드 작성자의 의도와 다르게 파악하는 것이다
set 함수를 setAndCheckIfExists 라고 바꿀 수도 있지만
if(attributeExists("username")){
setAttribute("username", "unclebob");
...
}
위와 같이 check 와 set 을 분리해 혼란을 방지하는 방법이 가장 좋다
오류 코드보다 예외를 사용하라
명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다
자칫 if 문에서 명령을 표현식으로 사용하기 쉽기 때문이다
동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다
if (deletePage(page) == E_OK)
오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제를 마주한다
"오류 코드가 E_OK 일때 ...."
의 동작을 직접 구현해야 하기 때문이다
if(deletePage(page) == E_OK){
if(registry.deletedReference(page.name) == E_OK){
if(configKeys.deletedKey(page.name.makeKey())==E_OK){
logger.log("page deleted");
}
else{
logger.log("configKey not deleted");
}
}
else{
logger.log("deletedReference from registry failed");
}
}
else{
logger.log("delete failed");
return E_ERROR;
}
if 문의 중첩을 통해 E_OK 인지, 아닌지의 경우의 동작을 하나하나 구현해야 한다
반면에
try{
deletePage(page);
registry.deletedReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch(Exception e){
logger.log(e.getMessage());
}
try catch 문을 사용한다면 깔끔하게 구현할 수 있다
Try / Catch 블록 뽑아내기
try catch 블록은 별도 함수로 뽑아내는 편이 좋다
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());
}
위 함수에서 delete 함수는 모든 오류를 처리한다
그래서 코드를 이해하기 쉽다
실제로 페이지를 제거하는 함수는 deletePageAndAllReferences 이다
이 함수는 예외를 처리하지 않고 오로지 페이지를 제거하는 동작 하나만 수행한다
이렇게 정상 동작과 오류 처리 동작을 따로 함수로 분리하여 구현하면 코드를 이해하고 수정하기 쉬워진다
오류 처리도 한 가지 작업이다
함수는 한 가지 작업만 해야 한다
오류 처리도 마찬가지
함수에 try 키워드가 있다면 함수는 try 문으로 시작해 catch / finally 문으로 끝나야 한다
의존성 자석
public enum Error{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_REFERENCE,
WATING_FOR_EVENT;
}
위와 같은 코드는 의존성 자석이다
다른 클래스에서 import 해서 사용해야 한다
즉, Error enum 이 변하면 Error enum 을 사용하는 클래스 전부 다시 컴파일하고 배치해야 한다
>> Error enum 클래스는 수정이 어려워진다
이 과정이 복잡하므로 새 오류 코드를 추가하는 대신 기존 오류 코드를 재사용하게 된다
이런 오류코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되고, 재컴파일 / 재배치 없이도 새ㅐ 예외 클래스를 추가할 수 있다
반복하지 마라
중복을 제거하라
중복이 일어나면 코드 길이, 변경사항, 오류에 대한 대처 등 신경 써야 할 부분이 기하급수적으로 늘어난다
AOP (Aspect Oriented Programming), COP (Component Oriented Programming) 등 중복을 위한 전략 존재
>> Spring 프레임워크에서는 AOP로 Exception Handling 을 global하게 관리할 수 있다
구조적 프로그래밍
다익스트라
" 모든 함수와 함수 내 모든 블록에서 입구와 출구는 하나만 존재해야 한다 "
즉, 함수는 return 문이 하나여야 한다
루프 안에서 break 나 continue 를 사용해서는 안되며 goto 는 절대로 사용하면 안된다
>> 함수를 작게 만든다면 간혹 return, break, continue 를 여러차례 사용해도 된다
때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워지기도 한다
But goto문은 큰 함수에만 의미가 있으므로 그냥 쓰지 말자
함수를 어떻게 짜죠?
함수를 짜는 행위는 글짓기와 비슷하다
서투르고 어수선한 초안을 끊임없이 다듬고 정리해야 한다
우선 함수를 만들고 그 함수를 빠짐없이 테스트하는 단위 테스트케이스를 만든다
코드를 다듬고, 함수를 만들고, 이름을 변경하고, 중복을 제거하고 메서드를 줄이고 순서를 변경한다
이 와중에도 코드는 단위테스트를 항상 통과해야 한다
처음부터 모든것이 완벽한 함수를 짤 수는 없다
점진적으로 개선해 나아가자