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

⭐️ 기능 수정

목표

연극 장르를 추가하고 장르마다 공연료와 적립 포인트 계산법을 다르게 지정하도록 기능을 수정해보자. 현재 상태에서는 조건문을 수정해야한다. 이런 형태의 조건부 로직은 코드 수정 횟수가 늘어날수록 골칫거리로 전락하기 쉽다. 조건부 로직을 명확한 구조로 보완해보자.

객체지향의 핵심 특성인 다향성(polymorphism)을 활용하는 것이 자연스럽다. 희극 서브클래스와 비극 서브클래스가 각자의 구체적인 계산 로직을 정의하는 것이다.

  • 호출하는 쪽: 다형성 버전의 공연료 계산 함수를 호출

  • 연결하는 쪽: 희극 or 비극이냐에 따라 정확한 계산 로직을 연결

조건부 로직을 다형성으로 바꾸기

  1. 조건부 코드 한 덩어리를 다형성으로 활용하는 방식으로 바꿔준다.

  2. 상속 계층 부터 정의해야 한다.

export default 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 = volumeCreidtsFor(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 (perf.audience > 30) {
        result += 1000 * (aPerformance.audience - 30);
      }
      break;
    case "comedy":
      result = 30000;
      if (perf.audience > 20) {
        result += 1000 + 500 * (aPerformance.audience - 20);
      }
      result += 300 * aPerformance.audience
      break;
    default:
      throw new Error(`알 수 없는 장르: ${aPerformance.play.type}`);
    }
    return result;
  }
  
  function volumeCreditsFor(aPerformance) {
    let result = 0;
    volumeCredits += Math.max(aPerformance.audience - 30, 0);
    if ("comedy" === playFor(aPerformance).type) 
      volumeCredits += Math.Floor(aPerformance.audience / 5 );
    return result;
  }
  
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); 

공연료 계산기 만들기

enrichPerformance() 함수: 공연의 중간 데이터 구조에 채워주는

→ 조건부 로직을 포함한 amountFor() volumeCreditsFor()를 호출하여 적립 포인트를 계산한다.

→ 두 함수를 전용 클래스로 옮기자 (PerformanceCalculator)라 부르자

function enrichPerformance(aPerformance) {
  const calculator = new PerformanceCalculator(aPerformance);  // 공연료 계산기 생성
  const result = Object.assign({}, aPerformance); 
  result.play = playFor(result); 
  result.amount = amountFor(result);
  result.volumeCredits = volumeCreidtsFor(result);
  return result;
}
class PerformanceCalculator {    // 공연료 계산기 클래스    
  constructor(aPerformance) {
    this.performance = aPerformance;
  }
}

이 클래스의 객체로 할 수 있는 일은 아직 없다. 함수 선언 바꾸기를 적용하여 공연할 연극을 계산기로 전달한다.

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 = volumeCreidtsFor(result);
  return result;
}
class PerformanceCalculator {   
  constructor(aPerformance, aPlay) {
    this.performance = aPerformance;
    this.play = aPlay;
  }
}

함수들을 계산기로 옮기기

공연료 계산 코드를 계산기 클래스 안으로 복사한다. aPerformancethis.performance로 바꾸고 playFor(aPerformance)this.play로 바꿔준다.

get amount() { // amountFor() 함수의 코드를 계산기 클래스로 복사
  let result = 0;
    switch (this.play.type) {   // amountFor() 함수가 매개변수로 받던 정보를 계산기 필드에서 바로 얻음
    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 += 1000 + 500 * (this.performance.audience - 20);
      }
      result += 300 * this.performance.audience
      break;
    default:
      throw new Error(`알 수 없는 장르: ${this.play.type}`);
    }
  return result;

복사한 함수가 동작하게끔 수정했다면 원본 함수가 방금 만든 함수로 작업을 위임하도록 바꾼다.

