작게 만들어라
- 함수를 만드는 첫째 규칙은 작게만드는 것이다.
- 함수가 작을수록 한눈에 파악하기 쉽기 때문이다.
한 가지만 해라
- 함수는 한 가지를 해야한다.
- 하지만 그 한 가지를 잘 해야한다.
- 함수가 여러 기능을 하게된다면 테스트하는것도 복잡해 진다.
함수 당 하나의 추상화 수준만을 가져야한다.
- 함수 내 모든 문장의 추상화 수준이 동일해야한다.
- 추상화 수준을 맞추지 않으면 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어렵기 때문이다.
- 내려가기 규칙을 사용하면 추상화 수준을 맞추기 좋다.
- 내려가기 규칙을 사용하면 코드를 위에서 아래로 이야기 처럼 읽기 쉽게 코드를 작성할 수 있다.
- 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
Switch 문
- switch 문은 작게 만드는 것이 어렵다.
- 본질적으로 switch 문은 N가지를 처리하기 때문이다.
- 불행하게도 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 를 위반한다. -> 코드를 변경할 이유가 여럿이기 때문이다.
- 네째, OCP를 위반한다. -> 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다.
- 가장 큰 문제는 위 함수와 구조가 동일한 함수가 무한정 존재한다는 사실이다.
- 예를들어 isPay(Employee e, Date date), deliverPay(Employee e, Money pay) 와 같은 메서들이 존재할 수 있고 이 메서들 또한 위에서 언급한 문제들을 가지고 있는 유해한 메서드이다.
- 위의 문제를 해결하기 위해서 switch 문을 추상 팩토리에 숨길 수 있다.
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay();
}
public interface EmployeeFactory {
public Employee makeEmployee(Employee r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImple implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return commissionedEmployee(r);
case HOURLY:
return HourlyEmployee(r);
case SALARIED:
return salariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
- 이렇게 switch 문을 상속관계로 숨겨서 사용하면, 적어도 1번만 사용하고 반복해서 사용할 수 있다.
- 팩토리는 switch 문을 사용해서 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
- calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐서 호출된다.
- 이렇게 상속관계로 숨겨서 절대로 다른 코드에 노출하지 않는다.
서술적인 이름을 사용하라
- 함수 이름은 서술적으로 작성하는 것이 좋다.
- 예를 들어 testOrder() 라는 메소드 보다는 hasAvailableProduct() 라는 메서드가 더 서술적이다.
- 이름이 길어도 괜찮다. 겁먹을 필요없다.
함수 인수
단항 함수 (함수 인수 1개)
- 단항 함수가 좋은 경우
- 질문을 던지는 경우 : boolean fileExists("MyFile")
- 인자를 변환해 리턴하는 경우 : InputStream fileOpen("MyFile")
- 위의 경우가 아니라면 가급적이면 단항 함수를 쓰지 않는 것이 좋다.
- 예를들어, render(true) 와 같이 boolean 타입을 함수의 인자로 사용하는 것은 좋지 않다.
- 그 이유는 대놓고 2가지 일을 처리하기 때문이다.
- 이런경우는 true, false 로직을 적합한 함수로 추출하는 것이 좋다.
이항 함수 (함수 인수 2개)
- 이항 함수는 단항 함수보다 이해하기 어렵다.
- 그렇기 때문에 가능하면 단항 함수로 변경시키는 것이 좋다.
- 이항 함수가 좋은 경우
- Point p = new Point(0, 0) 처럼 의미가 명확한 경우도 있다.
삼항 함수 또는 그 이상의 함수
- 신중히 고려해야 한다.
- 꼭 이렇게 많은 인수를 두는 명백한 이유가 있는지 생각해 봐야한다.
동사와 키워드
- 함수의 의도나 인수의 순서와 의도를 제대로 표현하기 위해서는 좋은 함수 이름은 필수이다.
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
- 예를들어, write(name) 보다는 writeField(name) 이 좀더 나은 이름이다.
- 그 이유는 name 이 필드라는 사실이 분명히 드러나기 때문이다.
- 함수 이름에 키워드를 추가하는 것도 함수명에 의도를 포함시키기 좋다.
- 예를들어, assertEquls 보다 assertExpectedEqualsActual 가 좀더 나은 이름이다.
- 그 이유는 인수의 순서를 기억할 필요가 없기 때문이다.
부수 효과를 일으키지 마라
- 부수 효과는 거짓말이다.
- 함수에서는 한 가지일만 해야하는데 다른일을 하는 것이기 때문이다.
- 이런 경우 예상치 못하게 클래스 변수를 수정한다던가 시스템 전역변수를 수정하는 등의 문제를 초래할 수 있다.
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String id, String passwd) {
User user = UserGateway.findById(id);
if (user != null) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codePhrase, passwd);
if ("Valid Passwd".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
- 위의 함수를 보면 비밀번호를 체크해주는 함수 같다.
- 함수 내부에는 Session.initialize(); 와 같은 부수효과가 있다.
- 위와 같은 경우, 잘못하면 의도하지 않게 세션 정보가 날라갈 수 있다.
- 이런 경우는 checkPasswordAndInitializeSession 이라는 함수명이 훨씬 좋다.
명령과 조회를 분리하라
- 함수는 뭔가를 수행하거나 뭔가에 대답하거나 둘 중 하나만 해야한다.
- 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나이다.
public boolean set(String attribute, String value);
if (set("username", "effortguy")) {
// ...
}
- 위의 함수는 set 해서 성공하면 true 를 반환하는 함수이다.
- 이렇게 되면 set 이라는 함수의 의미를 파악하기 어렵다.
if (existsAttribute("username")) {
setAttribute("username", "effortguy");
}
- 위와 같이 명령과 조회를 분리하는것이 확실히 보기 좋다.
오류코드보다 예외를 사용하라
- if (deletePage(page) == E_OK) 와 같은 방식은 '명령과 조회를 분리' 하는 규칙을 위반한다.
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/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());
}
REFERENCES