티스토리 뷰
해당 장에서는 프로그램의 가장 기본 단위인 함수 설계를 잘하는 방법을 다룬다.
1. 작게 만들어라.
책에서는 함수를 최대한 작게 만들어라를 강조하고 있다. 함수를 작게 만들수록 읽고 쓰기가 쉬워진다. 예제로 든 코드를 보면 리팩터링 한 상태의 함수는 10줄이었는데 해당 함수를 3줄로 줄였고 이렇게 해야 한다고 말하고 있다.
- 블록과 들여쓰기
if/else, while문에 들어가는 블록은 한 줄이어야 한다.
2. 한가지만 해라
"함수는 한 가지를 해야 한다. 그 한 가지를 잘해야 한다. 그 한 가지만 해야 한다."
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다. 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이다.
3. 함수 당 추상화 수준은 하나로!
함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 어떤 문장의 추상화 정도는 높고 다른 건 낮고 이러면 읽기가 힘들어진다.
4. 내려가기 규칙: 위에서 아래로 읽기
한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
5. Switch
Switch문을 작게 만들기는 어렵다. 또한 한 가지 작업만 하는 Switch문도 만들 기 어렵다. Switch문을 저 차원 클래스에 숨기고 절대로 반복하지 않으려면 다형성을 이용하여 다음과 같은 방법을 사용한다.
<직원 유형에 따라 다른 값을 계산해 반환하는 함수>
public Money calculatePay(Employee e) throws InvalidEmployeeType{ switch (e.type){ case COMMISIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: return new InvalidEmployeeType(e.type); } }
위의 코드는 많은 문제가 있다.
1) 함수가 너무 길다
2) 한 가지 작업만 수행하지 않음(SRP위배)
3) OCP 위반. 새 직원 유형을 추가할 때마다 코드를 변경해야 한다.
위의 코드를 다음과 같이 추상화시켜 switch로 해당 Employee 클래스를 반환시켜주자.
public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } 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 COMMISIONED: return new CommissionedEmployee(r); case HOURYLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmployee(r); default: throw new InvalidEmployeeType(r.type); } } }
저자는 일반적으로 switch 문을 다 한 번만 참아준다고 한다. 위와 같이 다형적 객체를 생성하는 코드 안에서 이다.
6. 서술적인 이름을 사용하라.
함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 이름을 붙일 때는 일관성이 있어야 한다.
7. 함수 인수
함수에서 가장 이상적인 인수 개수는 0개(무항)이다. 다음은 1개 이고 그다음은 2개이다. 3개는 가능한 피하는 것이 좋고 4개는 특별한 이유가 있어야 하지만 특별한 이유가 있어도 사용하지 않는 것이 좋다. 테스트에서도 마찬가지다 인수가 많을수록 테스트는 더 복잡해진다.
- 많이 쓰는 단항 형식
함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 다음과 같다
(1) 인수에 질문을 던지는 경우 : boolean fileExists("myFile");
(2) 인수를 뭔가로 반환해 결과를 반환하는 경우: InputStream fileOpen("myFile")/String형 파일 이름을 InputStream으로 반환
(3) 이벤트 형식으로 사용: 이벤트 함수는 입력 인수만 있고 출력 인수는 없다.
위의 경우가 아닐 경우 단항 함수는 가급적 피해야 한다. 예를 들어 void addEntry(String text); 와 같은 경우를 의미한다.
- Flag 인수
Flag인수는 추하다. 함수로 bool값을 넘기는 것은 끔찍한 일이다. 왜냐하면 함수가 한 번에 여러 가지 일을 처리한다고 대놓고 말하는 것이기 때문이다.
-이항 함수
이항 함수는 단항 함수보다 이해하기 어렵다 불가피한 경우는 어쩔 수 없지만 가급적이면 단항 함수를 만들도록 노력하자.
-인수 객체
인수가 2-3개 필요하면 일부를 독자적인 클래스 변수로 선언할 가능성을 생각해보자.
Circle makeCircle(double x, double y, double radius); Circle makeCircle(Pointer center, double radius);
-인수 목록
때로는 인수 개수가 가변적인 함수도 필요하다. 하지만 가변 인수 역시 해당 인수를 하나로 치면 단항 함수 원칙을 지키는 것이 좋다.
void monad(Integer... args); //단항 :) void dyad(String name, Integer... args); //이항 vodi triad(String name, int count, Integer... args); //삼항 :(
8. 부수 효과를 일으키지 마라!
부수효과란 함수 이름에서 정한 한 가지 기능 이외에 다른 기능을 수행해 부수적인 효과를 가져오는 것이다. 예를 들어 로그인 체크를 할 때 Session을 초기화시키는 역할을 같이 하는 함수를 만들 경우 이러한 부수효과를 가져온다.
9. 출력 인수
객체지향 설계에서는 출력 인수를 사용할 필요가 거의 없다 출력인수 대신 사용하기 위해 만들어진 this가 있기 때문이다.
10. 명령과 조회를 분리하라.
함수는 뭔가를 수행하거나 답하거나 둘 중 하나만 해야 한다. 즉 객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나만 하자.
11. 오류코드보다는 예외를 사용하자.
즉 if/else 문으로 오류를 가려내는 거보다 try/catch로 예외를 던지는 것이 코드르 더 깔끔하게 해 준다. 하지만 try/catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다 그렇기 때문에 try/catch블록을 별도 함수로 뽑아내는 것이 좋다.
<try/catch 추출 전>
try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch { logger.log(e.getMessage()); }
<try/catch 추출 후>
public void delete(Page page) { try { deletePageAndAllReferences(Page 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()); }
위에서 delete 함수는 모든 에러를 처리한다. 그 이후 실제로 페이지와 참조 페이지를 제거하는 함수는 deletePageAndAllReferences이다.
-오류 처리도 한 가지 작업이다.
함수는 한 가지 작업만 해야 하는 것처럼 오류 처리도 한 가지 작업에 속한다. 즉 오류처리 함수는 오류처리만 해야 한다.
12. 반복하지 마라.
반복되는 코드가 여러 군데 퍼져 있으면 해당 코드를 수정하려면 반복된 곳 모두를 수정해야 한다. 그렇기 때문에 중복을 없애자!
13. 구조적 프로그래밍
모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다. 즉 함수는 return문이 하나여야 한다. 루프 안에서 break이나 continue를 사용해서는 안되며 goto는 절대로 안된다. 하지만 함수가 작은 경우 return, break, continue를 여러 번 사용하면 오히려 의도를 표현하기 쉬워지는 경우도 있다.
14. 함수를 짜는 방법
함수 역시 글을 쓰는 것과 마찬가지로 계속 가다듬어야 한다. 코드를 작성한 후 테스트 코드를 작성 하자. 이후 해당 코드에 대해 함수를 생성하고, 이름을 바꾸고 중복을 제거 하자 또한 메서드를 줄이고 순서를 바꾸자. 테스트 코드가 잘 통과하는지 확인하면서 계속 리팩터링을 진행하자.
'BOOK' 카테고리의 다른 글
객체지향의 사실과 오해 (0) | 2021.03.28 |
---|---|
[Clean code] 5장 형식맞추기 (0) | 2019.12.15 |
[Clean code] 4장. 주석 (0) | 2019.12.14 |
[Clean Code] 2장. 의미있는 이름 (0) | 2019.12.08 |