function amountFor(aPerformance) {
  return new PerformanceCalculator(aPerformance, playFor(aPerformance)).amount;
}

원래 함수를 인라인하여 새 함수를 직접 호출하도록 수정한다.

function enrichPerformance(aPerformance) {
  const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance); 
  const result = Object.assign({}, aPerformance); 
  result.play = calculator.play;
  result.amount = calculator.amount;  // amountFor() 대신 계산기의 함수 이용
  result.volumeCredits = volumeCreidtsFor(result);
  return result;
}

적립 포인트를 계산하는 함수도 같은 방법으로 옮긴다.

get volumeCredits() {
  let result = 0;
  volumeCredits += Math.max(aPerformance.audience - 30, 0);
  if ("comedy" === playFor(aPerformance).type) 
    volumeCredits += Math.Floor(aPerformance.audience / 5 );
  return result;
}
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;
}

공연료 계산기를 다형성 버전으로 만들기

클래스에 로직을 담았으니 다형성을 지원하게 만들자.

📍 타입 코드 대신 서브클래스를 사용하도록 변경하자. 타입 코드를 서브클래스로 바꾸기

PerformanceCalculator의 서브클래스들을 준비하고 createdStatementData()에서 그 중 적합한 서브 클래스를 사용하게 만들어야 한다. 그리고 서브클래스를 사용하려면 생성자 대신 함수를 호출하도록 바꿔야한다. 자바스크립트는 생성자가 서브클래스의 인스턴스를 반환할 수 없기 때문이다.

생성자를 팩터리 함수로 바꾸기를 적용한다.

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 createPerformanceCalculator(aPerformance, aPlay) {
  return new PerformanceCalculator(aPerformance, aPlay);
}

함수를 이용하면 PerformanceCalculator의 서브클래스 중에서 어느 것을 생성해서 반환할지 선택할 수 있다.

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 extends PerformanceCalculator {
}
class ComedyCalculator extends PerformanceCalculator {
}

다형성을 지원하기 위한 구조는 갖춰졌다.

📍 조건부 로직을 다형성으로 바꾸기를 적용하자.

🎬 비극 공연의 공연료 계산

get amount() {
  let result = 40000;
  if (this.performance.audience > 30) {
    result += 1000 * (this.performance.audience - 30);
  }
  return result;
}

PerformanceCalculator의 조건부로직이 오버라이드 된다.

get amount() {
  let result = 0;
  switch (this.play.type) { 
    case "tragedy":
      throw '오류 발생'; // 비극 공연료는 TragedyCalculator를 이용하도록 유도
    case "comedy":
      result = 30000;
      if (this.performance.audience > 20) {
        result += 1000 + 500 * (this.performance.audience - 20);
      }
      result += 300 * this.performance.audience
      break;
    default:
      throw new Error(`알 수 없는 장르: ${this.play.type}`);
    }
}

🎬 희극 공연의 공연료 계산

get amount() {
  let result = 30000;
  if (this.performance.audience > 20) {
    result += 1000 + 500 * (this.performance.audience - 20);
  }
  return result;
}

슈퍼 클래스의 amount() 메스드는 호출할 일이 없ㄷ으니 삭제해도 좋으나 미래의 나를 위해 한마디 남겨놓자.

// PerformanceCalculator 클래스
get amount() {
  throw new Error('서브클래스에서 처리하도록 설계되었습니다.');
}

💰 적립 포인트 조건부 로직 교체하기

일반적인 경우를 기본으로 삼아 슈퍼클래스에 남겨두고, 장르마다 달라지는 부분은 필요할 때, 오버라이드하게 만드는 것이 좋다. 포인트 방식이 조금 다른 희극 처리 로직을 해당 서브클래스로 내린다.

get volumeCredits() {
  return Math.max(this.performance.audience - 30, 0);
}
get volumeCredits() {
  return super.volumeCredits + Math.floor(this.performance.audience / 5);
}

Last updated