작게 만들어라
- 함수는 작을수록 좋다
- if문, else문, while문 등에 들어가는 블록은 한 줄이어야 한다
- 중첩 구조가 생길만큼 함수가 커져서는 안된다
- 들여쓰기는 1단이나 2단을 넘어서면 안됨
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();
}
to
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
한 가지만 해라
- 위의 첫번째 함수를 보면 여러가지 역할을 한다
- 하지만 2번째는 한 가지만 처리한다
- 설정 페이지와 헤제 페이지를 테스트 페이지에 넣는다
여기서 한 가지가 무엇일까?
- 위의 두 번째 함수를 보면 세 가지를 한다고 주장할 수 있다
- 페이지가 테스트 페이지인지 판단한다
- 그렇다면 설정 페이지와 헤제 페이지를 넣는다
- 페이지를 HTML로 렌더링 한다
- 이 때 해당 함수는 한 가지만 할까? 아니면 세 가지를 할까?
- 위에서 언급하는 세 단계는 지정된 함수 이름 아래에서 추상화 수준이 하나다
- 함수는 간단한 TO문단으로 기술할 수 있다
- TO RenderPageWithSetupsAndTeardowns,
- 페이지가 테스트 페이지인지 확인한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다
- 테스트 페이지든 아니든 페이지를 HTML로 렌더링 한다
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다
- 함수를 만드는 이유는 큰 개념을, 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위한 것
함수 내 색션
- 한 함수에서 여러 섹션으로 나눈다면 여러 작업을 한다는 증거
- 의미 있는 다른 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 역항르 하는 것
함수 당 추상화 수준은 하나로
- 함수가 확실히 한 가지 일을 하려면 함수 내 모든 문자으이 추상화 수준이 동일해야 한다
getHtml() // 추상화 수준 높음
String pagePathName = PathParser.render(pagePath); // 추상화 수준이 중간
.append() // 추상화 수준 낮음
- 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다
- 특정 표현이 근본 개념인지, 세부사항인지 구별하기 어려운 탓
내려가기 규칙
- 함수는 위에서 아래로 이야기처럼 읽혀야 좋다
- 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다
- 위 에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다
- 이것을 내려가기 규칙
- TO 문단을 읽듯이 프로그램이 읽혀야 함
- TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다
- TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다
- TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetUp" 페이지를 찾아 include 문과 페이지 경로를 추가한다
- TO 부모 계층을 검색하려면 ,,,,,
- TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다
- 짧으면서도 한 가지만 하는 함수가 핵심
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);
}
}
- switch문은 작게 만들기 어렵지만,
- 다형성을 이용하여 switch문을 abstract factory에 숨겨
- 다형적 객체를 생성하는 코드 안에서만 switch를 사용하도록 한다.
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 SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
서술적인 이름을 사용하라
- testableHtml보다는 SetupTeardownIncluder.render 가 함수가 하는 일을 좀 더 잘 표현하므로 훨씬 좋은 이름
- isTestable, includesetupAndTeardownPages 등등
- 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드
- 이를 위해서는 길더라도 서술적인 이름이 좋다
- 길고 서술적인 이름이 길고 서술적인 주석보다 좋음
- 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택
- 이름을 붙일 때는 일관성이 있어야 한다
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다
incldueSetupAndTeardownPagrd(){
includeSetupPages();
includeSuiteSetupPage();
includeSetupPage();
}
함수 인수
- 함수에서 이상적인 인수는 0개, 다음은 1개, 다음은 2개 ....
- 3개는 가능한 피하는 편이 좋고, 4개 이상은 특별한 이유가 필요하다,
- 사실 특별한 이유가 있어도 사용하면 안됨
- 인수는 개념을 이해하기 어렵게 만든다
- 출력 인수는 입력 인수보다 이해하기 어렵다
- 함수에서 입력 인수로 결과를 받으리라 생각하지 않음
- 입력 인수를 변환하는 함수라도, 결과는 반환값으로 돌려줘라
많이 쓰는 단항 형식
- 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지
- 인수에 질문을 던지는 경우
boolean fileExists("MyFile")
- 인수로 뭔가를 변환해 결과를 반환하는 경우
InputStream fileOpen("MyFile")
- 인수에 질문을 던지는 경우
플래그 인수
- 플래그 인수는 함수가 여러 가지를 처리한다는 뜻
- 쓰지 말자
이항 함수
- 인수가 하나인 함수보다 이해하기 어렵다
- 불가피하지 않다면 사용하지 않거나, 가능하다면 단항 함수로 바꾸도록 애써야 한다
- assertEquals(expected, actual)를 보면 expected에 actual을 넣는 실수가 얼마나 많나
- ㅋㅋㅋㅋ
삼항 함수
- 삼항 함수를 만들 때는 신중히 고려해라
인수 객체
- 여러 인수를 넘기면 객체를 생성해 인수를 줄여라
- 이름을 통해 묶어 개념을 표현할 수 있다
동사와 키워드
- 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
write(name)
//보다
writeField(name)
//이 좋다
- 함수 이름에 키워드를 추가하는 형식
asertEquals
->
assertExpectedEqualsActual
부수효과를 일으키지 마라
- 함수에서 한 가지를 하겠다고 약속하고선 클래스 변수를 수정하는 등 다른 역할을 한다면 부작용이 생길 수 있다
- 인수를 변경하거나 시스템 전역 변수를 수정하는 등등
- 시간적인 결합이나 순서 종속성을 초래할 수 있다
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;
}
}
- 위 코드를 보면 메소드 이름은 checkPassword 인데 세션을 초기화하고 있다
- 그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다
- 이렇게 특정 상황에서만 호출이 가능해지는 것이 시간적 결합이라 한다
출력 인수
- 일반적으로 인수를 함수 입력으로 해석한다
appendFooter(s);
public void appendFooter(StringBuffer report);
report.appendFooter();
함수 선언부를 찾아봐야만 appendFooter가 인수를 출력으로 사용한다는 사실을 알 수 있다
객체지향프로그래밍이 나오기 전에는 출력 인수가 불가피한 경우도 있었지만, 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다
출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문
appendFooter -> report.appendFooter()
함수에서 상태를 변경한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다
명령과 조회를 분리하라
- 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다
- 객체 상태를 변경하거나, 객체 정보를 반환하거나
- 둘 다 하면 혼란을 초래한다
public boolean set(String attribute, String value);
// 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true 반환, 실패하면 false 반환
if (set("name", "sim"))...
// 이상하다
- 한 번에 여러가지를 하니 if(set()) 같은 이상한 코드가 나온다
- set이 변경하는건지 확인하는건지, 동사인지 형용사인지 구분이 안간다
- 명령과 조회를 분리해야 한다
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;
}
- 여기서 에러코드가 아닌 예외를 사용하면 코드가 깔끔해진다
public void delete(Page page) {
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logError(e);
}
}
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 함수는 모든 오류를 처리한다
- 나머지 함수는 예외를 처리하지 않는다
- 이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다
오류 처리도 한 가지 작업이다
- 함수는 한 가지 작업만 해야하고 오류 처리도 한 가지 작업이다
- 따라서 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch/finally로 끝나야 한다
반복하지 마라
- 조금씩 달라지더라도, 중복이 많아지면 코드 길이가 늘어날 뿐만 아니라 오류가 발생할 확률도 높아진다
- 상속, AOP, COP 모두 중복을 없애기 위해 나왔다 중복을 없애라
구조적 프로그래밍
- 함수 내 모든 블록에 입구와 출구가 하나만 존재
- break나 continue를 사용해서는 안되며 goto는 절대절대 안됨
- 함수가 작다면 별다른 이익이 없겠지만, 함수가 아주 크다면 상당한 이익을 제공한다
함수를 어떻게 짜죠?
- 처음에는 적당히 짠다, 길고 복잡하고, 중복도 있고, 인수 목록도 길고
- 하지만 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만들어야 한다
- 그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다
- 메소드를 줄이고, 순서를 바꾸고 클래스를 쪼개기도 한다
- 항상 단위테스트는 통과해야 한다
'CS > 클린코드' 카테고리의 다른 글
[클린코드] chapter-6 객체와 자료구조 (0) | 2022.08.28 |
---|---|
[클린코드] chapter-5 형식 맞추기 (0) | 2022.08.25 |
[클린코드] chapter-4 주석 (0) | 2022.08.23 |
[클린코드] chapter-2 의미있는 이름 (0) | 2022.08.19 |
[클린코드] Chapter-1 (0) | 2022.08.17 |