1

오민준·2025년 1월 22일
0

Chapter 1 Intro

  • 챕터1에서는 리팩토링 원칙을 이해하기 이전에 실제로 리팩토링을 수행한다.
  • 원칙은 일반화되기 쉬워 실제 적용방법을 파악하기 어렵지만 예시가 있다면 명확해지기 때문이다.

1.1

다양한 연극을 외주로 받아 공연하는 극단이 있다.
공연요청이 들어오면 연극의 장르와 관객 규모를 기초로 비용을 책정한다.
공연료와 별개로 포인트를 지급해 다음번 의뢰 시 공연료를 할인받을 수도 있다.

공연할 연극 정보

plays.json
{
  "hamlet": {"name": "Hamlet", "type": "tragedy"}
  "as-like": {"name": "As You Like It", "type": "comedy"}
  "othello": {"name": "Othello", "type": "tragedy"}
}

공연료 청구서에 들어갈 데이터

invoices.json
[
  {
    "customer": "BigCo",
    "performances": [
      {
        "playID": "hamlet",
        "audience": 55
      },
      {
        "playID": "as-like",
        "audience": 35
      },
      {
        "playID": "othello",
        "audience": 40
      },
    ]
  } 
]

공연료 청구서를 출력하는 코드

export 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 = 40_000;

        if (perf.audience > 30) {
          thisAmount += 1_000 * (perf.audience - 30);
        }
        break;
      case 'comedy':
        thisAmount = 30_000;

        if (perf.audience > 20) {
          thisAmount += 10_000 + 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;
}

1.2

  • 짧은 프로그램이 잘 작동하는 상황에서 그저 코드가 '지저분하다'라는 이유로 수정하는 것은 좋지 않다.
  • 설계가 나빠 수정하기 어려운 프로그램이 있다면 구조부터 바로잡은 뒤에 기능을 수정하는 편이 수월하다.
  • 위 프로그램에 수정할 부분
    • 청구 내역을 HTML로 출력하는 기능이 필요하다.
    • 연극 장르와 공연료 정책이 변경될 수 있지만 그에 맞지 않는 구조를 갖고 있다.

1.3

  • 리팩토링하기 전에 자가진단이 가능한 테스트코드부터 마련해야한다.

1.4 staetement() 함수 쪼개기

  • 전체 동작을 각각의 작은 부분으로 나눌 수 있는 지점을 찾다보면 중간 지점의 switch문이 눈에 띈다.
  • switch문은 한 번의 공연에 대한 요금을 계산하고 있는데 이는 코드를 분석해서 얻은 정보이며, 이런식으로 분석을 통해 파악한 정보는 휘발되므로 다시 분석하지 않아도 되는 코드를 작성해야 한다.
  • 프로그램 수정을 작은 단계로 나눠 진행해야 버그를 쉽게 잡을 수 있다.

volumeCredits 변수를 제거하는 작업 단계
1. 반복문 쪼개기로 변수 값을 누적시키는 부분을 분리한다.
2. 문장 슬라이드하기로 변수 초기화 문장을 변수 값 누적 코드 바로 앞으로 옮기고
3. 함수 추출하기로 적립 포인트 계산 부분을 별도 함수로 추출한다.
4. 변수 인라인하기로 volumeCredits 변수를 제거한다.

1.5

  • 그 결과 계산 로직은 모두 여러개의 보조함수로 빠져 최상위 statement() 함수는 짧아지고 출력한 문장을 생성하기만 한다.

1.6 계산 단계와 포맷팅 단계 분리하기

  • 앞서 말한대로 지금까지는 코드의 구조를 보강하는 데에 주안점을 두었다면 이제는 기능 변경, statement()의 HTML 버전을 만드는 작업을 하려고 한다.
export 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 = 40_000;

        if (aPerformance.audience > 30) {
          result += 1_000 * (aPerformance.audience - 30);
        }
        break;
      case 'comedy':
        result = 30_000;

        if (aPerformance.audience > 20) {
          result += 10_000 + 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' === aPerformance.play.type) {
      result += Math.floor(aPerformance.audience / 5);
    }

    return result;
  }

  function totalAmount(data) {
    return data.performances.reduce((acc, cur) => acc + cur.amount, 0);
  }

  function totalVolumeCredits(data) {
    return data.performances.reduce((acc, cur) => acc + cur.volumeCredits, 0);
  }
}
import { createStatementData } from './createStatementData.js';

export function statement(invoice, plays) {
  return renderPlainText(createStatementData(invoice, plays));
}

function renderPlainText(data) {
  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 htmlStatement(invoice, plays) {
  return renderHtml(createStatementData(invoice, plays));
}

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(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 usd(aNumber) {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }).format(
    aNumber / 100
  );
}

1.8 다형성을 활용해 계산 코드 재구성하기

  • 연극 장르를 추가하고 장르마다 공연료와 적립 포인트 곗나법을 다르게 지정하도록 기능을 수정한다.
  • amountFor()와 같은 조건부 로직은 코드 수정횟수가 늘어날수록 골치거리이므로 구조적인 요소로 적절히 보완해야한다.
  • 공연료와 적립 포인트 계산 함수를 담을 클래스를 만든다.

dumplicate code

profile
ChatGPT-Driven Development를 지양합니다.

0개의 댓글

관련 채용 정보