함수는 작게 만들어라.
함수는 한 가지만 해라
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if( isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
- 페이지가 테스트 페이지인지 판단한다.
- 조건문을 통해 설정 페이지와 해제 페이지를 넣는다.
- 페이지를 HTML로 렌더링 한다.
함수는 한 가지 해야 한다고 했는데 위의 함수는 3가지 기능을 하고 있다.
함수당 추상화 수준은 하나로
추상화 수준
이란? 해당 코드를 읽으면서 파악할 수 있는 정보의 수준을 말합니다. 즉, 해당 코드로 더 자세한 정보를 알 수 있으면 추상화 더 낮아진다고 할 수 있습니다.
높은 추상화 수준
:getHtml()
이라는 함수는 HTML을 가져오는 것은 알겠는데, 어떤 것이랑 연관되어 있는지 알 수 없기 때문에 추상화 수준이 높다고 할 수 있습니다.
보통 추상화 수준
:String pagePathName = PathParser.render(pagepath);
는 PathParser객체의 render 함수를 이용해 pagePathName을 가져올 수 있다는 정보를 유추할 수 있기 때문에 추상화 수준이 보통이라고 할 수 있습니다.
낮은 추상화 수준
:.append("\n")
는 바로 어떤 의미인지 유추 가능하기 때문에 추상화 수준이 낮다고 할 수 있습니다.
한 함수 내에 여러 추상화 수준을 섞으면 특정 표현이 높은 추상화 수준인지 낮은 추상화 수준인지 구분하기 어렵기 때문에 코드를 읽는 사람이 헷갈립니다.
Ex)
// 상위 추상화 수준의 메서드
public void processOrder(Order order) {
validateOrder(order);
calculateTotal(order);
processPayment(order);
updateInventory(order);
sendConfirmation(order);
}
// 하위 추상화 수준의 메서드
private void processPayment(Order order) {
// 결제 처리 로직
// ...
}
// 하위 추상화 수준의 메서드
private void updateInventory(Order order) {
// 재고 업데이트 로직
// ...
}
// 하위 추상화 수준의 메서드
private void sendConfirmation(Order order) {
// 주문 확인 이메일 전송 로직
// ...
}
이렇게 추상화 수준이 하나라면 한가지 기능을 수행하는 함수인 것이다.
내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
그 말 인즉슨 위에서 한 이야기를 아래에서 설명해야 하는 구조여야 한다는 뜻이다.
상위 추상화 수준의 메서드(위에서 한 얘기) / 하위 추상화 수준의 메서드(아래에서 한 얘기)
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);
}
}
위의 코드에는 문제점이 4가지 있다.
- 함수의 길이가 길다.
- 한 가지 작업만 수행하지 않는다. (추상화 단계가 한 가지가 아니다)
- calculateCommissionedPay(e); 과 throw new InvalidEmployeeType(e.type);의 추상화 단계가 한 단계가 아니기 때문이다.
- SRP를 위반한다.
- calculatePay메소드는 3가지 책임을 가지고 있다. Employee의 유형에 따라 다른 계산 메소드를 호출하고, 급여를 계산하고, 예외를 처리하는 역할을 담당하고 있습니다.
- OCP를 위반한다.
- 만약 새로운 Employee 유형이 추가되면 calculatePay메소드를 수정해야 합니다. OCP를 따르기 위해서는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 하므로 위반하고 있습니다.
서술적인 이름을 사용해라
testableHTML → setupTeardownIncluder.render
길고 서술적이어도 되니까 함수 기능을 잘 표현하는 이름을 선택해라.
함수 인수
인수가 어려운 이유는 인수를 넘길 때마다 의미를 해석해야 하기 때문이다.
테스트 케이스의 경우 가능한 모든 경우를 다 테스트 해봐야 하기때문에 상당히 귀찮아진다…
단항 형식
가장 흔히 쓰이는 두가지 경우가 있다.
인수에 질문을 던지는 경우
public boolean fileExists("myFile")
인수를 변환해 결과를 반환하는 경우
InputStream fileOpen("MyFile")
플래그 인수
플래그 인수는 추하다. 함수로 boolean값을 넘기는 관례는 정말 끔직하다.
함수가 한꺼번에 여러가지를 한다고 대놓고 공표하는 셈이니까.
이항 함수와 삼항함수는 최대한 지양하되, 어쩔 수 없는 경우에는 사용한다.
이때 인수를 클래스 변수로 만들 수 있다면 최대한 만드는 것을 지향한다.
부수 효과를 일으키지 마라
부수효과란 ? 메서드나 함수가 외부 상태를 변경하는 경우
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword (String userName, String password) {
User user = userGateway.findbyName(userName);
if (user != user.NULL) {
~~~~
Session.initilalize();
return true;
}
}
}
위의 코드는 이름에서 부터 알 수 있듯이 User의 유효성을 검사하는 클래스이다.
checkPassword라는 메소드를 통해 true, false값으로 유효성을 검사하는듯 보이지만
Session.initialize()라는 매우 무서운 메소드가 숨어있다.
사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다.
명령과 조회를 분리하라
public boolean set(String attribute, String value);
이 함수는 attribute인 속성을 찾아 값을 value로 설정한 후, 성공 여부를 반환하는 메소드이다.
if (set("username", "unclebob"))
위의 set이라는 코드의 기능을 자세히 아는 사람들은 username을 unclebob으로 변경하고 성공여부에 따라 조건문이 작동하는 것을 알지만 처음 보는 사람들은 쉽게 알 수 없다.
그러므로 명령과 조회를 분리해야 한다.
if(attributeExists("username")) { // 조회
setAttribute("username", "unclebob"); // 명
~~
}
예외를 사용해라(try/catch)
예외 코드를 사용할 때 예외 코드만 분담하는 함수를 만드는 것이 바람직하다.
(오류 처리도 ‘한 가지’ 작업에 속하기 때문에 오류만 전담하는 함수를 만들어야 한다)
public void deletePage(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());
~~~~
}
deletePage함수는 모든 예외를 처리합니다.
deletePageAndAllReferences 메소드 에서는 deletePage 함수를 이용해서 페이지를 지웁니다.
이때 발생하는 모든 에러는 deletePage에서 처리해줍니다.
실제로 페이지를 지우는 함수인 deletePageAndAllReferences에서는 예외 처리를 하지 않는다.
반복하지 마라
반복을 하면 코드의 길이가 늘어날 뿐더러, 변동 사항이 생기면 중복된 코드를 전부 고쳐야 돼서 시간이 많이 걸린다.
구조적 프로그래밍
데이크스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다.
즉 함수에는 return 문이 하나여야 한다. 루프 안에서 break이나, continue를 사용하면 안 되며, goto는 절대로 안된다.
그러나 어쩔 수 없는 경우에는 사용해도 된다.