만약에 연극 종류가 추가되면 어떻게 될까? 이제 다형성으로 변경할 시점이다.
// createStatementData.js
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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 > 20) {
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default:
throw new Error('알 수 없는 장르: ${aPerformance.play.type}');
}
return result;
}
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance.audience - 30, 0);
if ("comedy" === playFor(aPerformance).type)
result += Math.floor(aPerformance.audience / 5);
return result;
}
function totalAmount(data) {
return data.performances
.reduce((total, p) => total + p.amount, 0);
}
func totalVolumeCredits(data) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
}
// statement.js
import createStatementData from './createStatementData.js';
function statement(invoice, plays) {
return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays) {
return renderHtml(createStatementData(invoice, plays));
}
function renderPlainText(data) {
let result = "청구 내역 (고객명: ${data.customer})\n";
for (let perf of data.performances) {
result += " ${perf.play.name}: ${usd(amountFor(perf))} (${perf.audience}석)\n";
}
result += "총액: ${usd(data.totalAmount)}\n";
result += "적립 포인트: ${data.totalVolumeCredits}점\n";
return result;
}
function renderHtml(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(amountFor(perf))}</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 usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style: "currency", currency: "USD",
minimumFractionDigits: 2 }).format(aNumber/100);
}
amountFor()
함수를 건들고, 여기서 처리할 수 밖에 없다.switch
문을 사용하게 되고, 확장이 될수록 골칫거리가 될 것이라는 것을 파악할 수 있다.createStatementData.js
에만 집중하면 된다.// createStatementData.js
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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 > 20) {
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default:
throw new Error('알 수 없는 장르: ${aPerformance.play.type}');
}
return result;
}
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance.audience - 30, 0);
if ("comedy" === playFor(aPerformance).type)
result += Math.floor(aPerformance.audience / 5);
return result;
}
function totalAmount(data) {
return data.performances
.reduce((total, p) => total + p.amount, 0);
}
func totalVolumeCredits(data) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
}
amountFor()
함수가 aPerformance
를 매개변수로 받는다.type
에 따라 계산로직이 분기되고 있다.volumeCreditsFor()
함수도 마찬가지다.PerformanceCalculator
)// createStatementData.js
class PerformanceCalculator {
constructor(aPerformance) {
this.performance = aPerformance;
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
return statementData;
function enrichPerformance(aPerformance) {
const calculator = new PerformanceCalculator(aPerformance);
const result = Object.assign({}, aPerformance);
result.play = playFor(aPerformance);
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 > 20) {
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default:
throw new Error('알 수 없는 장르: ${aPerformance.play.type}');
}
return result;
}
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance.audience - 30, 0);
if ("comedy" === playFor(aPerformance).type)
result += Math.floor(aPerformance.audience / 5);
return result;
}
function totalAmount(data) {
return data.performances
.reduce((total, p) => total + p.amount, 0);
}
func totalVolumeCredits(data) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
}
// createStatementData.js
class PerformanceCalculator {
constructor(aPerformance, aPlay) { // 변경됨
this.performance = aPerformance;
this.play = aPlay; // 변경됨
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
return statementData;
function enrichPerformance(aPerformance) {
const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); // 변경됨
const result = Object.assign({}, aPerformance);
result.play = calculator.play; // 변경됨
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
...
}
amountFor()
함수를 옮겨보자.PerfomanaceCalculator
클래스 안에서 작업 가능하도록 하자.// createStatementData.js
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
function 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 > 20) { // 변경됨
result += 10000 + 500 * (this.performance.audience - 20); // 변경됨
}
result += 300 * this.performance.audience; // 변경됨
break;
default:
throw new Error('알 수 없는 장르: ${this.play.type}'); // 변경됨
}
return result;
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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 = volumeCreditsFor(result);
return result;
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance.audience - 30, 0);
if ("comedy" === playFor(aPerformance).type)
result += Math.floor(aPerformance.audience / 5);
return result;
}
function totalAmount(data) {
return data.performances
.reduce((total, p) => total + p.amount, 0);
}
func totalVolumeCredits(data) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
...
}
amountFor()
함수를 PerformanceCalculator
클래스로 옮겼다.// 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 > 20) {
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.aPerformance.audience - 30, 0); // 변경됨
if ("comedy" === this.play.type) // 변경됨
result += Math.floor(this.aPerformance.audience / 5); // 변경됨
return result;
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
...
}
PerformanceCalculator
클래스를 다형성으로 바꿔보자..type
대신 서브클래스를 사용하도록 바꾸는 것이다. (타입 코드를 서브클래스로 바꾸기)// createStatementData.js
function createPerformanceCalculator(aPerformance, aPlay) {
switch(aPlay.type) {
case "tragedy":
return new TragedyCalculator(aPerformance, aPlay);
case "comedy":
return new ComedyCalculator(aPerformance, aPlay);
default:
throw new Error('알 수 없는 장르: ${aPlay.type}');
}
}
class TragedyCalculator(aPerformance, aPlay) {
}
class ComedyCalculator(aPerformance, aPlay) {
}
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 > 20) {
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.aPerformance.audience - 30, 0); // 변경됨
if ("comedy" === this.play.type) // 변경됨
result += Math.floor(this.aPerformance.audience / 5); // 변경됨
return result;
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
...
}
// createStatementData.js
function createPerformanceCalculator(aPerformance, aPlay) {
switch(aPlay.type) {
case "tragedy":
return new TragedyCalculator(aPerformance, aPlay);
case "comedy":
return new ComedyCalculator(aPerformance, aPlay);
default:
throw new Error('알 수 없는 장르: ${aPlay.type}');
}
}
class TragedyCalculator(aPerformance, aPlay) {
get amount() { // 변경됨
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}
class ComedyCalculator(aPerformance, aPlay) {
get amount() { // 변경됨
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}
}
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
get amount() {
throw new Error('서브클래스에서 처리하도록 설계되었습니다.'); // 변경됨
}
get volumeCredits() {
let result = 0;
result += Math.max(this.aPerformance.audience - 30, 0);
if ("comedy" === this.play.type)
result += Math.floor(this.aPerformance.audience / 5);
return result;
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
...
}
volumeCredits()
함수를 옮겨보자.// createStatementData.js
function createPerformanceCalculator(aPerformance, aPlay) {
switch(aPlay.type) {
case "tragedy":
return new TragedyCalculator(aPerformance, aPlay);
case "comedy":
return new ComedyCalculator(aPerformance, aPlay);
default:
throw new Error('알 수 없는 장르: ${aPlay.type}');
}
}
class TragedyCalculator(aPerformance, aPlay) {
get amount() {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}
class ComedyCalculator(aPerformance, aPlay) {
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() { // 변경됨
super.volumeCredits + Math.floor(this.aPerformance.audience / 5);
}
}
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
get amount() {
throw new Error('서브클래스에서 처리하도록 설계되었습니다.');
}
get volumeCredits() {
Math.max(this.aPerformance.audience - 30, 0);
}
}
export default function createStatementData(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount();
statementData.totalVolumeCredits = totalVolumeCredits();
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) {
data.performances
.reduce((total, p) => total + p.volumeCredits, 0);
}
...
}