핌팩토링
10. 조건문 로직 간소화
by AsCE_hyunseung
2022. 4. 28.
10.1 조건문 분해하기
배경
- 복잡한 조건부 로직은 프로그램을 복잡하게 만드는 원흉이다.
- 조건을 검사하고 그 결과에 따른 동작을 표현한 코드는 모슨 일이 일어나는지는 이야기해주지만 ‘왜'일어나는지는 제대로 말해주지 않을 때가 많은 것이 문제다.
- 거대한 코드 블록이 주어지면 코드를 부위별로 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔준다.
절차
- 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.
예시
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
------------------------------------------------------
if (summer()) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)
}
10.2 조건식 통합하기
배경
- 어차피 같은 일을 할 거라면 조건 검사도 하나로 통합하는 게 낫다.
- 조건부 코드를 통합하는 게 중요한 이유는 두 가지가 있다.
- 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 내가 하려는 일이 더 명확해진다.
- 이 작업이 함수 추출하기까지 이어질 가능성이 높다.
- 독립적인 검사들이라고 판단되면 이 리팩토링을 행하면 안된다.
절차
- 해당 조건식들 모두에 부수효과가 없는지 확인한다.
- 조건문 두 개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합한다.
- 조건이 하나만 남을 때까지 반복한다.
- 하나로 합쳐진 조건식을 함수로 추출할지 고려해본다.
예시
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
---------------------------------------------
if (isNotEligibleForDisablity()) return 0;
function isNotEligibleForDisablity() {
return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime));
}
10.3 중첩 조건문을 보호 구문으로 바꾸기
배경
- 조건문은 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태, 한쪽만 정상이 형태 이렇게 두 형태가 있다.
- 두 형태는 의도하는 바가 서로 다르므로 그 의도가 코드에 드러나야 한다.
- 두 경로 모두 정상 동작이라면 if-else 절을 사용하고, 한쪽만 정상이라면 비정상 조건을 if에서 검사한다음 참이면 함수에서 빠져나온다. (보호 구문)
- 이 리팩터링 기법의 핵심은 의도를 부각하는 데 있다.
- 보호 구문은 “이건 이 함수의 핵심이 아니다. 이 일이 일어나면 무언가 조치를 취한 후 함수에서 빠져나온다" 라고 이야기 한다.
절차
- 교체해야 할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾼다.
- 모든 보호 구문이 같은 결과를 반환하다면 보호 구문들의 조건식을 통합한다.
예시
function getPayAmount() {
let result;
if (isDead) {
result = deadAmount();
}
else {
if (isSeparated) {
result = separatedAmount();
} else {
result = normalPayAmount();
}
}
return result;
}
------------------------------------
function getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount()
return normalPayAmount();
}
10.4 조건부 로직을 다형성으로 바꾸기
배경
- 조건문 구조를 클래스와 다형성을 이용해서 분리한다.
- case별로 클래스를 하나씩 만들어 공통 switch 로직의 중복을 없앨 수 있다.
- 다형성을 활용하여 어떻게 동작할지를 각 타입이 알아서 처리하도록 하면 된다.
절차, 예시
- 다형성 동작을 표현하는 클래스들이 아직 없다면 만들어준다. 인스턴스를 만들어 반환하는 팩터리 함수도 함께 만든다.
- 호출하는 코드에서 팩터리 함수를 사용하게 한다.
- 조건부 로직 함수를 슈퍼클래스로 옮긴다.
- 서브클래스 중 하나를 선택한다. 서브클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브클래스 메서드로 복사한 다음 적절히 수정한다.
- 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
- 슈퍼클래스 메서드에는 기본 동작 부분만 남긴다. 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던진다.
switch (bird.type) {
case '유럽 제비':
return "보통이다";
case '아프리카 제비':
return (bird.numberOfCoconuts > 2) ? '지쳤다' : '보통이다';
case '노르웨이 파랑 앵무':
return (bird.voltage > 100) ? '그을렸다.' : '예쁘다';
default:
return '알 수 없다';
}
------------------------------------------------------------
class EuropeanSwallow {
get plumage() {
return "보통이다";
}
}
class AfricanSwallow {
get plumage() {
return (bird.numberOfCoconuts > 2) ? '지쳤다' : '보통이다';
}
}
class NorwegianBlueParrot {
get plumage() {
return (bird.voltage > 100) ? '그을렸다.' : '예쁘다';
}
}
10.5 특이 케이스 추가하기
배경
- 코드베이스에서 특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 급 반응들을 한 데로 모으는 게 효율적이다.
- 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴을 활용하면 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.
절차, 예시
- 데이터 구조에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 한다.
- 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하며, 이 속성은 true를 반환하게 한다.
- 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
- 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
- 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
- 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
- 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인한다.
let weeksDelinquent = isUnknown(customer) ? 0 : customer.paymentHistory.weeksDelinquentInLastYear
--------------------------------------------------
class UnknownCustomer {
get paymentHistory {
return new NullPaymentHistory();
}
}
class NullPaymentHistory {
get weeksDelinquentInLastYear {
return 0;
}
}
let weeksDelinquent = customer.paymentHistory.weeksDelinquentInLastYear
10.6 어서션 추가하기
배경
- 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있을 수 있다.
- 어서션을 이용해서 코드 자체에 삽입해놓는 것이다.
- 어서션의 존재 유무가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성돼야 한다.
- 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어든다.
절차, 예시
- 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
if (this.discountRate) {
base -= (this.discountRate * base)
}
----------------------------------------
assert(this.discountRate >= 0);
if (this.discountRate) {
base -= (this.discountRate * base)
}
10.7 제어 플래그를 탈출문으로 바꾸기
배경
- 제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말한다.
- return 문을 하나로 유지하기보다는 함수에서 할 일을 다 마쳤다면 그 사실을 return 문으로 명확히 하는게 낫다.
절차, 예시
let found = false;
for (const p of people) {
if (!found) {
if (p === '조커') {
sendAlert();
found = true;
}
if (p === '사루만') {
sendAlert();
found = true;
}
}
}
- 제어 플래그를 사용하는 코드를 함수로 추출할지 고려한다.
- checkForMiscreants(people); function checkForMiscreants(people) { let found = false; for (const p of people) { if (!found) { if (p === '조커') { sendAlert(); found = true; } if (p === '사루만') { sendAlert(); found = true; } } }
- 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다.
- checkForMiscreants(people); function checkForMiscreants(people) { let found = false; for (const p of people) { if (!found) { if (p === '조커') { sendAlert(); **return**; } if (p === '사루만') { sendAlert(); **return**; } } } }
- 모두 수정했다면 제어 플래그를 제거한다.