//리팩토링 전
function calculateCharge(date, quantity, plan) {
let charge = 0;
if (!date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else charge = quantity * plan.regularRate + plan.regularServiceCharge;
return charge;
}
//리팩토링 후
function calculateCharge(date, quantity, plan) {
return isSummer() ? summerCharge() : regularCharge();
function isSummer() {
return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd)
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
}
복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가능 흔한 원흉에 속한다. 조건을 검사하고 그 결과에 따른 동작을 표현한 코드는 무슨 일이 일어나는지는 이야기해주지만 왜 일어나는지는 제대로 말해주지 않는다. 코드를 부위별로 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔주면 전체적인 의도가 더 확실이 드러난다.
//리팩토링 전
function disabilityAmount(employee) {
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;
return 1;
}
//리팩토링 후
function disabilityAmount(employee) {
return isNotEligibleForDisability(employee) ? 0 : 1;
}
function isNotEligibleForDisability(employee) {
return employee.seniority < 2 || employee.monthsDisabled > 12 || employee.isPartTime;
}
같은 일을 하는 코드라면 조건 검사도 하나로 통합하는 게 낫다. 조건부 코드를 통합하는 중요하면 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 내가 하려는 일이 더 명확해지고 함수 추출하기까지 이어질 가능성이 높다. 복잡한 조건식을 함수로 추출하면 코드의 의도가 훨씬 분명하게 드러나느 경우가 많다.
//리팩토링 전
function adjustedCapital(instrument) {
let result = 0;
if (instrument.capital > 0) {
if (instrument.interestRate > 0 && instrument.duration > 0) {
result =
(instrument.income / instrument.duration) *
anInstrument.adjustmentFactor;
}
}
return result;
}
//리팩토링 후
function adjustedCapital(instrument) {
if (!isEligibleForAdjustedCapital()) {
return 0
}
return (instrument.income / instrument.duration) * anInstrument.adjustmentFactor;
}
function isEligibleForAdjustedCapital(instrument) {
return instrument.capital > 0 &&
instrument.interestRate > 0 &&
instrument.duration > 0;
}
조건문은 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지느 형태와, 한쪽만 정상인 형태 두가지로 쓰인다. 두 형태는 의도하는 바가 서로 다르므로 그 의도가 코드에 드러나야 하느데 두 경로 모두 정상 동작이라면 ifd와 else절을, 한쪽만 정상이라면 비정상 조건을 if에서 검사하는 방식을 사용하는 것이 좋다. 두 번째 검사 형태를 흔히 보호 구문이라고 하는데 중첩 조건문을 보호 구문으로 바꾸기 리팩터링의 핵심은 의도를 부각하는데 있다.
//리팩토링 전
function plumages(birds) {
let map = birds.map((b) => [b.name, plumage(b)]);
let map1 = new Map(map);
return map1;
}
function speeds(birds) {
return new Map(birds.map((b) => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return 'average';
case 'AfricanSwallow':
return bird.numberOfCoconuts > 2 ? 'tired' : 'average';
case 'NorwegianBlueParrot':
return bird.voltage > 100 ? 'scorched' : 'beautiful';
default:
return 'unknown';
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * bird.numberOfCoconuts;
case 'NorwegianBlueParrot':
return bird.isNailed ? 0 : 10 + bird.voltage / 10;
default:
return null;
}
//리팩토링 후
class Bird {
constructor(bird) {
Object.assign(this, bird);
}
get name() {
return this.name;
}
get plumage() {
return 'unknown';
}
get airSpeedVelocity() {
return null;
}
}
class EuropeanSwallow extends Bird {
get plumage() {
return 'average';
}
get airSpeedVelocity() {
return 35;
}
}
class AfricanSwallow extends Bird {
get plumage() {
return this.numberOfCoconuts > 2 ? 'tired' : 'average';
}
get airSpeedVelocity() {
return 40 - 2 * this.numberOfCoconuts;
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return this.voltage > 100 ? 'scorched' : 'beautiful'
}
get airSpeedVelocity() {
return this.isNailed ? 0 : 10 + this.voltage / 10;
}
}
const createBird = (bird) => {
switch (bird.type) {
case 'EuropeanSwallow':
return new EuropeanSwallow(bird);
case 'AfricanSwallow':
return new AfricanSwallow(bird);
case 'NorwegianBlueParrot':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
function plumages(birds) {
return new Map(
birds.map((b) => createBird(b))
.map((b) => [b.name, b.plumage])
);
}
function speeds(birds) {
return new Map(
birds.map((b) => createBird(b))
.map((b) => [b.name, b.airSpeedVelocity])
);
}
조건부 로직을 직관적으로 구조화할 방법을 항상 고민해야 하는데 클래스와 다형성을 이용하면 더 확실하게 분리할 수 있다. 특수한 상황을 다루는 로직들을 기본 동작에서 분리하기 위해 상속과 다형성을 이용할 수 있다.
//리팩토링 전
export class Site {
constructor(customer) {
this._customer = customer;
}
get customer() {
return this._customer;
}
}
export class Customer {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
get billingPlan() {
//
}
set billingPlan(arg) {
//
}
get paymentHistory() {
//
}
}
export function customerName(site) {
const aCustomer = site.customer;
...
let customerName;
if (aCustomer === 'unknown') customerName = 'occupant';
else customerName = aCustomer.name;
return customerName;
}
//리팩토링 후
export class Customer {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
get billingPlan() {
//
}
set billingPlan(arg) {
//
}
get paymentHistory() {
//
}
}
class UnknownCustomer extends Customer {
get name() {
return 'occupant';
}
}
export class Site {
constructor(customer) {
this._customer = customer;
}
get customer() {
return this._customer === 'unknown' ? new UnknownCustomer() : new Customer(this._customer);
}
}
export function customerName(site) {
const aCustomer = site.customer;
...
customerName = aCustomer.name;
return customerName;
}
특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 그 반응들을 한 데로 모으는게 효율적이다. 특수한 경우에 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴이라는 것이 있는데, 단순히 데이터를 읽기만 한다면 반환할 값들ㅇ르 담은 리터럴 객체 형태롤 준비하면 되고 그 이상의 동작을 수행한다면 필요한 메서드를 담은 객체를 생성하면 된다.
//리팩토링 전
class Customer {
constructor() {
this.discountRate = 0;
}
applyDiscount(number) {
return this.discountRate ? number - this.discountRate * number : number;
}
}
//리팩토링 후
import { strict as assert } from 'node:assert';
class Customer {
constructor() {
this.discountRate = 0;
}
applyDiscount(number) {
assert(number >= 0);
return this.discountRate ? number - this.discountRate * number : number;
}
}
특정 조건이 참일 때만 제대로 동작하느 코드 영역이 있을 수 있는데 어셔션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘 못했다는 뜻이다. 어서션은 프로그램이 어떤 상태임을 가정한 채 실행되는지를 다른 갭라자에게 알려주는 훌륭한 소통 도구이다.
//리팩토링 전
for (const p of people) {
if (!found) {
if (p === 'Don') {
sendAlert();
found = true;
}
}
//리팩토링 후
for (const p of people) {
if (p === 'Don') {
sendAlert();
break;
}
}
if(people.includes('Don')) {
sendAlert();
}
제어 플러그란 코드의 동작을 변경하는 데 사용되는 변수를 말하는데 제어 플러그보다는 함수에서 할 일을 마쳤다면 그 사실을 return 문으로 명확하게 알리는 편이 낫다.