CleanCode 3장. 함수

Jini.Dev·2022년 6월 1일
0

3장. 함수

프로그램 기본 단위

너무 긴 코드, 중복된 코드, 괴상한 문자열, 낯설고 모호한 자료 유형, 다양한 추상화 수준의 코드로 이루어진 함수는 이해하기 어렵다.
읽기 쉽고 이해하기 쉬운 함수는 어떻게 작성해야하는가?



작게 만들어라!

되도록 한 함수당 3~5줄 이내를 권장한다

블록과 들여쓰기

if/else, while문 등에 들어가는 블록은 한 줄이어야 한다.
블록 안에서 호출하는 함수 이름을 잘지으면 코드를 이해하기도 쉬워진다.
중첩구조가 생길만큰 함수가 커져서는 안된다.
함수에서 들여쓰기 수준이 2단을 넘어서지 않아야 읽고 이해하기 쉬워진다.


한 가지만 해라!

함수는 한가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야 한다.

지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면,
그 함수는 한 가지 작업만 하는 것이다.
함수를 만드는 이유가 큰 개념을(함수 이름을) 다음 추상화 수준에서 여러 단계로 나눠서 하기 위함이니까

함수를 단순 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면,
그 함수는 여러 작업을 하고있는 것이다.

함수 내 섹션

함수를 여러 섹션으로 나눌 수 있다면 그 함수는 여러작업을 하는 셈이다.
한가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.


함수 당 추상화 수준은 하나로

함수가 ‘한가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 된다.
한 함수 내에 추상화 수준이 섞이면 읽는 사람이 헷갈린다.

근본 개념과 세부사항을 뒤섞기 시작하면 단순히 구분하기 어려운것만이 문제가 아니다.
깨진창문처럼 사람들이 함수에 세부사항을 점점 더 추가할 것이다.

위에서 아래로 코드 읽기: '내려가기' 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
내려가기 규칙 : 함수 추상화 부분이 한번에 한단계씩 낮아지는 것
각 함수는 다음 함수를 소개하고, 각 함 수는 일정한 추상화 수준을 유지하도록

추상화 수준이 하나인 함수, 짧으면서도 '한가지'만 하는 함수



Switch문

switch문은 작게 만들기 어렵다(if/else의 연속 도 마찬가지!)

public Money calculatePay(Employee e) throws InvalidExployeeType {
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidExployeeType(e.type);
    }
}

위 코드는 긴 함수에 '한 가지' 작업만 수행하지 않고,
SRP(Single Responsibility Principle)와 OCP(Open Closed Prinicple)를 위반한다.

위와 같은 문제를 해결 하기 위해
다형성(polymorphism) 을 이용하여 switch문을 추상팩토리(abstract factory)에 숨긴다.

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
----------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeedRecord r) throws InvalidEmployeeType;
}
----------
public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployyee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                thorw new InvalidEmployeeType(r.type);
        }
    }
}

팩토리는 인스턴스(다형적 객체)를 생성하는 코드 안에서만 switch를 사용하도록 상속 관계로 숨긴다.


하지만, switch문은 불가피하게 써야될 상황이 많으므로 상황에 따라서는 사용 할 수도 있다.


서술적인 이름을 사용하라!

“코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다” - 워드

작은 함수는 그 기능이 명확하므로 이름을 붙이기가 더 쉬우며,
길고 서술적인 이름이 긴 주석보다 좋다.
일관성 있는 서술형 이름을 사용한다면 코드를 순차적으로 이해하기도 쉬워진다.


함수 인수

함수에서 이상적인 인수 개수는 0개(무항).
인수는 코드 이해에 방해가 되는 요소이므로 최선은 0개이고, 차선은 1개뿐인 경우이다.
출력인수(함수의 반환 값이 아닌 입력 인수로 결과를 받는 경우)는 이해하기 어려우므로 왠만하면 쓰지 않는 것이 좋겠다.

많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우 ex)boolean fileExists(“MyFile”);
  • 인수를 뭔가로 변환해 결과를 변환하는 경우 ex)InputStream fileOpen(“MyFile”); -> 파일이름을 InputSream으로 변환
  • 이벤트 함수일 경우 (이 경우에는 이벤트라는 사실이 코드에 명확하게 드러나야 한다.)

위의 3가지가 아니라면 단항 함수는 가급적 피하는 것이 좋다.

플래그 인수

플래그 인수는 추하다.
boolean 값을 넘기는 것 자체가 그 함수는 한꺼번에 여러가지 일을 처리한다고 공표하는 것과 마찬가지다.

이항 함수

단항 함수보다 이해하기가 어렵다.
2개의 인수간의 자연적인 순서가 있어야함 
   ex)Point 클래스의 경우 이항함수가 적절하다.
       Point p = new Point(x,y); 
