May 6, 2022 • ☕️ 4 min read
조건부 로직 간소화 - 2
코드베이스에서 특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 그 반응들을 한 데로 모으는 게 효율적이다. 이때 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴을 사용한다.
리팩터링의 대상이 될 속성을 담은 데이터 구조(혹은 클래스)를 컨테이너라고 한자.
class Site {
get customer() { return this._customer; }
}
class Customer {
get name() { ... }
get billingPlan() { ... }
set billingPlan(arg) { ... }
get paymentHistory() { ... }
}
// 클라이언트 1
const aCustomer = site.customer;
let customerName;
if (aCustomer === "미확인 고객") {
customerName = "거주자";
} else {
customerName = aCustomer.name;
}
// 클라이언트 2
const plan = (aCustomer === "미확인 고객") ?
registry.billingPlans.basic
: aCustomer.billingPlans;
// 클라이언트 3
if (aCustomer !== "미확인 고객") {
aCustomer.billingPlan = newPlan;
}
// 클라이언트 4
const weekDelinquent = (aCustomer === "미확인 고객") ?
0
: aCustomer.paymentHistory.weeksDelinquentInLastYear;
클라이언트들은 알려지지 않은 “미확인 고객” 필드를 처리해야 한다.
class Site {
get customer() {
return this._customer === "미확인 고객"
? new UnknownCustomer()
: this._customer;
}
}
class Customer {
get isUnknown() {
return false;
}
}
class UnknownCustomer {
get isUnknown() {
return true;
}
get name() {
return "거주자";
}
get billingPlan() {
return registry.billingPlans.basic;
}
set billingPlan(arg) {
// ...
}
}
function isUnknown(arg) {
if (!(arg instanceof Customer || arg instanceof UnknownCustomer)) {
throw new Error(`잘못된 값과 비교: <${arg}>`);
}
return arg.isUnknown;
}
// 클라이언트 (읽는 경우)
const plan = aCustomer.billingPlan;
// 클라이언트 (쓰는 경우)
aCustomer.billingPlan = newPlan;
class NullPaymentHistory {
get weeksDelinquentInLastYear() {
return 0;
}
}
// 클라이언트 4
const weekDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
class Site {
get customer() {
return (this._customer === "미확인 고객") ? createUnknownCustomer() : this._customer;
}
}
class Customer {
get name() { ... }
get billingPlan() { ... }
set billingPlan(arg) { ... }
get paymentHistory() { ... }
get isUnknown() {
return false;
}
}
function createUnknownCustomer() {
return {
isUnknown: true,
name: "거주자",
billingPlan: registry.billingPlans.basic,
paymentHistory: {
weeksDelinquentInLastYear: 0,
}
}
}
function isUnknown(arg) {
return arg.isUnknown;
}
// 클라이언트 1
customerName = aCustomer.name;
// 클라이언트 2
const plan = aCustomer.billingPlans;
// 클라이언트 3
const weekDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있을 수 있다. 어서션을 이용하면, 코드 자체에 필요한 가정을 항상 명시적으로 기술할 수 있다.
어서션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘못했다는 뜻이다. 어서션은 오류 찾기에 활용될 뿐 아니라, 프로그램이 어떤 상태임을 가정한 채 실행되는지를 다른 개발자에게 알려주는 훌륭한 소통 도구다.
class Customer {
applyDiscount(aNumber) {
return this.discountRate ? aNumber - this.discountRate * aNumber : aNumber;
}
}
이 코드에는 할인율이 항상 양수라는 가정이 깔려 있다.
class Customer {
applyDiscount(aNumber) {
if (!this.discountRate) return aNumber;
else {
assert(this.discountRate >= 0);
return aNumber - this.discountRate * aNumber;
}
}
set discountRate(aNumber) {
assert(null === aNumber || aNumber >= 0);
this._discountRate = aNumber;
}
}
이런 어서션은 오류의 출처를 특정하기 어려울 때 특히 제값을 한다.
제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말하며, 어딘가에서 값을 계산해 제어 플래그에 설정한 후 다른 어딘가의 조건문에서 검사하는 형태로 쓰인다.
제어 플래그는 리팩터링으로 충분히 간소화할 수 있다. 제어 플래그의 주 서식지는 반복문 안으로, 주로 return
, break
, continue
와 함께 사용된다.
// before
let found = false;
for (const p of people) {
if (!found) {
if (p === "조커") {
sendAlert();
found = true;
}
if (p === "사루만") {
sendAlert();
found = true;
}
}
}
// after
checkForMiscreants(people);
function checkForMiscreants(people) {
for (const p of people) {
if (p === "조커") {
sendAlert();
return;
}
if (p === "사루만") {
sendAlert();
return;
}
}
}