April 26, 2022 • ☕️ 5 min read
조건부 로직 간소화 - 1
복잡한 조건부 로직은 프로그램을 복잡하게 만든다. 코드를 부위별로 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔주면 전체적인 의도가 더 확실히 드러난다.
// 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 = quantity * plan.regularRate + plan.regularServiceCharge;
}
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;
}
비교하는 조건은 다르지만 그 결과로 수행하는 동작은 똑같은 코드들이 있다면 조건 검사도 하나로 통합하는 것이 좋다. ‘and’ 연산자와 ‘or’ 연산자를 사용하면 여러 개의 비교 로직을 하나로 합칠 수 있다.
조건부 코드를 통합하는 것이 중요한 이유는 두 가지다.
// before
function disabilityAmount(anEmployee) {
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
}
// after
function disabilityAmount(anEmployee) {
if (isNotEligibleForDisability()) return 0;
}
function isNotEligibleForDisability() {
return (
anEmployee.seniority < 2 ||
anEmployee.monthDisabled > 12 ||
anEmployee.isPartTime
);
}
조건문은 주로 두 가지 형태로 쓰인다. 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태와, 한쪽만 정상인 형태다.
두 경로 모두 정상 동작이라면 if
와 else
절을 사용한다. 한쪽만 정상이라면 비정상 조건을 if
에서 검사한 다음, 조건이 참이면(비정상이면) 함수에서 빠져나온다. 두 번째 검사 형태를 흔히 보호 구문(guard clause) 이라고 한다.
중첩 조건문을 보호 구문으로 바꾸는 리팩터링의 핵심은 의도 부각에 있다. if-then-else
구조를 사용할 때는 if
와 else
절에 똑같은 무게를 두어, 코드를 읽는 이에게 양 갈래가 똑같이 중요하다는 뜻을 전달한다. 반면, 보호 구문은 이 일이 일어나면 무언가 조치를 취한 후 함수에서 빠져나온다는 것을 알 수 있어야 한다.
// before
function payAmount(employee) {
let result;
if (employee.isSeparated) {
// 퇴사한 직원인가?
result = { amount: 0, reasonCode: "SEP" };
} else {
if (employee.isRetired) {
// 은퇴한 직원인가?
result = { amount: 0, reasonCode: "RET" };
} else {
// 급여 계산 로직
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).edit();
sed.do.eusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result = someFinalComputation();
}
}
}
// after
function payAmount(employee) {
if (employee.isSeparated) return { amount: 0, reasonCode: "SEP" };
if (employee.isRetired) return { amount: 0, reasonCode: "RET" };
// 급여 계산 로직
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).edit();
sed.do.eusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
return someFinalComputation();
}
복잡한 조건부 로직을 클래스와 다형성을 이용하여 분리할 수 있다.
타입을 여러 개 만들고 각 타입이 조건부 로직을 자신만의 방식으로 처리하도록 구성하는 방법이 있다.
또는 기본 동작을 위한 case문과 그 변형 동작으로 구성된 로직을 떠올릴 수 있다. 기본 동작 로직을 슈퍼클래스로 넣고, 변형 동작을 뜻하는 case들을 각각의 서브클래스로 만든다.
// 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 ? "지쳤다" : "보통이다";
default:
return "알 수 없다";
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
case "유럽 제비":
return 35;
case "아프리카 제비":
return 40 - 2 * bird.numberOfCoconuts;
default:
return null;
}
}
// after
function plumages(birds) {
return new Map(
birds.map((b) => createBird(b)).map((bird) => [bird.name, bird.plumage])
);
}
function speeds(birds) {
return new Map(
birds
.map((b) => createBird(b))
.map((bird) => [bird.name, bird.airSpeedVelocity])
);
}
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 * this.numberOfCoconuts;
}
}
function createBird(bird) {
switch (this.type) {
case "유럽 제비":
return new EuropeanSwallow(bird);
case "아프리카 제비":
return new AfricanSwallow(bird);
default:
return new Bird(bird);
}
}