프로그램을 만들 때 함수를 설계하는 것은 간단하지만 매번 어려운 작업이다.
함수를 만들고 나면 너무 많은 기능이 포함되어있는 것 같아 함수를 분리해야할 필요성을 자주 느낀다. 또, 변수 이름을 짓는 것만큼 함수 이름을 짓는 것도 매번 고뇌이다. 이러한 문제점들을 3장의 내용을 통해 해결의 실마리를 찾고 싶고, 어렴풋이 알고 있었던 "구조적 프로그래밍"의 개념도 확실히 익히고자 한다.
작게 만들어라 !
필자는 Sparkle 이라는 자바/스윙 프로그램을 예시로 들어 적은 양의 코드의 중요성을 알려준다. Swing 은 자바의 GUI 컴포넌트의 한 종류이다. 이 프로그램은 각 함수가 명백하고, 각 함수가 이야기 하나를 표현한다.
if, else, while문에 들어가는 블록은 한 줄이어야 한다. 그 줄에서 다른 함수를 호출하고, 이 함수는 코드 의도를 명확히 하기 위해 이름을 적절히 지어야 한다.
한 가지만 해라 !
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만을 해야 한다.
함수가 한 가지만을 한다는 것은 지정된 함수 이름 아래에 추상화 수준이 하나라는 것이다. 만약 함수가 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 것이다.
함수 당 추상화 수준은 하나로 !
추상화란, 구체적인 것에서 핵심적인 것 하나만을 간추려 내는 것이다.
말하는 내용이 구체적일수록 추상화 수준이 낮아지는 것이다. 예시를 들어보자.
- 문서를 출력한다.
- 문서를 프린터를 이용해 출력한다.
- 문서를 와이파이 방식의 프린터를 이용해 출력한다.
이 세가지 문장은 아래로 내려갈 수록 추상화 수준이 낮아진다. 필자가 든 예시를 그대로 적어보자면,
- getHtml()
- String pagePathName = PathParser.render(pagepath);
- .append("\n")
이 세 줄의 코드도 마찬가지로, 내려갈 수록 추상화 수준이 낮아진다. 즉, 구체적인 명령을 수행하게 된다.
내려가기 규칙
한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 이를 내려가기 규칙이라고 한다.
Swtich 문
switch문은 본질적으로 두 가지 이상의 작업을 한다. 따라서 작게 만들기 어려운 특징이 있으므로, 다형성을 이용해 저차원 클래스에 숨기고 반복하지 않아야 한다.
일반적으로 사용하는 Switch 문에는 아래 네 가지 문제점이 있다.
- 함수가 길다.
- 한 가지 작업만 수행하지 않는다.
- SRP(Single Responsibility Principle) 을 위반한다.
- OCP (Open Closed Principle)을 위반한다.
swtich문을 추상 팩토리에 숨기고, 인터페이스를 거쳐 switch문이 호출되도록 하는 것이다.
public abstract class Employee{
public abstract boolean isPayday();
public abstract Money calculatePay();
}
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 ..
}
}
}
서술적인 이름을 사용하라 !
"코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라고 불러도 되겠다."
서술적인 이름이라는 것은 '자세한' 이름이라는 것이다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋고, 길고 서술적인 주석보다 좋다.
이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에 함수 이름은 같은 문구, 동사, 명사를 사용한다. '2장 의미있는 이름'에서 언급한 것과 같이, 회원 정보를 저장하는 메소드를 insert, save와 같은 다양한 표현으로 구현하는 것은 옳지 않다.
함수 인수
함수는 특정 목적에 맞게 인수를 '처리'해야 한다. 즉 인수는 처리 대상이다.
인수에는 '입력 인수', '출력 인수' 두 가지가 있다.
boolean fileExists("Myfile"){ // 입력 인수
..
}
public void appendFooter(StringBuffer report){ // 출력 인수
..
}
입력 인수를 받아 함수에서 조회를 하는 등의 처리를 하는 함수 fileExists와, StringBuffer에 원하는 값을 추가하기 위한 함수 appendFooter이다.
가장 이상적인 함수 인수의 개수는 0개이다. 인수는 개념을 이해하기 어렵게 만든다. 테스트 케이스를 작성할 때도, 인수가 많다면 인수 조합을 구성해 테스트하기가 부담스러워진다.
또한, 출력 인수는 입력 인수보다 인지적인 이유로 이해하기 어렵기 때문에 가급적 피하기를 권한다.
많이 쓰는 단항 형식
함수에 인수를 1개 넘기는 경우는 주로 아래의 경우이다.
1) 조회 : boolean fileExists("MyFile")과 같은 예시가 있다.
2) 변환 : InputStream fileOpen("MyFile")과 같이 인수로 뭔가를 변환해 결과를 반환하는 것이 있다.
3) 이벤트 : 함수 호출 자체를 이벤트로 해석해, 입력 인수로 시스템의 상태를 바꾸는 것이다. 이벤트 함수의 경우, 이벤트라는 사실과 그 행위가 함수 이름에 명확히 드러나야 한다.
이외의 경우에는 가급적 단항 함수를 피하도록 한다.
플래그 함수
"플래그 인수는 추하다" 라는 말에 뜨끔했다. 설계를 신경쓰지 않고 프로그램을 짜다 보면 한 함수에서 시스템 변수 중 하나에 의해 두 가지 방향으로 구현해야하는 경우가 생긴다. 나는 이 경우에서 인자에 boolean 타입의 flag 변수를 하나 넣고, if else문으로 나누어 구현하였다. 딱 보기에도 좋지 않은 설계였지만 클린 코드를 신경쓰지 않을 시절의 나는 그랬다.
말했듯이, 함수는 여러 가지를 한 번에 처리할 수 없다. 따라서 함수로 부울 값을 넘기지 말고, 함수를 분리해야한다.
예를 들어, render(true) 라는 코드보다 renderForSuite(), renderForSingleTest() 라는 함수로 나눠 기능을 따로 구현해야한다.
이항 함수
Point p - new Point(0, 0)과 같이 인수 2개가 한 값을 표현하는 자연적인 순서가 아니라면 2개의 인자는 지양한다.
인수가 2개인 경우에는 인수의 순서도 헷갈릴 뿐 더러, 함수를 구현하는 데 더 중요한 인수가 무엇인지 판단하는 것에 시간이 걸린다.
예를 들어서, assertEquals(expected, actual)이라는 함수는 자연적인 순서가 없다. 따라서 프로그래머는 인수의 순서를 기억해둬야 하는 문제가 생긴다.
이항 함수가 있다면, 클래스 구성원에 함수를 넣는 방법 등으로 단항 함수로 바꾸어야 한다. 예를 들어, writeField(outputStream, name)을 outputStream.writeField(name)으로 변경해주어 인수를 줄일 수 있다.
삼항 함수
마찬가지로, 인수를 이해하는 것에 걸리는 시간, 순서를 기억하는 시간 등의 문제가 발생하므로 지양한다.
단, assertEquals(1.0, amount, .001) 과 같이 부동소수점 비교가 상대적이기에 발생하는 추가적인 인수는 그만한 가치가 충분하다.
인수 객체
인수가 여러개라면 그 일부를 독자적인 클래스 변수로 선언해보자.
Circle makeCircle(double x, double y, double radius);
➡ Circle makeCircle(Point center, double radius);
인수 목록
인수의 개수가 가변적인 함수가 있다. String.format처럼 문자열을 입력받는 함수는 첫 번째 인수의 문자열 리터럴에 그 뒤에 오는 인수들을 차례로 넣는다. 따라서 String.format은 사실상 이항 함수이다.
public String format(String format, Object ...args)
이처럼 가변 인수가 있는 함수는 단항, 이항, 삼항 함수로 취급할 수 있다.
동사와 키워드
좋은 함수 이름은 함수의 의도나, 인수의 순서와 의도를 명확히 표현한다. 그 방법은 아래 두 가지가 있다.
1) 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. 예를 들어 write(name)보다는 writeField(name) 이 더 나은 이름이다.
2) 함수 이름에 키워드를 추가한다. 예를 들어, assertEquals보다 assertExpectedEqualsActual이 더 나은 이름이다.
'DEV book > Clean Code' 카테고리의 다른 글
[Clean Code] 2장 의미 있는 이름 (0) | 2023.04.28 |
---|---|
[CleanCode] 1장 깨끗한 코드 (0) | 2023.04.28 |