1. 작게 만들어라
- 함수를 작게 만들수록 하나의 함수를 이해하기가 쉬워진다
- 중첩 구조가 생길만큼 함수가 커서는 안되고 들여쓰기 수준은 1~2단을 넘지 않는 것이 좋다
2. 한 가지만 해라
- 함수는 한 가지를 해야한다. 그 한 가지를 잘해야 한다. 그 한 가지만을 해야한다
- 우리가 함수를 만드는 이유는 큰 개념을(즉 함수 이름을) 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이다
- 함수가 한가지만 하는지 판단하는 방법은 의미있는 이름으로 다른 함수를 추출할 수 있는가 없는가이다
- 한 함수에서는 추상화 수준이 하나여야 한다
3. 함수 당 추상화 수준은 하나로 맞춰라
getHtml();
String pagePathNAme = PathParser.render(pagepath);
pagePathName.append("\n");
- 하나의 함수에 추상화 수준이 다른 코드들이 뒤섞이면 가독성이 떨어진다
- 이렇게 근본 개념과 세부 사항이 뒤섞이기 시작하면 다른 개발자들이 함수에 세부 사항을 점점 더 추가한다
위에서 아래로 코드 읽기: 내려가기 규칙
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋다
- 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 단계씩 낮아지는 것을 내려가기 규칙이라고 부른다
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:
throws new InvalidEmployeeType(e.type);
}
}
- 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.
- 한가지 작업만 수행하지 않는다
- 코드를 변경할 이유가 여럿이기 때문에 단일 책임 원칙을 위반한다
- 새 직원 유형이 추가될 때마다 코드를 변경해야 하기 때문에 개방 폐쇄 원칙을 위반한다
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 EmployeeFactorylmpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r)
case HDURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r};
default :
throw new InvalidEmployeeType(r.type};
}
}
}
- before 버전의 문제를 해결한 것이 after 버전이다
- switch문을 추상 팩토리(ABSTRACT FACTRORY, 예제에서는 EmplyeeFactory)에 숨긴다
- 메소드의 실제 실행은 Employee 인터페이스를 거쳐 호출되며 다형성으로 인해 실제 파생 클래스의 함수가 실행된다
- 상속 관계로 switch문을 숨긴 뒤에 최대한 다른 코드에 노출하지 않는다
4. 서술적인 이름을 사용하라
- 길고 서술적인 이름이 짧고 어려운 이름보다 좋다
- 길고 서술적인 이름이 길고 서술적인 주석보다 좋다
- 이름을 붙일 때는 일관성이 있어야 한다
5. 함수 인수는 적을수록 좋다
- 함수에서 이상적인 인수 개수는 0개다
- 차선은 입력 인수가 1개인 경우다
많이 쓰는 단항 형식
- 사용할 때
- 인수에 질문을 던지는 경우
- 인수를 뭔가로 변환해 반환하는 경우
- 사용을 피해야 할 때
- 변환 함수에서 출력 인수를 사용하는 경우
- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다
플래그 인수
- 인수로 논리값(true or false)을 보내는 것은 함수가 한 번에 여러가지를 처리한다고 보기 때문에 자제하는 것이 좋다
findAll(ture);
findAll(false);
findTrueAll();
findFalseAll();
이항 함수
writeField(outputStream, name);
writeField(name);
- 인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다
- 이런 경우 오류가 발생하기 쉽다
- writeField 메소드를 outputStream 클래스 구성원으로 만들어 처리한다
outputStream.writeField(name)
- outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않는다
class A{
private OutputStream outputStream;
...
public void logic(String name){
...
writeField(name);
}
}
- FieldWrite 라는 클래스를 만들어 구성자에 outputStream을 받아서 처리한다
class FieldWrite{
private OutputStream outputStream;
public FieldWrite(OutputStream outputStream){
this.outputStream = outputStream;
}
public void FieldWrite(String name){...}
}
삼항 함수
- 삼항 함수는 인수가 2개인 함수보다 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다
인수 객체
- 인수가 2개 이상일 때 일부가 따로 정보를 제공하는 Meta Class로 축약시킬 수 있을지 살펴본다
Circle makeCircle(double x, double y, double radius);
- 이 경우 x와 y는 평면상의 위치 좌표값을 나타내기에 Point 클래스로 묶을 수 있다
- 두 개의 필드를 하나로 묶어 가독성을 높인다
Circle makeCircle(Point center, double radius);
인수 목록
- 인수가 가변적인 함수도 있다(Ex: String.format)
- 하지만 가변적으로 추가되는 인자를 동등하게 취급하면 하나의 인수형태(List)로 취급할 수 있다
동사와 키워드
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다
- 함수와 인수가 동사/명사 쌍을 이뤄야 한다
6. 부수효과를 일으키지 말아라
- 함수에서 하나의 로직 외에 다른 로직을 부수적으로 수행하게 하지 말아라.
- 비밀번호 확인 메소드(checkPassword)를 호출했는데 비밀번호 확인만 하는게 아니라 세션초기화(Session.initialize())까지 한다면 이 메소드에서는 비밀번호만 확인하기위해 메소드를 호출했다가 의도치않게 세션까지 초기화되는 무수효과로 에러가 발생할 수 있다.
- 즉, 세션을 초기화해도 괜찮은 경우에만 호출이 가능하게 된다
7. 명령과 조회를 분리해라
- 함수는 뭔가 수행하거나 뭔가에 답하거나 둘 중 하나만 하도록 한다
public boolean set(String attribute, Strinv value);
if(set("username", "unclebob")){...}
if(attributeExists("username")){
setAttribute("username", "unclebob");
...
}
- worst case는 set메소드를 호출하는데 이게 뭘 어디에 설정하는건지 왜 if문안에 있는건지 알기 어렵다
- attribute 조회와 설정(명령)을 나눠서 수행한다
8. 오류 코드보다 예외를 사용하라
- 오류 코드로 반환을 하게되면 그에 대한 처리로직을 따로 조건문을 통해 처리해줘야 하는데 이는 코드의 indent를 증가시킬 뿐 아니라 오류를 바로 처리해야한다는 문제가 생긴다
- 그렇기에 오류코드를 사용하기보다는 예외를 사용하면 try/catch를 통해 catch에서 관리할 수 있다
Try/Catch 블록 분리
try{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch(Exception e){
logger.log(e.getMessage());
}
pulbic 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());
}
- delete 함수에서 모든 예외를 받아 처리한다. 실제 페이지 제거 로직은 deletePageAndAllReferences 으로 분리했고 에러 출력에 대한 메소드도 logError로 분리했다
9. 반복하지 마라
- 같은 로직을 중복해서 작성하면 해당 로직이 변경되었을때 사용되는 모든 곳에서 수정을 해줘야 한다.
- 객체지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다.
감상
- 너무 많이 줄여버려면 뜻을 알 수 없어서 어쩔 수 없이 긴 함수명을 지은 적이 있는데 짧고 뜻을 알 수 없는 것보다 길고 이해가 잘 되는 이름이 낫다는 것을 보고 약간 마음이 놓였다. 그리고 예시로 작성된 코드 중에서 일부는 예전 프로젝트에서 비슷한 것을 봤던 기억이 났다. 특히 오류 코드로 반환을 하지 말라는 부분은 완전히 같아서 놀랐다. 일을 하던 당시에는 별 생각없이 넘어가던 부분이어서 이 책을 읽고 코드를 봤으면 좀 더 여러가지 생각을 했을텐데 싶어서 아쉬웠다.