//기본형 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 = []; out.push(["name",aCustomer.name]); out.push(["location",aCustomer.location]); return lines; }
function printOwing(invoice){ let outstanding = 0; console.log("******************"); console.log("**** 고객 채무 ****") console.log("******************"); //미해결 채무 for(const o of invoice.orders){ outstanding += o.amount; } //마감일 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; printBanner(); //미해결 채무 for(const o of invoice.orders){ outstanding += o.amount; } //마감일 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 printBanner() { console.log("******************"); console.log("**** 고객 채무 ****"); console.log("******************"); } }
리팩터링란에존재
예시: 지역변수를사용할때
지역변수를사용하지만다른값을다시대입하지않을때
//지역 변수를 사용할 때 function printOwing(invoice){ let outstanding = 0; printBanner(); //미해결 채무 for(const o of invoice.orders){ outstanding += o.amount; } //마감일 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 printBanner() { console.log("******************"); console.log("**** 고객 채무 ****"); console.log("******************"); } }
//중첩 함수가 지원되지 않는 언어는 이렇게 넣어줘야 할 것 printDetails(invoice, outstanding); function printDetails(invoice, outstanding) { console.log(`고객명 : ${invoice.customer}`); console.log(`채무액 : ${outstanding}`); console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`); }
recordDueDate(invoice); //중첩 함수가 지원되지 않는 언어는 이렇게 넣어줘야 할 것 printDetails(invoice, outstanding); function recordDueDate(invoice) { const today = Clock.today; invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30); }
예시: 지역변수의값을변경할때
이럴경우매개변수가들어가는지역변수에대한임시변수를새로하나만들어그변수를사용한다.
let outstanding = 0; printBanner(); //미해결 채무 for(const o of invoice.orders){ outstanding += o.amount; }
printBanner(); //문장 슬라이드로 사용되는 곳 근처로 옮김 let outstanding = 0; for(const o of invoice.orders){ outstanding += o.amount; }
printBanner(); let outstanding = 0; for(const o of invoice.orders){ outstanding += o.amount; } //추출할 부분을 새로운 함수로 복사 function calculateOutstanding(invoice){ let outstanding = 0; for(const o of invoice.orders){ outstanding += o.amount; } return outstanding; }
printBanner(); //추출한 함수가 반환한 값을 원래 변수에 저장한다. let outstanding = calculateOutstanding(invoice); function calculateOutstanding(invoice){ let outstanding = 0; for(const o of invoice.orders){ outstanding += o.amount; } return outstanding; }
//마지막으로 반환 값의 이름을 코딩 스타일에 맞게 변경 const outstanding = calculateOutstanding(invoice); function calculateOutstanding(invoice){ let result = 0; for(const o of invoice.orders){ result += o.amount; } return result; }
function printOwing(invoice){ printBanner(); const outstanding = calculateOutstanding(invoice); recordDueDate(invoice); printDetails(invoice, outstanding); function calculateOutstanding(invoice){ let result = 0; for(const o of invoice.orders){ result += o.amount; } return result; } function recordDueDate(invoice) { const today = Clock.today; invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30); } function printDetails(invoice, outstanding) { console.log(`고객명 : ${invoice.customer}`); console.log(`채무액 : ${outstanding}`); console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`); } function printBanner() { console.log("******************"); console.log("**** 고객 채무 ****"); console.log("******************"); } }
function statement(invoice, plays) {
return renderPlainText(invoice, plays) //본문 전체를 별도 함수로 추출
}
function renderPlainText(invoice, plays) {
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
for(let perf of invoice.performances){
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
result += `총액: ${usd(totalAount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;
function totalAount(){
let result = 0;
for(let perf of invoice.performances){
result += amountFor(perf);
}
return result;
}
…. 이하 추출한 본문
function statement(invoice, plays) {
const statementData = {};
return renderPlainText(statementData, invoice, plays) //중간데이터 구조를 인수로 전달
}
function renderPlainText(data, invoice, plays)
renderPlainText() 인수 invoice 중간 데이터구조로 옮기기
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances;
return renderPlainText(statementData, /*invoice,*/ plays) //필요없어진 인수 삭제
}
function renderPlainText(data, plays) {
let result = `청구 내역 (고객명 : ${data.customer})\n`;
for(let perf of data.performances){
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
result += `총액: ${usd(totalAount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;
function totalAount(){
데이터 복사
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainText(statementData, plays)
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);//얕은 복사 수행
return result;
}
}
이렇게 데이터를 복사해서 사용하는 이유는 데이터를 불변으로 취급하기 위함
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainText(statementData, plays)
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);
result.play = playFor(result);//중간 데이터에 연극 정보를 저장
return result;
}
function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
return plays[aPerformance.playID];
}
}
옮기면서renderPlainText()에서 playFor()사용했던부분전부변경
amountFor()도비슷하기옮기기
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainText(statementData, plays)
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);
result.play = playFor(result);//중간 데이터에 연극 정보를 저장
result.amount = amountFor(result);
return result;
}
function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
let result = 0;
switch(aPerformance.play.type){
case "tragedy" :
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" :
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
}
return result;
}
}
volumeCreditsFor()옮기기
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainText(statementData, plays)
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);
result.play = playFor(result);//중간 데이터에 연극 정보를 저장
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
let result = 0;
switch(aPerformance.play.type){
case "tragedy" :
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" :
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
}
return result;
}
function volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("comedy" === aPerformance.play.type)
volumeCredits += Math.floor(aPerformance.audience/5);
return volumeCredits;
}
}
마지막총합을구하는부분옮기기
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return renderPlainText(statementData, plays)
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);
result.play = playFor(result);//중간 데이터에 연극 정보를 저장
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
let result = 0;
switch(aPerformance.play.type){
case "tragedy" :
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" :
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
}
return result;
}
function volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("comedy" === aPerformance.play.type)
volumeCredits += Math.floor(aPerformance.audience/5);
return volumeCredits;
}
function totalAmount(data){
return data.performances //반복문을 파이프라인으로 바꿈
.reduce((total, p) => total + p.volumeCredits, 0 );
}
function totalVolumeCredits(data){
return data.performances //반복문을 파이프라인으로 바꿈
.reduce((total, p)=>total+p.volumeCredits, 0);
}
}
//statement.js 파일이라 가정
function statement(invoice, plays) {
return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays){
return renderHtml(createStatementData(invoice,plays));
//중간데이터 생성 함수를 공유한다.
}
function renderHtml(data, plays) {
let result = `<h1>청구 내역 (고객명 : ${data.customer})</h1>\n`;
result += "<table>\n";
result += "<tr><th>연극</th><th>좌석 수</th><th>금액</th></tr>"
for(let perf of data.performances){
result += `<tr><td>${perf.play.name}</td><td>(${perf.audience}석)</td><td>${usd(perf.amount)}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>총액: <em>${usd(data.totalAmount)}</em></p>\n`;
result += `<p>적립 포인트: <em>${data.totalVolumeCredits}</em>점</p>\n`;
return result;
}
function renderPlainText(data, plays) {
let result = `청구 내역 (고객명 : ${data.customer})\n`;
for(let perf of data.performances){
result += `${perf.play.name} : ${usd(perf.amount)} (${perf.audience}석)\n`;
}
result += `총액: ${usd(data.totalAmount)}\n`;
result += `적립 포인트: ${data.totalVolumeCredits}점\n`;
return result;
}
function usd(aNumber){
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(aNumber/100);
}
//statement.js 파일이라 가정 끝
//createStatementData.js 별도 파일이라 가정
function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return statementData;
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
function playFor(aPerformance){
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
let result = 0;
switch(aPerformance.play.type){
case "tragedy" :
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" :
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
}
return result;
}
function volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("comedy" === aPerformance.play.type)
volumeCredits += Math.floor(aPerformance.audience/5);
return volumeCredits;
}
function totalAmount(data){
return data.performances
.reduce((total, p) => total + p.amount, 0 );
}
function totalVolumeCredits(data){
return data.performances
.reduce((total, p)=>total+p.volumeCredits, 0);
}
}
//createStatementData.js 별도 파일이라 가정 끝
전체로직을구서하는요소들이뚜렸하게분리되면서파악이쉬워졌다.
그리고모듈화하면서 html버전도재사용으로쉽게만들었다.
프로그래밍에선명료함이중요하다.
1.8 다형성을활용해계산코드재구성하기
amountFor()함수를보면연극장르에따라계산방식이달라진다.
이런조건부로직은코드수정획수가늘어날수록골칫거기로전락한다.
"조건부로직을다형성으로바꾸기"
이리팩터링은조건부코드한덩어리를다형성을활용하는방식으로바꿔준다.
이리팩터링을적용하려면상속계층부터정의해야한다.
공연료계산기만들기
여기서 핵심은 각 공연 정보를 중간 데이터 구조에 채워주는 enrichPerformance()함수
이 함수는 조건부 로직을 포함한 함수인 amountFor()와 volumeCreditsFor()를 호출한다.
두 함수를 전용 클래스로 옮기는 작업을 수행한다.
createStatementData()함수속
...
function enrichPerformance(aPerformance){
const calculator = new PerformanceCalculator(aPerformance); //공연료 계산기 생성
const result = Object.assign({}, aPerformance);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
최상위
class PerformanceCalculator{
constructor(aPerformance){
this.performance = aPerformance;
}
}
함수들을 계산기로 옮기기
//createStatementData.js 별도 파일이라 가정
class PerformanceCalculator{
constructor(aPerformance, aPlay){
this.performance = aPerformance;
this.play = aPlay;
}
get amount(){
let result = 0;
switch(this.play.type){
case "tragedy" :
result = 40000;
if(this.performance.audience > 30){
result += 1000 * (this.performance.audience - 30);
}
break;
case "comedy" :
result = 30000;
if(this.performance.audience > 30){
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
break;
default: throw new Error(`알수 없는 장르: ${this.play.type}`);
}
return result;
}
get volumeCredits(){
let result = 0;
result += Math.max(this.performance.audience -30,0);
if("comedy" === this.play.type)
result += Math.floor(this.performance.audience/5);
return result;
}
}
function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return statementData;
function enrichPerformance(aPerformance){
const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); //공연 정보를 계산기로 전달
const result = Object.assign({}, aPerformance);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}
function playFor(aPerformance){
return plays[aPerformance.playID];
}
function totalAmount(data){
return data.performances
.reduce((total, p) => total + p.amount, 0 );
}
function totalVolumeCredits(data){
return data.performances
.reduce((total, p)=>total+p.volumeCredits, 0);
}
}
//createStatementData.js 별도 파일이라 가정 끝
공연료 계산기를 다형성 버전으로 만들기
클래스에 로직을 담았으니 다형성을 지원하도록 만든다.
먼저 타입 코드를 서브클래스로 바꾸기를 진행한다.
그러기 위해선 클래스를 직접적으로 생성하는 코드를 팩토리 함수를 호출하게 하여 의존성을 낮춰야 한다.
class PerformanceCalculator{
constructor(aPerformance, aPlay){
this.performance = aPerformance;
this.play = aPlay;
}
get amount(){
return new Error('서브클래스에서 처리하도록 설계되었습니다.');
}
get volumeCredits(){
let result = 0;
result += Math.max(this.performance.audience -30,0);
if("comedy" === this.play.type)
result += Math.floor(this.performance.audience/5);
return result;
}
}
class TragedyCalcultor extends PerformanceCalculator{
get amount(){
let result = 0;
result = 40000;
if(this.performance.audience > 30){
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}
class ComedyCalcultor extends PerformanceCalculator{
get amount(){
let result = 0;
result = 30000;
if(this.performance.audience > 30){
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}
}
function createPerformanceCalculator(aPerformance, aPlay){
switch(aPlay.type){
case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
default: throw new Error(`알 수 없는 장르 : ${aPlay.type}`);
}
}
1.9 상태 점검 : 다형성을 활용하여 데이터 생성하기
//createStatementData.js 별도 파일이라 가정
class PerformanceCalculator{
constructor(aPerformance, aPlay){
this.performance = aPerformance;
this.play = aPlay;
}
get amount(){
return new Error('서브클래스에서 처리하도록 설계되었습니다.');
}
get volumeCredits(){
return Math.max(this.performance.audience -30,0);
}
}
class TragedyCalcultor extends PerformanceCalculator{
get amount(){
let result = 0;
result = 40000;
if(this.performance.audience > 30){
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}
class ComedyCalcultor extends PerformanceCalculator{
get amount(){
let result = 0;
result = 30000;
if(this.performance.audience > 30){
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}
get volumeCredits(){
return super.volumeCredits + Math.floor(this.performance.audience/5);
}
}
function createPerformanceCalculator(aPerformance, aPlay){
switch(aPlay.type){
case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
default: throw new Error(`알 수 없는 장르 : ${aPlay.type}`);
}
}
function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return statementData;
function enrichPerformance(aPerformance){
const calculator = createPerformanceCalculator(aPerformance,
playFor(aPerformance)); //생성자 대신 팩토리 함수 이용
const result = Object.assign({}, aPerformance);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}
function playFor(aPerformance){
return plays[aPerformance.playID];
}
function totalAmount(data){
return data.performances
.reduce((total, p) => total + p.amount, 0 );
}
function totalVolumeCredits(data){
return data.performances
.reduce((total, p)=>total+p.volumeCredits, 0);
}
}
//createStatementData.js 별도 파일이라 가정 끝
조건부 로직을 생성 함수하나로 옮겼다.
연극 장르별 계산 코드들을 함께 묶었다.
1.10 마치며
리팩터링을 크게 3 단계로 진행했다.
1단계 : 원본 함수를 중첩 함수 여러개로 나누기
2단계 : 쪼개기를 적용, 계산 코드/출력 코드 분리
3단계 : 계산 로직을 다형성으로 표현
좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'다
코드를 수정할 때 고쳐야 할 곳을 쉽게 찾고, 오류 없이 빠르게 수정할 수 있어야 한다.
고객의 요구사항을 빠르게 반영할 수 있어야 한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>
//statement.js 파일이라 가정
function statement(invoice, plays) {
return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays){
return renderHtml(createStatementData(invoice,plays));
//중간데이터 생성 함수를 공유한다.
}
function renderHtml(data, plays) {
let result = `<h1>청구 내역 (고객명 : ${data.customer})</h1>\n`;
result += "<table>\n";
result += "<tr><th>연극</th><th>좌석 수</th><th>금액</th></tr>"
for(let perf of data.performances){
result += `<tr><td>${perf.play.name}</td><td>(${perf.audience}석)</td><td>${usd(perf.amount)}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>총액: <em>${usd(data.totalAmount)}</em></p>\n`;
result += `<p>적립 포인트: <em>${data.totalVolumeCredits}</em>점</p>\n`;
return result;
}
function renderPlainText(data, plays) {
let result = `청구 내역 (고객명 : ${data.customer})\n`;
for(let perf of data.performances){
result += `${perf.play.name} : ${usd(perf.amount)} (${perf.audience}석)\n`;
}
result += `총액: ${usd(data.totalAmount)}\n`;
result += `적립 포인트: ${data.totalVolumeCredits}점\n`;
return result;
}
function usd(aNumber){
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(aNumber/100);
}
//statement.js 파일이라 가정 끝
//createStatementData.js 별도 파일이라 가정
class PerformanceCalculator{
constructor(aPerformance, aPlay){
this.performance = aPerformance;
this.play = aPlay;
}
get amount(){
return new Error('서브클래스에서 처리하도록 설계되었습니다.');
}
get volumeCredits(){
return Math.max(this.performance.audience -30,0);
}
}
class TragedyCalcultor extends PerformanceCalculator{
get amount(){
let result = 0;
result = 40000;
if(this.performance.audience > 30){
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}
class ComedyCalcultor extends PerformanceCalculator{
get amount(){
let result = 0;
result = 30000;
if(this.performance.audience > 30){
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}
get volumeCredits(){
return super.volumeCredits + Math.floor(this.performance.audience/5);
}
}
function createPerformanceCalculator(aPerformance, aPlay){
switch(aPlay.type){
case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
default: throw new Error(`알 수 없는 장르 : ${aPlay.type}`);
}
}
function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return statementData;
function enrichPerformance(aPerformance){
const calculator = createPerformanceCalculator(aPerformance,
playFor(aPerformance)); //생성자 대신 팩토리 함수 이용
const result = Object.assign({}, aPerformance);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}
function playFor(aPerformance){
return plays[aPerformance.playID];
}
function totalAmount(data){
return data.performances
.reduce((total, p) => total + p.amount, 0 );
}
function totalVolumeCredits(data){
return data.performances
.reduce((total, p)=>total+p.volumeCredits, 0);
}
}
//createStatementData.js 별도 파일이라 가정 끝
console.log(statement(invoices,plays));
document.write(htmlStatement(invoices,plays));
</script>
</body>
</html>
//기본 함수
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format;
for(let perf of invoice.performances){
const play = plays[perf.playID];
let thisAmount = amountFor(perf, play);//함수로 추출했다.
//포인트를 적립한다.
volumeCredits += Math.max(perf.audience -30,0);
//희극 관객 5명 마다 추가 포인트를 제공한다.
if("comedy" === play.type) volumeCredits += Math.floor(perf.audience/5);
//청구 내역을 출력한다.
result += `${play.name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
totalAmount += thisAmount;
}
result += `총액: ${format(totalAmount/100)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
function amountFor(aPerformance, play) {//명확한 이름으로 변경
let result = 0;
switch(play.type){
case "tragedy" : //비극
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" : //희극
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${play.type}`);
}
return result;
}
}
play 변수 제거하기
aPerformance는루프변수에서오기에매순회마다값이변경된다.
반면에 play는개별공연(aPerformance)에서얻기때문에매개변수로전달할필요가없다.
amountFor() 안에서다시계산하면된다.
이런식으로함수를쪼갤때마다불필요한매개변수를최대한제거한다.
이러한임시변수들때문에로컬범위에존재하는이름이많아져추출작업이복잡해진다.
for(let perf of invoice.performances){
const play = playFor(perf); // 우변을 함수로 추출
...
function playFor(aPerformance){
return plays[aPerformance.playID];
}
정상동작테스트함그다음과정 변수인라인하기
for(let perf of invoice.performances){
// const play = playFor(perf); //변수 인라인하기
let thisAmount = amountFor(perf, playFor(perf));
//포인트를 적립한다.
volumeCredits += Math.max(perf.audience -30,0);
//희극 관객 5명 마다 추가 포인트를 제공한다.
if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
//청구 내역을 출력한다.
result += `${playFor(perf).name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
totalAmount += thisAmount;
}
amountFor()함수에서playFor()함수로인해매개변수를하나줄일수있게됐다.
function amountFor(aPerformance/*, play*/) {//필요없어진 매개변수제거
let result = 0;
switch(playFor(aPerformance).type){
case "tragedy" : //비극
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" : //희극
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
}
return result;
}
지역변수를제거해서함수추출하기가훨씬쉬워졌다.
이유는유효범위를신경써야할대상이줄었기때문이다.
추출작업전에거의항상지역변수부터제거하도록하자
amountFor()함수파라미터는다정리했으니 statement()를보자
임시변수인 thisAmount는선언되고값이변하지않는다.
따라서 "변수인라인하기"를적용한다.
statement()속…
for(let perf of invoice.performances){
//let thisAmount = amountFor(perf); //thisAmount변수 인라인
//포인트를 적립한다.
volumeCredits += Math.max(perf.audience -30,0);
//희극 관객 5명 마다 추가 포인트를 제공한다.
if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
//청구 내역을 출력한다.
result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);//thisAmount변수 인라인
}
적립 포인트 계산 코드 추출하기
play 변수를제거한결과로컬유효범위의변수가하나줄어서적립포인트계산부분을추출하기가쉬워졌다.
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format;
for(let perf of invoice.performances){
//포인트를 적립한다.
volumeCredits += Math.max(perf.audience -30,0);
//희극 관객 5명 마다 추가 포인트를 제공한다.
if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
//청구 내역을 출력한다.
result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);//thisAmount변수 인라인
}
result += `총액: ${format(totalAmount/100)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}
처리해야할변수두개 perf, volumeCredits
volumeCredits처리
//함수 추출
function volumeCreditsFor(perf){
let volumeCredits = 0;
volumeCredits += Math.max(perf.audience -30,0);
if("comedy" === playFor(perf).type)
volumeCredits += Math.floor(perf.audience/5);
return volumeCredits;
}
...............
for(let perf of invoice.performances){
volumeCredits += volumeCreditsFor(perf);//추출한 함수를 이용해 값을 누적
result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}
테스트 동작 확인 이제 적절한 이름으로
...............
function volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("comedy" === playFor(aPerformance).type)
volumeCredits += Math.floor(aPerformance.audience/5);
return volumeCredits;
}
format 변수 제거하기
임시변수는나중에문제를일으킬수있다.
임시변수는자신이속한루틴에서만의미가있어서루틴이길고복잡해지기쉽다.
임시변수를제거하는것이다음리팩터링
리팩토링대상코드
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format;
format은임시변수에함수를대입한형태다.(함수포인터처럼)
직접함수를선언해사용하도록바꾼다.
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
for(let perf of invoice.performances){
volumeCredits += volumeCreditsFor(perf);
result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}
result += `총액: ${format(totalAmount/100)}\n`;//임시 변수였던 format을 함수 호출로 대체
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}
function format(aNumber){
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(aNumber);
}
for(let perf of invoice.performances){
volumeCredits += volumeCreditsFor(perf);
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}
result += `총액: ${usd(totalAmount)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}
function usd(aNumber){
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(aNumber/100); // 단위 변환 로직도 이 함수로 안으로 이동
}
긴함수를작게쪼개는리팩터링은이름을잘지어야만효과가있다.
이름이좋으면함수본문을읽지않고도무슨일을하는지알수있다.
이름바꾸기는쉬우므로이름짓기를주저하지말고짓자. 더좋은이름이떠올르면다시변경하면그만이다.
volumeCredits 변수 제거하기
이변수는반복문을한바퀴돌때마다값을누적하기때문에리팩터링하기가더까다롭다.
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
for(let perf of invoice.performances){
volumeCredits += volumeCreditsFor(perf);
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}
result += `총액: ${usd(totalAmount)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}
먼저반복문쪼개기로 volumeCredits 값이누적되는부분을따로빼낸다.
위를아래처럼
function statement(invoice, plays) {
let totalAmount = 0;
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
for(let perf of invoice.performances){
//청구 내역을 출력한다.
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}
let volumeCredits = 0;// 변수 선언을 반복문 앞으로 옮김
for(let perf of invoice.performances){ // 값 누적 로직을 별도 for문으로 분리
volumeCredits += volumeCreditsFor(perf);
}
result += `총액: ${usd(totalAmount)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}
이렇게분리하면임시변수를질의함수로바꾸기가수월해진다.
함수로추출한것으로변수인라인을한다.
for(let perf of invoice.performances){
//청구 내역을 출력한다.
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}
result += `총액: ${usd(totalAmount)}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;
for(let perf of invoice.performances){
//청구 내역을 출력한다.
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
let totalAmount = tmpFunction();
function tmpFunction(){ //함수 추출/ 이름은 임시로
let totalAmount = 0;
for(let perf of invoice.performances){
totalAmount += amountFor(perf);
}
return totalAmount;
}
…..
function totalAount(){
let result = 0; // 함수 안 변수이름도 자기 스타일에 맞게 변경
for(let perf of invoice.performances){
result += amountFor(perf);
}
return result;
}
result += `총액: ${usd(totalAount())}\n`; //함수 인라인 후 의미있는 이름으로 변경하기
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
1.5 중간 점검: 난무하는 중첩 함수
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>
function statement(invoice, plays) {
let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
for(let perf of invoice.performances){
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
result += `총액: ${usd(totalAount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;
//중첩 함수 시작
function totalAount(){
let result = 0;
for(let perf of invoice.performances){
result += amountFor(perf);
}
return result;
}
function totalVolumeCredits(){
let volumeCredits = 0;
for(let perf of invoice.performances){
volumeCredits += volumeCreditsFor(perf);
}
return volumeCredits;
}
function usd(aNumber){
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(aNumber/100);
}
function volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("comedy" === playFor(aPerformance).type)
volumeCredits += Math.floor(aPerformance.audience/5);
return volumeCredits;
}
function playFor(aPerformance){
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
let result = 0;
switch(playFor(aPerformance).type){
case "tragedy" :
result = 40000;
if(aPerformance.audience > 30){
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy" :
result = 30000;
if(aPerformance.audience > 30){
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
}
return result;
}
}
console.log(statement(invoices,plays));
</script>
</body>
</html>