리팩터링 2판의 Chatper 06를 보고 정리한 글입니다.
가장 기본적이고 많이 사용해서 제일 먼저 배워야 하는 리팩터링으로 시작한다. 리팩터링의 용어와 그 의미가 어떤식으로 코드에서 나타내는지 알아보자.
이와 반대되는 리팩터링은 함수 인라인하기(뒤에서 다룸)이다.
함수 추출하기는 코드가 무슨일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙이는 것이다. 코드를 추출하는 기준은 아래와 같이 여러개가 있을 수 있다.
저자는 '목적과 구현을 분리'하는 방식이 가장 합리적이라고 생각한다.
목적과 구현을 분리한 예
스몰토크의 흑백 시스템에서 텍스트나 그래픽을 강조하기 위해 색상을 반전시키는 메서드의 이름이 highlight()
(목적)인데 실제 구현은 reverse()
(구현)라는 메서드만을 호출하고 있었음
책 안에서 3개의 예시를 다룬다.
리팩터링 전 예시 코드
function printOwing(invoice) {
let outstanding = 0;
printBanner();
for (const o of invoice.oreders) {
outstanding += o.amount;
}
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
변수가 사용되는 코드 근처로 슬라이드 시키고, 추출할 부분을 새로운 함수로 복사한뒤, 추출한 코드의 원래 자리를 새로 뽑아낸 함수를 호출하는 문장으로 교체하자.
함수 추출하기 적용 후
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function calculateOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.oreders) {
outstanding += o.amount;
}
return outstanding;
}
저자는 마지막에 calculateOutsatnding 함수안의 outstanding을 result로 반환 값의 이름을 바꿈.
값을 반환할 변수가 여러개인 경우 각각을 반환하는 함수 여러개로 만들거나 임시 변수 추출 작업을 임시 변수를 질의 함수로 바꾸거나 변수를 쪼개는 식으로 처리해보자.
또한 리팩터링 진행 시, 중첩함수로 추출할 수 있더라도 최소한 원본 함수와 같은 수준의 문맥으로 먼저 추출하자.
중첩함수로 추출하게 되면 변수를 처리하기가 까다로울 수 있기 때문!
반대되는 리팩터링: 함수 추출하기
함수 본문이 이름만큼 명확한 경우가 있다. 또한 함수 본문 코드를 이름만큼 깔끔하게 리팩터링할 때도 있는데 이럴 때 그 함수를 제거한다.
리팩터링 하기전 코드
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 = [];
line.push(['name', aCustomer.name]);
line.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)
);
}
}
변수 추출하기 적용 후
class Order {
constructor(aRecord) {
this._data = aRecord;
}
get quantity() {
return this._data.quantity;
}
get itemPrice() {
return this._data.itemPrice;
}
get price() {
return this.basePrice - this.quantityDiscount + this.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.basePrice * 0.1, 100);
}
}
객체는 특정 로직과 데이터를 외부와 공유하려 할 때 공유할 정보를 설명해주는 적당한 크기의 문맥이 되어준다. 덩치가 큰 클래스에서 공통 동작을 별도 이름으로 뽑아내서 추상화해두면 그 객체를 다룰 때 쉽게 활용할 수 있어서 매우 유용하다.
반대되는 리팩터링: 변수 추출하기
변수는 함수 안에서 표현식을 가리키는 이름으로 쓰여 대체로 긍정적인 효과를 주지만 그 이름이 원래 표현식과 다를 바 없을 때도 있다. 또한 변수가 주변 코드를 리팩터링하는 데 방해가 되기도 한다.
→ 이런 경우 변수를 인라인하는 것이 좋다.
따로 책에는 나와있지 않음
함수 선언은 실질적으로 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다. 이러한 연결부에서 가장 중요한 요소는 함수의 이름이다.
좋은 이름을 떠올리는데 효과적인 방법으로 주석을 이용해 함수의 목적을 설명해보는 것이다.
함수의 매개변수도 마찬가지로, 함수를 사용하는 문맥을 설정한다. 매개변수를 올바르게 선택하기란 단순히 규칙 몇 개로 표현할 수 없고, 이러한 문제의 정답은 정답이 없기 때문에 어떻게 연결하는 것이 더 나은지 더 잘 이해하게 될 때마다 그에 맞게 코드를 개선할 수 있도록 이 리팩터링과 친숙해져야만 한다.
함수 선언 바꾸기는 '간단한 절차'만으로 충분할 때도 많지만, 더 세분화된 '마이그레이션 절차'가 훨씬 적합한 경우도 많기 때문에 두 가지 절차에 대해서 설명한다.
간단한 절차
변경할 게 둘 이상이면(이름 변경과 매개변수 추가) 나눠서 처리하는 편이 나으므로 각각을 독립적으로 처리하자.
→ 이러다가 문제가 생기면 작업을 되돌리고 '마이그레이션 절차'를 따른다.
마이그레이션 절차
간단한 절차를 이용한 함수 이름 바꾸기 예시
리팩터링 전의 코드
function circum(radius) {
return 2 * Math.PI * radius;
}
함수 선언을 수정하고, circum()을 호출한 곳을 모두 찾아서 circumference()로 바꾼다.
이러한 간단한 절차의 단점은 호출문과 선언문을 한 번에 수정해야 한다는 것인데, 코드에 따라 복잡한 상황에 처하게 되면 마이그레이션 절차를 따르는것이 좋다.
함수 이름 바꾸기 적용 후 코드
function circumference(radius) {
return 2 * Math.PI * radius;
}
마이그레이션 절차를 이용한 함수 이름 바꾸기 예시
리팩터링 전의 코드
function circum(radius) {
return 2 * Math.PI * radius;
}
먼저 함수 본문 전체를 새로운 함수로 추출하고, 수정한 코드를 테스트한 뒤 예전 함수를 인라인 한다. 하나를 변경할 때마다 테스트하면서 한 번에 하나씩 처리하자.
리팩터링 후의 코드
function circum(radius) {
return circumference(radius);
}
function circumference(radius) {
return 2 * Math.PI * radius;
}
매개변수를 속성으로 바꾸는 예시
고객이 뉴잉글랜드에 살고 있는지 확인하는 함수
function inNewEngland(aCustomer) {
return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(aCustomer.address.state);
}
현재는 위와같이 고객을 매개변수로 받는데, 주 식별 코드를 매개변수로로 받도록 리팩터링하여 고객에 대한 의존성을 제거할 것이다.
// 1. 매개변수로 사용할 코드를 변수로 추출
function inNewEngland(aCustomer) {
const stateCode = aCustomer.address.state;
return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}
// 2. 함수 추출하기로 새 함수를 만든다.
function inNewEngland(aCustomer) {
const stateCode = aCustomer.address.state;
return xxNewinEngland(stateCode);
}
function xxNewinEngland(stateCode) {
return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}
// 3. 기존 함수 안에 변수로 추출해둔 입력 매개변수를 인라인한다.
function inNewEngland(aCustomer) {
return xxNewinEngland(aCustomer.address.state);
}
// 4. 함수 인라인하기로 기존 함수의 본문을 호출문들에 집어넣는다.
// 호출문
const newEnglanders = someCustomers.filter((c) => xxNewinEngland(c.address.state));
// 기존 함수를 모든 호출문에 인라인하고, 함수 선언 바꾸기를 다시 한번 적용하여 새 함수의 이름을 기존 함수의 이름으로 바꾼다.
const newEnglanders = someCustomers.filter((c) => inNewEngland(c.address.state));
function inNewEngland(stateCode) {
return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}
데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동한다. 유효범위가 넓어질수록 다루기 어려워지며, 전역 데이터가 골칫거리인 이유도 바로 여기에 있다.
그래서 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독접하는 함수를 만드는 식으로 캡슐화 하는것이 가장 좋은 방법일 때가 많다.
리팩터링 전의 코드
// 전역 변수에 중요한 데이터가 아래와 같이 존재
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
// 참조하는 코드
spaceship.owner = defaultOwner;
// 갱신하는 코드
defaultOwner = { firstName: '레베카', lastName: '파슨스' };
변수 캡슐화하기 적용후 코드
// 1. 기본적인 캡슐화를 위해 가장 먼저 데이터를 읽고 쓰는 함수부터 정의
function getDefaultOwner() {return defaultOwner;}
function setDefaultOwner(arg) {defaultOwner = arg;}
// 3. defaultOwner를 참조하는 코드를 찾아서 방금 만든 게터 함수를 호출하도록 변경
spaceship.owner = getDefaultOwner();
// 대입문은 세터 함수로 변경
setDefaultOwner({ firstName: '레베카', lastName: '파슨스' });
// 변수의 가시 범위를 제한
// defaultOwner.js 파일
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
export function getDefaultOwner() {return defaultOwner;}
export function setDefaultOwner(arg) {defaultOwner = arg;}
이렇게 까지하면 구조로의 접근이나 구조 자체를 다시 대입하는 행위는 제어할 수 있다. 하지만 필드 값을 변경하는 일은 제어할 수 없다.
따라서 변수에 담긴 내용을 변경하는 행위까지 제어할 수 있게 하는 방법으로 2가지가 있다.
export function getDefaultOwner() {return Object.assign({}, defaultOwner);}
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
export function getDefaultOwner() {return new Person(defaultOwnerData)}
export function setDefaultOwner(arg) {defaultOwner = arg;}
class Person {
constructor(data) {
this._lastName = data.lastName;
this._firstName = data.firstName;
}
get lastName() { return this._lastName;}
get firstName() { return this._firstName;}
}
명확한 프로그래밍의 핵심은 이름짓기다. 이름의 중요성은 사용 범위에 영향을 받는데, 맥락으로부터 변수의 목적을 명확히 알 수 있게 하는 것이 중요하다.
저자는 JS와 같은 동적 타입 언어라면 이름 앞에 타입을 드러내는 문자를 붙이는 스타일을 선호한다고 한다. (ex: aCustomer)
리팩터링 하기 전의 코드
let tpHd = 'untitled';
result += `<h1>${tpHd}</h1>`;
tpHd = obj.articleTitle;
리팩터링 이후
// 1. 변수 캡슐화하기로 처리
result += `<h1>${tpHd}</h1>`;
setTitle(obj.articleTitle);
function title() {
return tpHd;
}
function setTitle(arg) {
tpHd = arg;
}
상수 이름 바꾸기 코드 예시
상수의 이름은 캡슐화하지 않고도 복제방식으로 점진적으로 바꿀 수 있다.
const cpyNm = "애크미 구스베리";
// 원본의 이름을 바꾼 후, 원본의 원래 이름과 같은 복제본을 만든다.
const companyName = "애크미 구스베리";
const cpyNm = companyName;
점진적으로 다 바꾼후, 복제본을 삭제한다.
함수에 전달되는 매개변수가 여러개 일때, 이러한 매개변수 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다는 이점을 얻는다.
이러한 리팩터링은 코드를 더 근본적으로 바꿔준다는 데 있다. 데이터 구조가 문제 영역을 훨씬 간결하게 표현하는 새로운 추상개념으로 격상되면서, 코드의 개념적인 그림을 다시 그릴 수도 있다.
리팩터링 전의 코드
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);
}
// 호출하는 코드
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
이와 같이 범위의 시작과 끝을 호출하는 코드, 즉 범위라는 개념은 객체 하나로 묶어 표현하는게 나은 대표적인 예다.
매개변수 객체 만들기 리팩터링 이후 코드
// 1. 묶은 데이터를 표현하는 클래스를 선언
class NumberRange {
constructor(min, max) {
this._data = { min, max };
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
}
// 3. 새로 만든 객체를 ReadingsOutsideRange()의 매개변수로 추가
function readingsOutsideRange(station, min, max, range) {
return station.readings.filter((r) => r.temp < min || r.temp > max);
}
// 호출문에 null을 적어둠(JS는 그냥 냅둬도 됨)
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling, null);
// 4. 아직까지 동작은 하나도 바꾸지 않아서 테스트는 통과
// 5. 온도 범위를 객체 형태로 전달하도록 호출문을 하나씩 바꿈
const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling, range);
// 6. 기존 매개변수를 사용하는 부분을 변경
function readingsOutsideRange(station, range) {
return station.readings.filter((r) => r.temp < range.min || r.temp > range.max);
}
alerts = readingsOutsideRange(station, range);
클래스로 묶으면 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.
클래스의 두드러진 장점은 클라이언트가 객체의 핵심 데이터를 변경할 수 있고, 파생 객체들을 일관되게 관리할 수 있다는 것
사람들은 매달 차 계량기를 읽어서 측정값을 다음과 같이 기록한다고 하자.
reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
// 기본요금 계산하는 코드
// 클라이언트 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);
}
각 클라이언트마다 공통되는 로직이 보이는 경우 기본 요금 계산 함수를 잘 사용할 수 있도록 데이터를 클래스로 만들어 보자.
리팩터링 후의 코드
// 1. 레코드 캡슐화
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;
}
}
// 2. 이미 만들어져 있는 calculateBaseCharge() 옮기기 이 과정에서 메서드 이름을 바꿔도 됨
// Reading 클래스안의 메서드
get baseCharge() { // 기본 요금 계산 함수
return baseRate(this.month, this.year) * this.quantity;
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;
// 클라이언트 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));
// 클라이언트 1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseCharge;
// 클라이언트 2
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
// 변수 인라인화 하기
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
// 3. 세금을 부과할 소비량을 계산하는 코드를 함수로 추출
function taxableChargeFn(aReading) {
return Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = taxableChargeFn(aReading);
// 추출한 함수를 Reading 클래스로 옮기기
get taxableChargeFn() {
return Math.max(0, this.baseCharge - taxThreshold(this.year));
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.taxableCharge
위와 같이 리팩터링을 함으로써 데이터를 필요한 시점에 계산하게 만들고, 클래스를 이용해 데이터와 동작을 한곳에 묶어두게 만들 수 있다.
소프트웨어는 데이터를 입력받아 정보를 도출하고 이러한 정보는 여러곳에서 사용된다. 이러한 도출 작업을 한데로 모아두면 검색과 갱신을 일관된 장소에서 처리할 수 있고 로직 중복도 막을 수 있다.
이러한 경우 변환 함수 혹은 여러 함수를 클래스로 묶기 등의 방법을 사용할 수 있는데, 중요한 점은 원본 데이터를 갱신하느냐에 따라 방법을 선택하는 것이 좋다.
원본 데이터 갱신: 클래스로 묶는 편이 낫다.
차를 수돗물 처럼 제공하는 서비스의 매달 사용자가 마신 차의 양을 측정하는 상황 가정
리팩터링 전 코드
reading = {
cutomer: 'ivan',
quantity: 10,
month: 5,
year: 2017,
};
// 클라이언트 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. 입력 객체를 그대로 복사해 반환하는 변환 함수를 만든다.
function enrichReading(original) {
const result = _.cloneDeep(original);
return result;
}
// 2. 변경하려는 계산 로직 중 하나를 고른다.
// 클라이언트 3
const rawReading = acquireReading(); // 미가공 측정값
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
⬇
// calculateBaseCharge()를 부가 정보를 덧붙이는 코드 근처로 옮김
// 정보를 추가해 반환하는 함수의 경우 원본 측정값 레코드는 변경하는 지 확인하는 테스트 코드를 작성하면 좋음
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
return result;
}
// 기존 코드 변경
const basicChargeAmount = calculateBaseCharge(aReading);
⬇
const basicChargeAmount = aReading.baseCharge;
// 4. 세금 부과할 소비량 계산 코드에서 먼저 변환 함수부터 끼어넣자.
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
// 계산 코드를 변환함수로 옮기자.
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
return result;
}
// 새로 만든 필드를 사용하도록 원본 코드를 수정한다.
...
const taxableCharge = aReading.taxableCharge;
서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나누는 방법을 모색해보자.
→ 코드를 수정해야 할 때 두 대상을 동시에 생각할 필요없이 하나에만 집중할 수 있게해줌
가장 대표적인 예: 컴파일러(토큰화 → 파싱 후 구문트리 → 구문트리 변환 → 목적 코드 생성)
상품의 결제 금액을 계산하는 코드
리팩터링 전의 코드
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 price = applyShipping(basePrice, shippingMethod, quantity, discount);
return price;
}
// 두 번째 단계 처리함수
function applyShipping(basePrice, shippingMethod, quantity, discount) {
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 = {}; // 중간 데이터 구조
const price = applyShipping(priceData, basePrice, shippingMethod, quantity, discount);
return price;
}
function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
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, quantity, discount }; // 중간 데이터 구조
const price = applyShipping(priceData, shippingMethod);
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, quantity, 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;
}