리팩터링 2판을 잠시 읽어 보았다.
처음 단계에서 거의 모든 내용이 나온다.
새롭게 하나 배운 점은 리팩토링과 성능 중 어느 것이 더 중요하나?의 관점이었다.
나는 이제 것 자연스럽게 하나의 루프문에 처리 할 수 있는 모든 로직을 넣었었다.
하지만 이 책에서는 리팩터링을 위해 몇번의 루프문을 나누어서 처리한다.
루프문의 성능이 매우 좋아 체감되는 것이 없다는 설명이었다.
모든 개발자가 이 이야기에 찬성하는지는 모르겠다.
아마 이터러블의 길이가 너무 길다면 그런식의 리팩토링은 힘들겠지.
또 하나는 과하다 싶을 정도로 함수 추출을 많이 한다.
함수의 이름을 잘 짓는 것이 중요하다.
내포 함수도 많이 사용한다. 그리고 임시 변수를 싫어한다.
인라인 함수를 좋아한다.
데이터 준비단계와 출력단계를 나누어 한번의 데이터 준비 로직으로 두가지의 아웃풋을 낼 수 있도록 작업한다.
50줄이었던 코드가 75줄로 늘어나는데 보기에는 75줄의 코드가 더 잘 읽히는 신기한 경험이다.
컴퓨터가 읽기 좋은 코드가 아닌 사람이 읽기에 좋은 코드를 작성해야 한다.
비교를 위해 두개의 코드를 올려본다.
// 공연 정보
{
"hamlet": {
"name": "hamlet",
"type": "tragedy"
},
"as-like": {
"name": "As You Like It",
"type": "comedy"
},
"othello": {
"name": "Othello",
"type": "tragedy"
}
}
[
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
]
정리되지 않은 코드
import invoices from './invoices.json' assert { type: 'json' };
import plays from './plays.json' assert { type: 'json' };
console.log(statement(invoices[0], plays));
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', maximumFractionDigits: 2 })
.format;
for (let perf of invoice.performances) {
const play = plays[perf.playID];
let thisAmount = 0;
switch (play.type) {
case 'tragedy':
thisAmount = 40000;
if (perf.audience > 30) {
thisAmount += 1000 * (perf.audience - 30);
}
break;
case 'comedy':
thisAmount = 30000;
if (perf.audience > 20) {
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`);
}
// 포인트를 적립한다.
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;
}
정리된 코드
import invoices from './invoices.json' assert { type: 'json' };
import plays from './plays.json' assert { type: 'json' };
console.log(statement(invoices[0], plays));
function statement(invoice, plays) {
function createStatementData(invoice, plays) {
const result = {};
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance); // 함수 호출문이 생략됬지만 인자가 넘어감
result.totalAmount = totalAmount(result);
result.totalVolumeCredits = totalVolumeCredits(result);
return result;
}
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(`알 수 없는 장르: ${playFor(aPerformance).type}`);
}
return result;
}
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance.audience - 30, 0);
if ('comedy' === aPerformance.play.type) {
result += Math.floor(aPerformance.audience / 5);
}
return result;
}
function totalAmount(data) {
let result = 0;
for (let perf of data.performances) {
result += perf.amount;
}
return result;
}
function totalVolumeCredits(data) {
let result = 0;
for (let perf of data.performances) {
result += perf.volumeCredits;
}
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', maximumFractionDigits: 2 })
.format(aNumber / 100);
}
}
return renderPlainText(createStatementData(invoice, plays));
}