무조건 나쁜 것은 아니지만, 인수가 2개이니 만큼 이해가 어렵고 위험이 따르므로 가능하면 단항함수로 변경.

삼항 함수

이항 함수보다 이해하기가 훨씬 어려우므로, 위험도 2배 이상 늘어난다.
삼항 함수를 만들 때는 신중히 고려하라.

인수 객체

인수가 2~3개 필요한 경우, 일부를 독자적인 클래스 변수로 선언할 가능성을 살펴보자
ex) Circle makeCircle(doube x, double y, double radius)를
       Circle makeCircle(Point point, double radius)로
x,y를 인자로 넘기는 것보다 Point를 넘기는 것이 더 낫다.

인수 목록

때로는 String.format같은 함수들처럼 인수 개수가 가변적인 함수도 필요하다.
String.format의 인수는 List형 인수이기 때문에 이항함수라고 할 수 있다.

동사와 키워드

단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다. ex) writeField(name);
함수이름에 키워드(인수 이름)을 추가하면 인수 순서를 기억할 필요가 없어진다. ex) assertExpectedEqualsActual(expected, actual);


부수 효과를 일으키지 마라!

부수효과는 거짓말이다.
함수에서 한가지를 하겠다고 약속하고는 남몰래 다른 짓을 하는 것이므로, 한 함수에서는 딱 한가지만 수행할 것!
시간적인 결합이 필요하다면, 함수 이름에 분명히 명시한다.

출력인수

객체 지향 언어에서는 출력 인수로 사용하는 this가 있다.
일반적으로 출력 인수는 피해야 한다.
함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택하라.


명령과 조회를 분리하라

함수는 객체 상태를 변경하거나(수행), 객체 정보를 반환(답)하거나 둘 중 하나다.

둘 다 수행해서는 안 된다.
public boolean set(String attribute, String value); 같은 경우
속성 값 설정 성공여부에 따라 true/false를 반환하므로 괴상한 코드가 작성된다.
if(set(“username”, “unclebob”))...  set이 동사인지 형용사인지 분간하기 어렵다.
명령과 조회를 분리해 혼란을 주지 않도록 한다.


오류코드보다 예외를 사용하라!

명령 함수에서 오류코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반할 수도, 여러단계로 중첩된 코드를 야기할 수도 있다.
try/catch를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해 진다.

Try/Catch 블록 뽑아내기

정상 작동과 오류 처리 동작을 뒤섞는 추한 구조이므로 if/else와 마찬가지로 블록을 별도 함수로 뽑아내는 편이 좋다.

오류 처리도 한가지 작업이다.

함수는 '한 가지' 작업만 해야 한다.
오류처리도 한가지 작업에 속한다.

Error.java 의존성 자석

public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
위 의 Error enum class는 의존성 자석이다.
오류를 처리하는 곳곳에서 오류코드를 사용한다면 Error enum class를 import해서 쓰게 되는데, 새 오류코드를 추가하거나 변경할 때 번거롭기에 새 오류를 추가하는대신 기존오류코드를 재사용 한다.
오류 코드 대신 예외를 사용하면 새 예외는 Exception클래스에서 파생되어 재컴파일/재배치 없이 새 예외 클래스를 추가할 수 있다. (OCP응 보여주는 예)
그러므로 예외를 사용하는 것이 더 안전하다.


반복하지 마라!

DRY(Don't Repeat Yourself)원칙

중복은 모든 소프트웨어에서 모든 악의 근원이므로 늘 중복을 없애도록 노력해야한다.


구조적 프로그래밍

데이크스트라의 구조적 프로그래밍의 원칙에 따르면,

모든 함수와 함수 내 모든 블록에 입구와 출구가 하나여야 된다.

즉, 함수는 return문이 하나여야 되며, 
루프 안에서 break나 continue를 사용해선 안된며, goto는 절대로 사용하지 말자. 

함수가 클 경우에만 상당 이익을 제공하므로, 만약 함수를 작게 만든다면 간혹 return, break, continue를 여러차례 사용해도 괜찮다.
오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
반면 goto문은 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야만 한다.


함수를 어떻게 짜죠?

처음에는 길고 복잡하고, 들여쓰기 단계나 중복된 루프도 많고, 인수목록도 길다.
하지만 이 코드들을 빠짐없이 테스트하는 단위 테스트 케이스도 만들고,
코드를 다듬고,
함수를 만들고,
이름을 바꾸고,
중복을 제거한다.

처음부터 탁 짜지지는 않는다.

profile
정신 차려보니 개발자가 되어있었다.

0개의 댓글