function printOwing(invoice) {
let outstanding = 0;
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount
}
// 마감일(dueDate)을 기록한다.
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
// 세부 사항을 출력한다.
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`)
}
해당 코드를 잘라내서 새 함수에 붙이고, 원래 자리에 새 함수 호출문을 넣는다.
function printOwing(invoice) {
let outstanding = 0;
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
// 마감일(dueDate)을 기록한다.
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
printDetails();
function printDetails() {
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`)
}
}
function printOwing(invoice) {
let outstanding = 0;
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
// 마감일(dueDate)을 기록한다.
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
printDetails();
function printDetails() {
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`)
}
}
변수를 사용하지만 다른 값을 다시 대입하지는 않을 때는, 지역 변수들을 그냥 매개변수로 넘긴다.
function printOwing(invoice) {
let outstanding = 0;
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
recordDueDate(invoice); // 마감일을 기록한다.
printDetails(invoice, outstanding); // 세부사항을 출력한다.
}
function printDetails(invoice, outstanding) {
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`)
}
function recordDueDate(invoice) {
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
function printOwing(invoice) {
const outstanding = calculateOutstanding(invoice); // 미해결 채무(outstanding)를 계산한다.
recordDueDate(invoice); // 마감일을 기록한다.
printDetails(invoice, outstanding); // 세부사항을 출력한다.
}
function calculateOutstanding(invoice) {
let result = 0;
for (const o of invoice.orders) {
result += o.amount;
}
return result
}
function printDetails(invoice, outstanding) {
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`)
}
function recordDueDate(invoice) {
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
추출할 코드를 다르게 재구성하는 방향으로 처리한다.
기본적으로 함수가 값 하나만을 방식이 심플하므로 각각을 반환하는 함수 여러 개로 만든다.
굳이 한 함수에서 여러 값을 반환해야 한다면 값들을 레코드로 묶어서 반환해도 되지만, 임시변수를 질의 함수로 바꾸거나 변수를 쪼개는 식으로 처리하면 좋다.
function reportLines(aCustomer) {
const lines = [];
gatherCustomerData(lines, aCustomer);
return lines;
}
function gatherCustomerData(out, aCustomer) {
out.push(["name", aCustomer.name]);
out.push(["location", aCustomer.location]);
function reportLines(aCustomer) {
const lines = [];
lines.push(["name", aCustomer.name]);
lines.push(["location", aCustomer.location]);
return lines;
}
class Order {
constructor(aRecord) {
this._data = aRecord;
}
get quantity() {return this._data.quantity;}
get itemPrice() {return this._data.itemPrice;}
get price() {
return this.quantity * this.itemPrice
- Math.max(0, this.quantity - 500) * this.itemPrice * 0.05
+ Math.min(this.quantity * this.itemPrice * 0.1, 100);
}
클래스 안에서 추출하려는 이름이 가격을 계산하는 price() 메서드의 범위를 넘어 주문을 표현하는 Order클래스 전체에 적용된다. 이처럼 클래스 전체에 영향을 줄 때 변수가 아닌 메서드로 추출한다.
class Order {
constructor(aRecord) {
this._data = aRecord;
}
get quantity() {return this._data.quantity;}
get itemPrice() {return this._data.itemPrice;}
get price() {
return basePrice - quantityDiscount + shipping;
}
get basePrice() {return this.quantity * this.itemPrice;}
get quantityDiscount() {return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;}
get shipping() {return Math.min(this.quantity * this.itemPrice * 0.1, 100);}
let reservations = [];
function addReservation(customer) {
zz_addReservation(customer);
}
function zz_addReservation(customer) {
reservations.push(customer);
}
자바스크립트로 프로그래밍한다면, 호출문을 변경하기 전에 어서션을 추가하여 호출 하는 곳에서 새로 추가한 매개변수를 실제로 사용하는지 확인한다.
let reservations = [];
function addReservation(customer, isPriority) {
zz_addReservation(customer, isPriority);
}
function zz_addReservation(customer, isPriority) {
assert(isPriority === true || isPriority === false);
reservations.push(customer);
}
이렇게 해두면 호출문을 수정하는 과정에서 실수로 새 매개변수를 빠뜨린 부분을 찾는데 도움이 된다.
let reservations = [];
function addReservation(customer, isPriority) {
reservations.push(customer);
}
function inNewEngland(aCustomer) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(aCustomer.address.state);
}
const newEnglander = someCustomers.filter(c => inNewEngland(c));
주 식별 코드를 매개변수로 받도록 리팩터링 -> 고객에 대한 의존성이 제거되어 더 넓은 문맥에 활용가능
function isNewEngland(stateCode) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
const newEnglander = someCustomers.filter(c => isNewEngland(c.address.state));
let defaultOwner = { firstName: '마틴', lastName: '파울러' }
defaultOwner = { firstName: '레베카', lastName: '파슨스' }
spaceship.owner = defaultOwner;
데이터를 읽고 쓰는 함수부터 정의한다.(게터,세터) 그런 다음 게터와 세터함수를 쓰도록 바꾼다.
let defaultOwner = { firstName: '마틴', lastName: '파울러' }
export function getDefaultOwner() {return defaultOwner;}
export function setDefaultOwner() {defaultOwner = arg;}
setDefaultOwner()
spaceship.owner = getDefaultOwner();
result += `<h1>${title()}</h1>`;
setTitle(obj['articleTitle']);
function title() {return tpHd;}
function setTitle(arg) {tpHd = arg;}
const cpyNm = '애크미 구스베리';
먼저 원본의 이름을 바꾼 후, 원본의 원래 이름(기존 이름)과 같은 복제본을 만든다.
const companyNm = '애크미 구스베리';
const cpyNm = companyNm;
이제 기존 이름(복제본)을 참조하는 코드들을 새 이름으로 점진적으로 바꿀 수 있다. 다 바꿨다면 복제본을 삭제한다.
const station = {
name: "ZB1",
readings: [
{temp: 47, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:20"},
{temp: 58, time: "2016-11-10 09:30"},
{temp: 53, time: "2016-11-10 09:40"},
{temp: 51, time: "2016-11-10 09:50"},
]
}
// 정상 범위를 벗어난 측정값을 찾는 함수
function readingsOutsideRange(station, min, max) {
return station.readings.filter(r => r.temp < min || r.temp > max);
}
alters = readingsOutsideRange(
station,
operationPlan.temperatureFloor, // 최저 온도
operationPlan.temperatureCeiling // 최고 온도
);
묶은 데이터를 표현하는 클래스부터 선언한다.
const station = {
name: "ZB1",
readings: [
{temp: 47, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:20"},
{temp: 58, time: "2016-11-10 09:30"},
{temp: 53, time: "2016-11-10 09:40"},
{temp: 51, time: "2016-11-10 09:50"},
]
}
class NumberRange {
constructor(min, max) {
this._data = {min: min, max: max};
}
get min() {return this._data.min;}
get max() {return this._data.max;}
}
// 정상 범위를 벗어난 측정값을 찾는 함수
function readingsOutsideRange(station, range) {
return station.readings.filter(r => r.temp < range.min || r.temp > range.max);
}
const range = new NumberRange(operationPlan.temperatureFloor, operationPlan.temperatureCeiling)
alters = readingsOutsideRange(station, range);
클래스를 만들어두면 관련 동작들을 이 클래스로 옮길 수 있다는 이점이 생긴다.
const station = {
name: "ZB1",
readings: [
{temp: 47, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:20"},
{temp: 58, time: "2016-11-10 09:30"},
{temp: 53, time: "2016-11-10 09:40"},
{temp: 51, time: "2016-11-10 09:50"},
]
}
class NumberRange {
constructor(min, max) {
this._data = {min: min, max: max};
}
get min() {return this._data.min;}
get max() {return this._data.max;}
// 온도가 허용 범위 안에 있는지 검사하는 메서드
contains(arg) {return (arg >= this.min && arg<= this.max)}
}
// 정상 범위를 벗어난 측정값을 찾는 함수
function readingsOutsideRange(station, range) {
return station.readings.filter(r => !range.contains(r.temp));
}
const range = new NumberRange(operationPlan.temperatureFloor, operationPlan.temperatureCeiling)
alters = readingsOutsideRange(station, range);
// 클라이언트1
const aReading = acquireReading()
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity
// 클라이언트2
const aReading = acquireReading()
const base = baseRate(aReading.month, aReading.year) * aReading.quantity
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
// 클라이언트3
const aReading = acquireReading()
const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity
}
앞선 두 클라이언트(1,2)도 클라이언트3이 쓰는 함수를 사용하도록 고치려고 한다. 하지만 이렇게 최상위 함수로 두면 못 보고 지나치기 쉽다는 문제가 있다. 나라면 이런 함수를 데이터 처리 코드 가까이에 둔다. 그러기 위한 좋은 방법으로, 데이터를 클래스로 만들 수 있다.
class Reading {
constructor(data) {
this._customer = data.customer;
this._quantity = data.quantity;
this._month = data.month;
this._year = data.year;
}
get customer() {return this._customer;}
get quantity() {return this._quantity;}
get month() {return this._month;}
get year() {return this._year;}
}
이미 만들어져 있는 calculateBaseCharge()부터 옮기자. 새 클래스를 사용하려면 데이터를 얻자마자 객체로 만들어야 한다.
// 클라이언트3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
그런 다음 calcualteBaseCharge()를 새로만든 클래스에 옮긴다.
class Reading {
constructor(data) {
this._customer = data.customer;
this._quantity = data.quantity;
this._month = data.month;
this._year = data.year;
}
get customer() {return this._customer;}
get quantity() {return this._quantity;}
get month() {return this._month;}
get year() {return this._year;}
get baseCharge() {return baseRate(this.month, this.year) * this.quantity;}
}
클래스가 적용된 클라이언트3
// 클라이언트3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;
세금계산 하는 함수도 역시 클래스메서드로 변환할 수 있다.
class Reading {
constructor(data) {
this._customer = data.customer;
this._quantity = data.quantity;
this._month = data.month;
this._year = data.year;
}
get customer() {return this._customer;}
get quantity() {return this._quantity;}
get month() {return this._month;}
get year() {return this._year;}
get baseCharge() {return baseRate(this.month, this.year) * this.quantity;}
get texableCharge() {return MMath.max(0, this.baseCharge - taxThreshold(this.year))
}
파생 데이터 모두를 필요한 시점에 계산되게 만들었으니 저장된 데이터를 갱신하더라도 문제가 생기지 않는다.
// 클라이언트3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.texableCharge;
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
result.texableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
return result;
}
// 클라이언트3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.texableCharge;
테스트에 성공하면 texableCharge 변수도 인라인한다.
상품의 결제 금액을 계산하는 코드로 시작해보자
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount = (Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;)
const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
const price = basePrice - discount + shippingCost;
return price;
}
간단한 예지만 가만 보면 계산이 두 단계로 이뤄짐을 알 수 있다. 앞의 몇 줄은 상품 정보를 이용해서 결제 금액 중 상품 가격을 계산한다. 반면 뒤의 코드는 배송 정보를 이용하여 결제 금액 중 배송비를 계산한다. 나중에 상품 가격과 배송비 계산을 더 복잡하게 만드는 변경이 생긴다면 이 코드는 두 단계로 나누는 것이 좋다.
중간 데이터구조를 만들어서 두 단계가 연계될 수 있도록 만든다.
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount = (Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;)
const priceData = {basePrice: basePrice};
const price = applyShipping(priceData, shippingMethod, quantity, discount);
return price;
}
function applyShipping(priceData, shippingMethod, quantity, discount) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
const price = priceData.basePrice - discount + shippingCost;
return price;
}
quantity의 경우 첫 번째 단계에서 사용하지만 거기서 생성된 것은 아니다. 그래서 매개변수로 나둬도 되지만 중간 데이터에 옮겨보자. discount도 마찬가지다.
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount = (
Math.max(quantity - product.discountThreshold, 0)
* product.basePrice
* product.discountRate;
)
const priceData = {
basePrice: basePrice,
quantity: quantity,
discount: discount
};
const price = applyShipping(priceData, shippingMethod, quantity, discount);
return price;
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
const price = priceData.basePrice - priceData.discount + shippingCost;
return price;
}
매개변수들을 모두 처리하면 중간 데이터 구조가 완성된다. 이제 첫 번째 단계 코드를 함수로 추출하고 이 데이터 구조를 반환하게 한다.
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod); // 변수 인라인
}
function calculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount = (
Math.max(quantity - product.discountThreshold, 0)
* product.basePrice
* product.discountRate;
)
return {basePrice: basePrice, quantity: quantity, discount: discount}; //변수 인라인
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost; // 변수인라인
}