모르지 않다는 것은 아는것과 다르다.

Books

클린코드 3장 (함수)

채마스 2022. 4. 17. 21:31

작게 만들어라

  • 함수를 만드는 첫째 규칙은 작게만드는 것이다.
  • 함수가 작을수록 한눈에 파악하기 쉽기 때문이다.

 

한 가지만 해라

  • 함수는 한 가지를 해야한다.
  • 하지만 그 한 가지를 잘 해야한다.
  • 함수가 여러 기능을 하게된다면 테스트하는것도 복잡해 진다.

 

함수 당 하나의 추상화 수준만을 가져야한다.

  • 함수 내 모든 문장의 추상화 수준이 동일해야한다.
  • 추상화 수준을 맞추지 않으면 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어렵기 때문이다.
  • 내려가기 규칙을 사용하면 추상화 수준을 맞추기 좋다.
    • 내려가기 규칙을 사용하면 코드를 위에서 아래로 이야기 처럼 읽기 쉽게 코드를 작성할 수 있다.
    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

 

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

  • 클린코드 3장

'Books' 카테고리의 다른 글

클린코드 6장 (객체와 자료구조)  (0) 2022.04.23
클린코드 4장 (주석)  (0) 2022.04.23
클린코드 1장, 2장  (0) 2022.04.17
템플릿 콜백 패턴 (토비의 스프링 3장)  (0) 2022.02.28
테스트란 (토비의 스프링 2장)  (0) 2022.02.27