함수작게 만들어라블록과 들여쓰기한가지만 해라함수 내 섹션함수 당 추상화 수준은 하나로 하자!위에서 아래로 코드읽기 : 내려가기 규칙Switch 문예제개선서술적인 이름을 사용하자함수 인수많이쓰는 단항 형식플래그 인수이항 함수삼항 함수인수 객체인수 목록동사와 키워드동사키워드부수 효과를 일으키지 마라출력 인수명령과 조회를 분리하라오류 코드보다는 예외를 사용하자Try/Catch 블록 뽑아내기오류처리도 한 가지 작업이다.Error.java 의존성 자석반복하지 마라구조적 프로그래밍함수를 어떻게 짜죠?
함수
어떤 프로그램이든 가장 기본적인 단위는 “함수”다 3장은 함수를 잘 만드는 방법을 소개한다.
작게 만들어라
- 함수를 만드는 첫번째 규칙은 “작게”다. 이 책의 저자는 함수가 적을 수록 좋다는 근거를 대기는 어렵지만, 40여년 동안 온갖 크기로 함수를 구현해봤을 때 작은 함수가 좋았다고 확신하고 있다.
public static String renderPageWithSetupsandTeardowns(PageDate pageData, boolean isSuite) throws Exception { if (isTestPage(pageDate)) includeSetupAndTeardownpages(pageData, isSuite); return pageData.getHtml(); }
- 저자는 함수를 분리하여 수십줄의 코드를 9줄의 코드로 9줄의 코드를 3줄의 코드로 수정할 수 있다고 한다.
- 각 함수가 하나의 이야기를 포현하고 각 함수가 너무도 멋지게 다음 무대를 준비했다고 표현하고 있다.
- 그리고 그것이 올바른 코드라고 말하고 있다.
블록과 들여쓰기
- if, else, while 문 등에 들어가는 블록은 한 줄이어야 한다는 뜻이다.
- 대개 거기서 함수를 호출하고 그러면 바깥을 감싸는 함수가 작아질 뿐 아니라 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다.
- 즉 중첩 구조가 생길 만큼 함수가 커져서는 안된다는 뜻을 말하고 있다.
- 함수의 들여쓰기 수준은 1단이나 2단을 넘어서지 않도록 코드를 작성하자.
한가지만 해라
- 기존의 수십줄의 코드는 여러 가지를 처리한다. (버퍼 생성하기, 페이지 가져오기, 상속된 페이지 검색하기, 경로를 랜더링하기, 불가사의한 문자열을 덧붙여 HTML을 생성하기)
- 반면 개선된 코드는 설정 페이지와 해제 페이지를 테스트 페이지에 넣는다.
- 함수는 한가지의 일을 “잘”해야하고 한 가지만을 해야한다.
- 위의 개선된 코드는 3가지의 일을 한다고 생각할 수 있지만, 지정된 함수 이름 아래에서 추상화 수준이 하나다.
renderPagewithSetupsAndTeardowns의 함수 이름에서 알 수 있듯이 함수이름에서 표현하는 일들을 충실하게 수행하고 있다.
- 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러작업을 하는 것이므로 “한 가지만 하는 함수”가 아니라 할 수 있다.
함수 내 섹션
- 한 함수 내에서 여러가지 섹션으로 나눠진다면 여러가지 작업을 한다는 증거다. 한 가지 작업만 하는 함수는 섹션을 나누기가 어렵다.
함수 당 추상화 수준은 하나로 하자!
- 함수가 확실히 “한 가지” 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
- 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
- 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.
위에서 아래로 코드읽기 : 내려가기 규칙
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
- 내려가기 규칙 : 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
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 calculcateDay(); public abstract void deliverPay(Money pay); } public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } public class EmployeeFactoryImpl implements EmployeeFactory{ @Override public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmplyee(r); case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmployee(r); default: throw new InvalidEmployeeType(r.type): } } }
이 코드는 어떻게 개선되었을까?
- 위 개선안 코드는 switch 문을 추상 팩토리에 숨긴다.
- calculatePay, isPayDay, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출한다. (다형성을 이용함)
저자는 불가피한 상황도 생기지만 switch문을 다형성 객체를 생성해주는 코드에서 단 한번만 참아주는 것이 좋다고 주장하고 있음
서술적인 이름을 사용하자
testableHtml 보다 SetupteardownIncluder.render와 같이 함수가 하는 일을 좀 더 잘 표현하는 이름이 좋다.
- 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
- 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
- 함수 이름을 정할 때 여러 단어가 쉽게 읽히는 명명법을 사용한다.
- 그 후 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
- 이름을 붙일 때는 일관성이 있어야 한다.
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.(
includeSetupAndTeardownPages,includeSetupPages,includeSuiteSetupPage,includeSetupPage등)
함수 인수
함수에서 인수는 적을 수록 이상적이다. 3개는 가능한 피하고 4개 이상은 특별한 이유가 필요하지만 이유가 있어도 사용하면 안된다고 주장한다.
- 인수는 코드를 읽는 사람이 의미를 해석해야 하는 임무를 추가한다.
- 추상화 수준이 다르다면 더욱 적합하지 않다.
- 테스트 관점에서도 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스에선 복잡해진다.
많이쓰는 단항 형식
- 하나의 인수에 질문을 던지는 경우
- 입력 인수를 변환해 결과를 반환하는 경우
- 이벤트(입력 인수로 시스템 상태를 바꾸는 경우) - 이벤트라는 사실이 코드에 명확히 드러나야 한다.
boolean fileExists("Myfile"); InputStream fileOpen("MyFile"); passwordAttempFailedNtimes(int attempts);
- 위 경우를 제외하고는 단항 함수는 가급적 피해야 한다.
void transform(StringBuffer out)와 같이 출력 인수를 변환 함수에서 사용하면 혼란을 일으킨다.StringBuffer transform(StringBuffer in)이 좀 더 낫다.
플래그 인수
- 부울 값을 인수로 넘기는 것은 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
- 플래그 인수는 사용하지 않는 것이 좋다.
이항 함수
- 인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다. 좌표를 생성하는 Point p = new Point(0,0)과 같은 코드는 물론 예외겠지만 인수가 많을 수록 혼란을 야기한다.
- assertEquals(expected, actual)도 첫번째 인수가 expected고 두 번째 인수가 actual 이라는 것을 인지하고 있어야 한다. 될 수 있으면 단항 인수가 더 좋다.
삼항 함수
- 인수가 두 개인 함수보다 더 이해하기 어렵다. 신중히 고려해야 한다.
인수 객체
- 인수개 2~3개 필요하다면 일부를 묶어 클래스 변수로 만드는 것을 고려 해본다.
인수 목록
- 때로는 인수 개수가 가변적인 함수도 필요하다. (String.format())
- 가변 인수 전부를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다.
- String.format()은 이항 함수
동사와 키워드
- 함수의 의도나 인수의 순서와 의도를 제대로 표현하기 위해 좋은 함수이름이 필수이다.
동사
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.
- write(name) 또는 writeField(name)
키워드
- assertEquals 보다 assertExpectedEqualsActual이 인수의 순서를 기억할 필요 없으므로 좋다.
부수 효과를 일으키지 마라
- 부수 효과란 예상치 못하게 클래스 변수를 수정하거나 인수나 시스템 전역 변수를 수정하는 등의 행위를 말한다.
- password를 체크하는 checkPassword() 함수에서 세션을 초기화 하는 등의 행동이 있다면 이것은 부수효과라 할 수 있다. 함수 이름에서는 비밀번호가 일치하는지 확인하는 행동만을 예상할 수 있다. 그러므로 checkPasswordAndInitializeSession과 같은 함수 이름이 더 적합하다.
출력 인수
- appendFooter(s);
- 일반적으로 우리는 s를 입력 인수로 생각하고 s를 바닥글로 첨부할 것이라고 생각하지만 만약에 함수 선언부가 public void appendFooter(StringBuffer report) 라면? 이것이 출력 인수라면? 인지적으로 굉장히 거슬린다.
- report.appendFooter()가 적합하다.
- 일반적으로 출력 인수는 피해야한다.
- 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
명령과 조회를 분리하라
- 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
public boolean set(String attribute, String value);
- 위 함수는 attribute인 속성값을 찾아 value로 설정하고 성공하면 true, 실패하면 false를 반환하는 함수라고 했을 때
if (set("username", "unclebob"))
- 위 구문은 굉장히 모호하다. 관점에 따라 다르게 해석될 수 있다.
- username이 unclebob으로 설정 되어있는지 확인하는 것인가?
- username을 unclebob으로 설정하는 것인가?
if (attributeExists("username")) { setAttribute("username", "unclebob"); }
- 위 코드로 개선할 수 있다.
오류 코드보다는 예외를 사용하자
오류 코드를 사용하면 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야한다는 문제점에 부딪히게 된다.
if (deletePage(page) == OK) { if(registry.deleteReference(page.name) == OK) { if(configKeys.deleteKey(paga.name.makeKey()) == OK) { ~~~~~~ } } }
- 예외 처리는 오류 처리 코드가 원래 코드에서 분리되기 때문에 코드가 더 깔끔해진다.
try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()) } catch(Exception e) { ~~~~~~ }
Try/Catch 블록 뽑아내기
public 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) { ~~~~
오류처리도 한 가지 작업이다.
함수는 한가지 작업만 해야 하고 오류 처리도 한 가지 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
Error.java 의존성 자석
- 오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 뜻을 가지고 있다.
- 다른 클래스에서 Error enum을 import해야 하므로 의존성이 생긴다.
- 오류 코드 대신 예외를 사용하면 새 예외는 Exception에서 파생되므로 새 예외 클래스를 쉽게 추가할 수 있다.
반복하지 마라
- 코드의 중복은 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러군데에서 손봐야 한다.
- 그에 따라 오류가 발생할 확률도 높다.
- 구조적 프로그램이 AOP, COP 모두 중복 제거 전략이다.
구조적 프로그래밍
함수는return문이 하나여야 한다. 루프 안에서break,continue,goto사용은 금물..
- 이 규칙은 함수가 클 때 큰 효과를 얻는다.
- 함수를 작게 만든다면 사용해도 괜찮다. 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
- 반면 goto는 작은 함수에서는 피해야 한다. (그냥 안쓰는게 나을 것 같다.)
함수를 어떻게 짜죠?
- 길고 복잡한 코드를 다듬고, 함수를 만들고, 이름을 바구고, 중복을 제거하고, 메서드를 줄이고, 순서를 바꾸는 등 점진적으로 수정해 나가며 규칙을 만족하는 함수를 만든다.