작게 만들어라!
함수는 작을수록 좋다.
20줄도 길다.
들여쓰기(indent)는 1단이나 2단을 넘어가면 안 된다.
한 가지만 해라!
함수의 추상화 수준은 하나여야 한다.
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.
서술적인 이름을 사용하라!
코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.
함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
IDE에서 이런저런 이름을 시도한 후 최대한 서술적인 이름을 골라도 좋다.
또한 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
ex)
`includeSetupAndTeardownPages`
`includeSetupPages`
`includeSuiteSetupPage`
`includeSetupPage`
함수 인수
함수에서 이상적인 인수 개수는 0개다.
다음은 1개(단항), 그 다음은 2개(이항)다.
3개(삼항)는 가능한 피하는 편이 좋다.
인수가 없다면 테스트 케이스 작성 시에도 간단하다.
많이 쓰는 단항 형식
함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다.
1. 인수에 질문을 던지는 경우
boolean isExist(String value)
2. 인수를 뭔가로 변환해 결과를 반환하는 경우
int plus(int x, int y)
플래그 인수
플래그 인수는 추하다.
함수로 부울 값을 넘기는 것은 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
이항 함수
좌표나, 두 값을 이용한 메서드는 적절하다.
하지만 가능하면 단항 함수로 바꾸도록 애써야 한다.
삼항 함수
인수가 3개인 함수는 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다.
예를 들어 `assertEquals(message, expected, actual)` 이 함수를 사용할 때
첫 인수가 `expected`라고 예상하기 때문에 주춤하게 된다.
인수 객체
인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
이와 같이 리팩토링 할 수 있다.
before
Circle makeCircle(double x, double y, double radius);
after
Circle makeCircle(Point center, double radius);
동사와 키워드
함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
writeFiled(name);
name이 필드라는 사실이 분명히 드러난다.
순서가 헷갈릴 때는 키워드를 넣는 방법도 있다.
before
assertEquals(expected, actual);
after
assertExpectedEqualsActual(expected, actual);
위와 같이 수정하면 인수 순서를 기억할 필요가 없어진다.
부수 효과를 일으키지 마라!
이는 사용자의 패스워드를 확인하는 함수이다.
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();`는 부수효과를 일으킨다.
이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다.
그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다.
다시 말해, 세션을 초기화해도 괜찮은 경우에만 호출이 가능하다. 자칫 잘못 호출하면 의도하지 않게 세션 정보가 날아간다.
차라리 `checkPasswordAndInitializeSession`이라는 이름이 적절하다.
(하지만 함수가 '한 가지'만 한다는 규칙을 위반한다.)
출력 인수
일반적으로 우리는 인수를 함수 입력으로 해석하기 때문에 출력 인수는 피해야 한다.
`public void appendFooter(StringBuffer report)`
이 함수를 사용할 때 `appendFooter(s)`는 s를 Footer에 추가하라고 읽힌다.
명령과 조회를 분리하라!
함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 혼란을 초래한다.
`public boolean set(String attribute, String value);`
이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공여부를 반환한다.
이를 사용하면 괴랄한 코드가 나온다.
`if(set("name", "hyun")) {}`
이 코드를 처음 보면 name을 hyun으로 설정하라는 건지, 설정되어있는지를 확인하라는건지 모호하다.
조회와 명령을 분리해 아래처럼 수정할 수 있다.
if (attributeExists("name")) {
setAttribute("name", "hyun");
}
오류 코드보다 예외를 사용하라!
오류 코드를 반환하면 중첩 코드를 야기하며, 오류코드를 곧바로 처리해야 하는 문제가 발생한다.
before
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;
}
after
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());
}
이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기도 쉬워진다.
오류 처리도 한 가지 작업이다.
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
오류 처리도 '한 가지' 작업에 속하기 대문에 함수에 키워드 try가 있다면 함수는 try문으로 시작해 catch/finally 문으로 끝나는게 좋다.
Error.java 의존성 자석
public enum Error {
OK
, INVALID
, NO_SUCH
, LOCKED
, OUT_OF_RESOURCES
, WAITING_FOR_EVENT
;
}
이와 같이 오류코드를 정의하면, 다른 클래스에서 Error enum을 import해 사용해야 하므로 결합도가 강해진다.
오류 코드 대신 예외를 사용하면 새로운 오류에 대한 케이스 추가에도 유연하다.
반복하지 마라!
중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.
구조적 프로그래밍
다익스트라의 구조적 프로그래밍의 원칙을 따르자면 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나여야 된다.
즉, 함수는 `return`문이 하나여야 되며, 루프 안에서 `break`나 `continue`를 사용해선 안된다.
`goto`는 절대로, 절대로 사용하지 말자. 함수가 클 경우에만 상당 이익을 제공하므로, 함수를 작게 만든다면 오히려 여러차례 사용하는 것이 함수의 의도를 표현하기 쉬워진다.
구조적 프로그래밍의 목표와 규율에는 공감하지만 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다. 함수가 아주 클 때만 상당한 이익을 제공한다.
그러므로 함수를 작게 만든다면 간혹 `return`, `break`, `continue`를 사용해도 괜찮다.
오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
함수를 어떻게 짜죠?
처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복 루프도 많다.
처음부터 리팩토링하려고 하지 말고 일단 구현하고 점진적으로 리팩토링하자.
이 서투른 코드에도 단위 테스트 케이스는 빠짐없이 통과하도록 하자.
'📝 끄적끄적 > 📖 Clean Code' 카테고리의 다른 글
2. 의미 있는 이름 (0) | 2023.10.18 |
---|---|
1. 깨끗한 코드 (0) | 2023.10.17 |