3장. 함수
3장에선 함수를
읽기 쉽고 이해하기 쉽게,
의도를 분명하게 표현하게,
직관적으로 내부를 파악할 수 있게
구현하는 방법을 설명한다.
작게 만들어라!
작가의 경험과 시행착오에 의하면 함수는 최대한 작게 만들어야 하며 20줄도 길다고 한다.
블록: 함수가 작을수록 코드 이해가 쉬워지기에 if문 / else문 / while문 등에 들어가는 블록은 한 줄이어야 한다.
들여 쓰기 : 1단 or 2단을 넘어서면 안 된다.
한 가지만 해라!
함수는 한 가지 작업만 해야 한다고 한다.
여기서 '한 가지'의 기준은 추상화 수준으로, 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다는 것이다.
이를 확인하는 다른 방법으로 함수 내에서 의미 있는 이름으로 다른 함수를 추출할 수 없다면 한 가지 작업을 하는 것이다.
함수당 추상화 수준은 하나로!
함수가 한 가지 작업만 하기 위해선 함수 내 모든 문장의 추상화 수준이 동일해야 한다고 한다.
추상화 수준이 동일하지 않으면 코드를 읽는 사람이 헷갈리고 더 나아가 사람들이 함수에 세부 사항을 점점 더 추가한다는 것이 그 이유다.
위에서 아래로 코드 읽기: 내려가기 규칙
- 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 와야 한다는 것으로 동작 순서에 맞게 함수를 배치해야 한다.
Switch문
함수를 작게 만들어야 하지만 switch문을 활용하면 이가 불가능하며 switch문은 본질적으로 한 가지 작업만 하지 않는다고 한다.
그럼에도 switch문을 피할 수 없기에 다형성을 이용해 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를 위반함
등의 문제가 있다.
따라서 추상팩토리를 활용해 아래와 같이 구현한다.
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 COMMISSIONED:
return calculateCommissionedPay(r);
case HOURLY:
return calculateHourlyPay(r);
case SALARIED:
return calculateSalariedPay(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
서술적인 이름을 사용하라!
서술적인 이름을 사용하면 함수가 하는 일을 더 잘 표현할 수 있고 서술적인 주석보다 좋기에 이를 위해 오랜 시간을 들여도 되고 함수의 이름이 길어져도 된다고 한다.
서술적인 이름으로 이루어진 함수는 개발자가 생각하기도 뚜렷해지기에 개선하기 쉬워진다는 장점이 있다고 설명한다.
함수 인수
함수에서 이상적인 인수 개수는 0개(무항)이며, 가능한 이를 위해 노력해야 한다고 한다.
차선책은 입력 인수가 1개인 경우뿐으로 인수가 많으면 많을수록 해당 함수를 이해하기 어렵다고 한다.
인수가 1개인 경우는
- 인수에 질문을 던지는 경우
- ex - boolean fileExists("MyFile");
- 인수를 무언가로 변환해 결과를 반환하는 경우
- ex - InputStream fileOpen("MyFile");
- 이벤트가 실행되는 경우(입력 인수만 있고 출력 인수가 없는 경우)
- ex - passwordAttemptFailedNtimes(int attempts)
가 가장 대표적이며 이외엔 단항 함수를 가급적 피해야 한다고 한다.
인수가 2개 이상인 경우는 불가능한 경우가 아니면 피해야 하며 클래스를 새로 만들어 메서드를 구현하거나 멤버 변수로 만드는 형식으로 변환하는 것이 좋다고 한다.
// 인수가 3개인 경우
Circle makeCircle(double x, double y, double radius);
// 새로운 클래스를 만들어 인수를 줄인 경우
Circle makeCircle(Point center, double radius);
만약 인수가 여러 개라면 함수 이름에 인수의 순서와 의도를 제대로 표현할 필요가 있다고 설명한다.
// 위보다
assertEquals(expected, actual)
// 아래가 더 낫다.
assertExpectedEqualsActual(expected, actual)
부수 효과를 일으키지 마라!
함수가 동작하면서 부수 효과가 있다는 것은 한 가지 일만 하는 것이 아니기에 이를 지양해야 한다고 한다.
아래는 책에 나온 예시 코드이다.
public class userValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
위 함수는 비밀번호를 확인하는 역할을 하는 것이 목적이지만
부수 효과로 Session.initialize() 호출을 통해 세션을 초기화하는 작업도 한다.
이러한 동작은 혼란을 야기하므로 함수 이름에 작업을 제대로 명시하거나 작업을 분리하는 것이 좋다는 설명이다.
명령과 조회를 분리하라!
함수는 하나의 역할만 해야 하므로 객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나만 해야 한다고 한다.
아래 함수는 attribute 속성을 value로 설정하고 성공했다면 true, 실패했다면 false를 반환하는 역할을 한다.
public boolean set(String attribute, String value);
이를 활용하면 아래의 결과가 나오며 무슨 동작을 하는지 명확히 알기 어렵다.
if(set("username", "uncelbob"))
따라서 객체 상태 변경과 정보 반환을 분리해 아래와 같이 구현해야 한다는 설명이다.
if(attributeExists("username")) {
setAttribute("username", "uncelbob");
}
오류 코드보다 예외를 사용하라!
오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반하므로 try/catch/finally를 활용해 예외를 활용해야 하며
이를 활용하면 오류 처리도 간편해지고 코드가 깔끔해지는 장점이 있다고 한다.
try/catch를 활용할 땐 블록 내부 동작을 함수로 뽑아내는 것을 추천한다.
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exceotion {
...
}
private void logError(Exception e) {
...
}
오류 처리도 한 가지 작업이므로 오류 처리를 하는 함수는 오류 처리만 해야 한다고 한다.
즉, try로 시작해 catch / finally로 끝나야 한다는 것이다.
반복하지 마라!
같은 코드를 여러 곳에 사용하면
- 코드의 전체 길이가 늘어난다.
- 코드 변경 시 모두 바꿔야 한다.
- 가독성이 떨어진다.
는 문제가 있기에 중복을 없애야 한다고 한다.
구조적 프로그래밍
goto를 지양하라는 내용이다.
함수를 어떻게 짜죠?
내용을 정리하면 아래와 같다.
SW를 구현하는 것은 글짓기와 비슷하다.
글을 지을 때 처음엔 어수선하다가 내용을 다듬고 정리하듯
함수도 처음엔 복잡하지만 저차 다듬고 줄이며 보기 좋은 함수가 된다.
처음부터 완벽한 함수를 짜는 것이 아니라 수정 보완을 거치는 것으로 규칙을 따르는 함수를 만드는 것이다.
느낀 점
Clean Code의 3장은 함수를 어떻게 구현해야 완성도가 높을지에 대한 내용을 다룬다.
가장 어려웠던 부분은 역시 추상화이다.
함수를 작게 만들어라, 한 가지 일만 하게 하라, 반복하지 마라 등은 직관적으로 이해할 수 있었는데, 추상화라는 개념이 어렵게 느껴졌다. 하지만 이에 대해 고민하면서 함수를 구현하니 어느 정도 알았다는 느낌이 든다.
프로그램의 모든 동작은 함수를 통해 이루어지기에 특히나 중요한 장이었다고 생각하며 다른 사람들이 이해하기 쉬운 함수를 구현하기 위해 노력하겠다.
'OOP & CleanCode' 카테고리의 다른 글
CleanCode - 6. 객체와 자료 구조 (1) | 2024.11.30 |
---|---|
CleanCode - 5. 형식 맞추기 (1) | 2024.01.11 |
CleanCode - 4. 주석 (1) | 2024.01.02 |
CleanCode - 1. 깨끗한 코드, 2. 의미 있는 이름 (2) | 2023.10.21 |