// 포인트를 적립한다. volumeCredits += Math.max(perf.audience - 30, 0); // 희극 관객 5명마다 추가 포인트를 제공한다. if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
functionamountFor(perf, play) { // 값이 바뀌지 않는 변수는 매개변수로 전달 let result = 0; switch (play.type) { case"tragedy": //비극 result = 40000; if (perf.audience > 30) result += 1000 * (perf.audience - 30); break; case"comedy": //희극 result = 30000; if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20); result += 300 * perf.audience; break; default: thrownewError(`알 수 없는 장르: ${play.type}`); } return result; }
3) 임시변수를 질의함수로 바꾸기
4) 변수 인라인화
play변수 제거하기 perf는 for문을 통해 매번 바뀌지만, play변수는 perf 를 통해 얻기 때문에 매개변수로 전달할 필요가 없다. 이런 임시 변수를 제거하면 로컬 범위에 존재하는 이름이 줄어 들어, 추출작업이 편해진다.
functionstatement(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 += thisAmount; totalAmount += amountFor(perf); } result += `총액: ${usd(totalAmount)}\n`; result += `적립 포인트: ${volumeCredits}점\n`; return result; }
statement(INVOICE[0], PLAYS);
6) 반복문 쪼개기
7) 연관 변수끼리 모으기(문장 슬라이드하기)
1 2 3 4 5 6 7 8 9
let volumeCredits = 0; // 변수 선언(초기화)을 반복문 앞으로 이동 for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); }
for (let perf of invoice.performances) { result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`; totalAmount += amountFor(perf); }
반복문을 쪼개서 성능이 느려지지 않을까 걱정할 수 있지만, 이 정도 중복은 성능에 미치는 영향이 미미할 때가 많다. 게다가 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서, 우리의 직관을 초월하는 결과를 내어준다.
하지만 리팩터링이 성능에 상당한 영향을 주기도한다. 그런 경우라도, 저자는 개의치 않고 리팩터링한다. 잘 다듬어진 코드라야 성능 개선 작업도 훨씬 수월하기 때문이다. 리팩터링 과정에서 성능이 크게 떨어졌다면, 리팩터링 후 시간을 내어 성능을 개선한다.
위에서 배운 3) 임시변수를 질의함수로 바꾸기1) 함수로 추출4) 변수 인라인화 도 함께 적용해보면
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
functiontotalVolumeCredits() { let result = 0; for (let perf of invoice.performances) { result += volumeCreditsFor(perf); } return result; }
functiontotalAmount() { let result = 0; for (let perf of invoice.performances) { result += amountFor(perf); } return result; }
functionstatement(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(totalAmount())}\n`; result += `적립 포인트: ${totalVolumeCredits()}점\n`; return result; functiontotalVolumeCredits() { let result = 0; for (let perf of invoice.performances) { result += volumeCreditsFor(perf); } return result; } functiontotalAmount() { let result = 0; for (let perf of invoice.performances) { result += amountFor(perf); } return result; } functionusd(num) { returnnewIntl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format(num/100) } functionplayFor(aPerf) { return plays[aPerf.playID] } functionvolumeCreditsFor(perf) { let result = 0 result += Math.max(perf.audience - 30, 0);
if ("comedy" === playFor(perf).type) { result += Math.floor(perf.audience / 5); }
return result } functionamountFor(perf) { let result = 0; switch (playFor(perf).type) { case"tragedy": result = 40000; if (perf.audience > 30) result += 1000 * (perf.audience - 30); break; case"comedy": result = 30000; if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20); result += 300 * perf.audience; break; default: thrownewError(`알 수 없는 장르: ${playFor(perf).type}`); } return result; } }
functionstatement(invoice, plays) { const statementData = { customer: invoice.customer, performances: invoice.performances.map(enrichPerformance) }; return renderPlainText(statementData, plays); functionenrichPerformance(aPerf) { const result = Object.assing({}, aPerf); result.play = playFor(result); result.amount = amountFor(result); result.volumeCredits = volumeCreditsFor(result); return result; } // 함수를 데이터 계산 단계로 이동시켰다. functionplayFor(aPerf) {...} functionamountFor(perf) { let result = 0; // play를 바로 사용할 수 있다! switch (perf.play.type) { case"tragedy": result = 40000; if (perf.audience > 30) result += 1000 * (perf.audience - 30); break; case"comedy": result = 30000; if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20); result += 300 * perf.audience; break; default: thrownewError(`알 수 없는 장르: ${perf.play.type}`); } return result; } functionvolumeCreditsFor(perf) { let result = 0 result += Math.max(perf.audience - 30, 0);
if ("comedy" === perf.play.type) { result += Math.floor(perf.audience / 5); }
return result } }
functionrenderPlainText(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(totalAmount())}\n`; result += `적립 포인트: ${totalVolumeCredits()}점\n`; return result; functiontotalVolumeCredits() { let result = 0; for (let perf of invoice.performances) { // volumeCredits 바로 사용 result += perf.volumeCredits; } return result; } functiontotalAmount() { let result = 0; for (let perf of invoice.performances) { // amount 사용하도록 수정 result += perf.amount; } return result; } functionusd(num) {...} }
functionrenderHtml(data) { 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>`; result += `<td>${usd(perf.amount)}</td></tr>\n`; } result += `</table>\n`; result += `총액: ${usd(data.totalAmount)}\n`; result += `적립 포인트: ${data.totalVolumeCredits}점\n`; return result; }
// usd를 renderHtml()에서도 사용할 수 있도록 최상위로 옮김 functionusd(num) { ... }
4. 요구사항 : 다양한 장르 커버
조건부 로직을 포함한 amountFor()와 volumeCreditsFor()를 호출하여 공연료를 계산하는데, 이 두 함수를 전용클래스로 옮기고, 다형성을 이용해서 조건문을 없애보자.
함수 옮기기 기법을 적용해서 계산기 클래스로 옮기자 기존의 performance, play 데이터를 this.performance 와 this.play로 바꿔준다. 그리고 조건문이 들어있는 amountFor() 와 volumeCreditsFor() 함수도 클래스로 이동 시킨다.
this를 남발 하는 것보다, 만약 this의 값이 변화한다면 변수로 받아 사용하는 것이 코드의 이해에 더 좋을 것이라고 생각한다.(내 생각)
classPerformanceCalculator{ constructor(aPerf, aPlay) { this.performance = aPerf; this.play = aPlay; } get amount() { thrownewError("서브클래스를 통해서 처리하도록 설계") } get volumeCredits() { throwMath.max(this.performance.audience - 30, 0) } }
classTragedyCalculatorextendsPerformanceCalculator{ get amount() { let result = 40000; if (this.performance.audience > 30) { result += 1000 * (this.performance.audience - 30); } return result; } }
classComedyCalculatorextendsPerformanceCalculator{ get amount() { let result = 30000; if (this.performance.audience > 20) { result += 10000 + 500 * (this.performance.audience - 20); } result += 300 * this.performance.audience; return result; } get volumeCredits() { returnsuper.volumeCredits + Math.fllor(this.performance.audience / 5); } }
amountFor() volumeCreditsFor() 의 조건부 로직을 생성함수 하나로 옮겼다. 같은 타입의 다형성을 기반으로 실행되는 함수가 많을 수록 이렇게 구성하는 쪽이 유리하다!
classPerformanceCalculator{ constructor(aPerf, aPlay) { this.performance = aPerf; this.play = aPlay; } get amount() { thrownewError("서브클래스를 통해서 처리하도록 설계") } get volumeCredits() { throwMath.max(this.performance.audience - 30, 0) } }
classTragedyCalculatorextendsPerformanceCalculator{ get amount() { let result = 40000; if (this.performance.audience > 30) { result += 1000 * (this.performance.audience - 30); } return result; } }
classComedyCalculatorextendsPerformanceCalculator{ get amount() { let result = 30000; if (this.performance.audience > 20) { result += 10000 + 500 * (this.performance.audience - 20); } result += 300 * this.performance.audience; return result; } get volumeCredits() { returnsuper.volumeCredits + Math.fllor(this.performance.audience / 5); } }
쓰인 기법들 정리
함수 추출
변수 이름 명확히 변경
임시변수를 질의함수로 바꾸기
변수 인라인화
함수 이름 바꾸기
반복문 쪼개기
연관 변수끼리 모으기
단계 쪼개기
함수 이동
생성자를 팩터리 함수로 바꾸기
조건부 로직 다형성으로 바꾸기
리팩터링을 크게 세단계로 나눠 진행했다.
원본함수를 중첩함수 여러 개로 나눴다.
단계 쪼개기를 적용해 계산코드와 출력 코드로 분리했다.
계산 로직을 다형성으로 표현했다.
좋은 코드를 가늠하는 기준은 분분하지만, “수정하기 쉬운 코드”야 말로 좋은 코드라고 저자는 생각한다. 오류없이 빠르게 수정할 수 있으며, 고객에게 필요한 기능을 더 빠르고 저렴한 비용으로 제공하도록 해준다.