Refactoring
Chapter 10. 조건부 로직 간소화
- 조건부 로직은 프로그램을 복잡하게 만드는 주요 원흉이다.
- 조건부 로직을 이해하기 쉽게 바꿔보자.
조건문 분해하기
복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가장 흔한 원횽이다.
코드를 부위별로 분해한 후 분해된 코드 덩어리들을 의도를 살린 이름의 함수 호출로 바꾸자
개요
Before
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
After
if (summer()) {
charge = summerCharge();
} else {
charge = regularCharge();
}
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
절차
- 조건식과 조건절을
함수로 추출
하기
Example
//취향에 따라 3항 연산자로 변경 가능
charge = summer() ? summerCharge() : regularCharge();
조건식 통합하기
비교하는 조건은 다르지만 결과 로직이 같다면 하나로 통합하자.
- 여러 조각의 조건들을 통합하면 더 명확해진다.
- ‘무엇’이 아닌 ‘왜’를 말해주는 함수 추출하기로 이어질 수 있다.
함수 추출하기를 적절히 활용하여 전체를 더 이해하기 쉽게 만들어보자.
개요
Before
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
After
if (isNotEligibleForDisability()) return 0;
function isNotEligibleForDisability() {
return ((anEmployee.seniority < 2)
|| (anEmployee.monthDisabled < 12)
|| (anEmployee.isPartTime));
}
절차
- 해당 조건식에 사이드 이펙트가 없는지 확인
- 사이드 이펙트가 있을 경우
질의 함수와 변경 함수 분리하기
선 적용
- 사이드 이펙트가 있을 경우
- 조건문 두 개를 선택하여 논리 연산자로 결합
- 테스트
- 조건이 하나만 남을 때까지 2~3 반복
- 하나로 합쳐진 조건식을
함수로 추출
할지 고려
중첩 조건문을 보호 구문으로 바꾸기
의도를 부각하는 것이 핵심이다.
두 경로 중 한 쪽만 정상이라면 비정상 조건을 if 에서 검사한 뒤, 조건이 참(비정상)이면 함수에서 빠져나오게 하자.
개요
Before
function getPayAmount() {
let result;
if (isDead)
result = deadAmount();
else {
if (isSeparated)
result = separateAmount();
else {
if (isRetired)
result = retiredAmount();
else
result = normalPayAmount();
}
}
return result;
}
After
function getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separateAmount();
if (isRetired) return retiredAmount();
return normalPayAmount();
}
절차
- 교체해야 할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾸기
- 테스트
- 필요에 따라 1.~2. 반복
- 보호 구문들의 조건식 통합하기
조건부 로직을 다형성으로 바꾸기
복잡한 조건부 로직은 클래스와 다형성을 이용하여 더 확실하게 분리하자.
개요
Before
function plumages(birds) {
return new Map(birds.map((b) => [b.name, plumage(b)]));
}
function speeds(birds) {
return new Map(birds.map((b) => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
switch (bird.type) {
case '유럽 제비':
return '보통이다';
case '아프리카 제비':
return bird.numberOfCoconuts > 2 ? '지쳤다' : '보통이다';
case '노르웨이 파랑 앵무':
return bird.voltage > 100 ? '그을렸다' : '예쁘다';
default:
return '알 수 없다';
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
//...
}
}
After
function plumages(birds) {
return new Map(birds
.map((b) => createBird(b))
.map((bird) => [bird.name, plumage(bird)]),
);
}
function speeds(birds) {
return new Map(birds
.map((b) => createBird(b))
.map((bird) => [bird.name, bird.airSpeedVelocity]),
);
}
function createBird(bird) {
switch (bird.type) {
case '유럽 제비':
return new EuropeanSwallow(bird);
case '아프리카 제비':
return new AfricanSwallow(bird);
case '노르웨이 파랑 앵무':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() { return '알 수 없다'; }
get airSpeedVelocity() { return null; }
}
class EuropeanSwallow extends Bird {
get plumage() { return '보통이다'; }
get airSpeedVelocity() { return 35; }
}
class AfricanSwallow extends Bird {
get plumage() { return this.numberOfCoconuts > 2 ? '지쳤다' : '보통이다'; }
get airSpeedVelocity() { return 40 - 2 * bird.numberOfCoconuts; }
}
class NorwegianBlueParrot extends Bird {
get plumage() { return this.voltage > 100 ? '그을렸다' : '예쁘다'; }
get airSpeedVelocity() { return bird.isNailed ? 0 : 10 + bird.voltage / 10; }
}
절차
- 다형적 동작을 표현하는
클래스 만들기
- 적합한 인스턴스를 알아서 만들어 반환하는
Factory 함수
도 만들기
- 적합한 인스턴스를 알아서 만들어 반환하는
- 호출 코드에서 Factory 함수를 사용하도록 수정
- 조건부 로직 함수를 슈퍼클래스로 옮기기
- 서브 클래스 중 하나를 선택하여, 슈퍼클래스의 조건부 로직 메서드를 오버라이드하기
- 해당 서브클래스에 해당하는 조건절을 메서드로 복사
- 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현하기
- 슈퍼클래스 메서드에는 깁노 동작 부분만 남기기
특이 케이스 추가하기
특정 값에 대해 똑같이 반응하는 코드가 여러 곳에 있다면 그 반응들을 한데로 모으자.
특이 케이스 패턴 : 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 패턴
- 특이 케이스를 확인하는 코드 대부분을 단순 함수 호출로 수정 가능
개요
Before
if (aCustomer === '미확인 고객') customerName = '거주자';
After
class UnknownCustomer { // 자바의 경우 서브 클래스로 사용
get name() { return '거주자'; }
}
절차
슈퍼 클래스에
특이 케이스인지를 검사
하는 속성 추가 (false 반환)class Customer { get isUnknown() { return false; } // 향후 추가 get name {} get billingPlan() {} set billingPlan(arg) {} get paymentHistory() {} }
특이 케이스 전용 서브 클래스
만들기- 특이 케이스인지 검사하는 속성만 포함 (true 반환)
class UnknownCustomer { get isUnknown() { return true; } // 향후 특이 케이스 기본값 추가 get name() { return '거주자'; } get paymentHistory() { return new NullPaymentHistory(); } } // .. class NullPaymentHistory { get weeksDelinquentInLastYEar() { return 0; } }
클라이언트에서 특이 케이스인지 검사하는 코드를
함수로 추출
- 값을 직접 비교하는 코드를 추출한 함수로 수정
function isUnknown(arg) { if (!(arg instanceof Customer || arg === '미확인 고객')) { throw new Error(`잘못된 값과 비교: <${arg}>`); } return arg === '미확인 고객'; }
코드에 새로운 특이 케이스
대상 추가
- 함수의 반환 값으로 받거나 변환 함수 적용
class Site { get customer() { return (this._customer === '미확인 고객')? new UnknownCustomer() : this._customer; } }
특이 케이스를 검사하는 함수 본문을 특이 케이스
객체 속성을 사용
하도록 수정function isUnknown(arg) { if (!(arg instanceof Customer || arg instanceof UnknownCustomer)) { throw new Error(`잘못된 값과 비교: <${arg}>`); } return arg.isUnknown; }
테스트
여러 함수를
클래스 묶기
나변환함수로 묶기
적용- 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮기기
특이 케이스 검사 함수를 이용하는 곳이 남아 있다면
검사 함수 인라인
어서션 추가하기
assertion 은 프로그램이 어떤 상태임을 가정하고 실행되는지 다른 개발자에게 알려주는 훌륭한 소통 도구다.
개요
Before
if (this.discountRate) {
base = base - this.discountRate * base;
}
After
assert(this.discountRate >= 0);
if (this.discountRate) {
base = base - this.discountRate * base;
}
절차
- 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션 추가하기
- 반드시 참이어야 하는 것 혹은 프로그래머가 일으킬만한 오류에만 어서션을 사용하자.
제어 플래그를 탈출문으로 바꾸기
제어 플래그 : 코드의 동작을 변경하는 데 사용되는 변수
개요
Before
for (const p of people) {
if (!found) {
if (p === '조커') {
sendAlert();
found = true;
}
}
}
After
for (const p of people) {
if (p === '조커') {
sendAlert();
break;
}
}
절차
- 제어 플래그를 사용하는 코드를
함수로 추출
할지 고려하기 - 제어 플래그를 갱신하는 코드 각각을
적절한 제어문
으로 바꾸기 (매번 테스트)- 제어문으로 주로 return, break, continue 사용
- 제어 플래그 제거하기