10장, 조건부 로직 간소화
2021.11.01
1. Deocompose Conditional, 조건문 분해하기
- 복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가장 흔한 원흉에 속한다.
- 코드를 이해하기 쉽게 거대한 코드 블록을 부위별로 분해해 적절한 이름을 가진 함수로 추출하자.
절차
- 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.
예시
before
1 if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) change = quantity * plan.summerRate;
2 else change = quantity * plan.regularRate + plan.regularServiceCharge;
after
1 if (summer()) charge = summerCharge();
2 else charge = regularCharge();
2. Consolidate Conditional Expression, 조건식 통합하기
- 비교하는 조건은 다르지만 그 결과로 수행하는 동작은 똑같은 코드들은 조건 검사도 하나로 통합하는게 낫다. (
and
혹은 or
연산자를 사용 )- 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 하려는 일이 더 명확해진다.
- 이 과정에서 함수로 추출까지 할 수 있다.
- 독립된 조건 검사들이라면 통합하면 안된다.
절차
- 해당 조건식들 모두 부수효과가 없는지 확인한다.
- 조건문 두 개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합한다.
- 테스트.
- 조건이 하나만 남을 때까지 2~3과정을 반복한다
- 하나로 합쳐진 조건식을 함수로 추출할지 고려해본다
예시
before
1 if (anEmployee.seniority < 2) return 0;
2 if (anEmployee.monthsDisabled > 12) return 0;
3 if (anEmployee.isPartTime) return 0;
after
1 if (isNotEligibleForDisability()) return 0;
2
3 function isNotEligibleForDisability() {
4 return anEmployee.seniority < 2 || anEmployee.monthsDisabled > 12 || anEmployee.isPartTime;
5 }
3. Replace Nested Conditional with Guard Clauses, 중첩 조건문을 보호 구문으로 바꾸기
절차
- 교체해야 할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾼다.
- 테스트.
- 1~2과정을 필요한 만큼 반복한다.
- 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합한다.
예시
before
1 function getPayAmount() {
2 let result;
3 if (isDead) result = deadAmount();
4 else {
5 if (isSeparated) result = separatedAmount();
6 else {
7 if (isRetired) result = retiredAmount();
8 else result = normalPayAmount();
9 }
10 }
11 return result;
12 }
after
1 function getPayAmount() {
2 if (isDead) return deadAmount();
3 if (isSeparated) return separatedAmount();
4 if (isRetired) return retiredAmount();
5 return normalPayAmount();
6 }
4. Replace Conditional with Polymorphism, 조건부 로직을 다형성으로 바꾸기
- 복잡한 조건부 로직은 직관적으로 구조화하면 코드가 조금 더 명확해진다.
- 다형성은 객체 지향 프로그래밍의 핵심이지만 남용하기 쉽다.
- 모든 조건부 로직을 다형성으로 대체할 필요는 없다.
절차
- 다형적 동작을 표현하는 클래스들이 아직 없다면 만들어준다. 이왕이면 적합한 인스턴스를 알아서 만들어 반환하는 팩터리 함수도 함께 만든다.
- 호출하는 코드에서 팩터리 함수를 사용하게 한다.
- 조건부 로직 함수를 슈퍼클래스로 옮긴다.
- 서브클래스 중 하나를 선택한다. 서브클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브클래스 메서드로 복사한 다음 적절히 수정한다.
- 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
- 슈퍼클래스 메서드에는 기본 동작 부분만 남긴다. 혹은 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던진다.
예시
before
1 switch (bird.type) {
2 case '유럽 제비':
3 return '보통이다';
4 case '아프리카 제비':
5 return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다';
6 case '노르웨이 파랑 앵무':
7 return bird.voltage > 100 ? '그을렸다' : '예쁘다';
8 default:
9 return '알 수 없다';
10 }
after
1 class EuropeanSwallow {
2 get plumage() {
3 return '보통이다';
4 }
5
6 }
7
8 class AfricanSwallow {
9 get plumage() {
10 return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다';
11 }
12
13 }
14
15 class NorwegianBlueParrot {
16 get plumage() {
17 return bird.voltage > 100 ? '그을렸다' : '예쁘다';
18 }
19
20 }
5. Introduce Special Case, 특이 케이스 추가하기
- 데이터 구조의 특정 값을 확인한 후 똑같은 동작을 수행하는 코드가 곳곳에 등장하는 경우가 더러 있있는데, 이러한 공통 동작을 한곳에 모으는게 효율적이다.
- 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.
- 널은 특이 케이스로 처리해야 할 때가 많다. ( 널 객체 패턴 )
절차
- 컨테이너에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 한다.
- 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하며, 이 속성은 true를 반환하게 한다.
- 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
- 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
- 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
- 테스트.
- 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
- 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인한다.
예시
before
if (aCustomer === '미확인 고객') customerName = '거주자';
after
1 class UnknownCustomer {
2 get name() {
3 return '거주자';
4 }
5 }
6. Introduce Assertion, 어서션 추가하기
- 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있을 수 있다.
- 이런 가정은 코드에 항상 명시적으로 기술되어 있지 않아서, 어서션(단언)을 이용하여 코드 자체에 삽입해 놓으면 좋다.
- 어서션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘못했다는 뜻이다.
- 어서션이 있고 없고가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성되어야 한다.
- 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어든다.
- 어서션을 남발하는 것 역시 위험하다.
- 반드시 참이야만 하는 것만 검사.
- 프로그래머가 일으킬만한 오류에만 사용.
절차
- 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
예시
before
if (this.discountRate) base = base - this.discountRate * base;
after
1 assert(this.discountRate >= 0);
2 if (this.discountRate) base = base - this.discountRate * base;
7. Replace Control Flag with Break, 제어 플래그를 탈출문으로 바꾸기
- 제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말하며, 어딘가에서 값을 계산해 제어 플래그에 설정한 후 다른 어딘가의 조건문에서 검사하는 형태로 쓰인다.
- 리팩터링으로 충분히 간소화할 수 있음에도 복잡하게 작성된 코드에서 나타나기 때문에, 악취나는 코드로 본다.
- 제어 플래그의 주 서식지는 반복문 안이다. break문이나 continue문 활용에 익숙하지 않은 사람이 심어놓기도 하고, 함수의 return문을 하나로 유지하고자 노력하는 사람이 심기도 한다.
절차
- 제어 플래그를 사용하는 코드를 함수로 추출할지 고려한다.
- 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다. 하나 바꿀 때마다 테스트.
- 모두 수정했다면 제어 플래그를 제거한다.
예시
before
1 for (const p of people) {
2 if (!found) {
3 if (p === '조커') {
4 sendAlert();
5 found = true;
6 }
7
8 }
9 }
after
1 for (const p of people) {
2 if (p === '조커') {
3 sendAlert();
4 break;
5 }
6
7 }