前回の続き。
完全なコードは下記リポジトリに置いてます。
https://github.com/ryo-utsunomiya/refactoring-2nd-ts
計算の再構成
前回は新しい出力方法を追加する改修と、それに向けたリファクタリングを行いました。今回は、演劇の新しいカテゴリを追加する改修と、それに向けたリファクタリングを行います。
現在のところ、料金の計算関数は以下のように実装されています(書籍の方は異なる実装になってますが、play.type
をswitchで調べる設計は同じです)。
get amount(): number {
let result = 0;
switch (this.play.type) {
case "tragedy":
result = 40000;
if (this.audience > 30) {
result += 1000 * (this.audience - 30);
}
break;
case "comedy":
result = 30000;
if (this.audience > 20) {
result += 10000 + 500 * (this.audience - 20);
}
result += 300 * this.audience;
break;
default:
throw new Error(`unknown type: ${this.play.type}`);
}
return result;
}
同様に、play.type
を調べる処理がvolumeCreditsにも実装されています。
このような設計はとっかかりとしてはよいものですが、playの種類が増えていくと破綻していきます。
そこで、ポリモーフィズムによる条件分岐の置き換え(Replace Conditional with Polymorphism)を使用して、条件分岐を局所化していきましょう。
公演(Performance)の計算機を導入する
はじめに、公演の料金計算処理をStatementDataから分離して、独立したクラスにします。
import { Performance, Play } from "./types";
export default class PerformanceCalculator {
performance: Performance;
play: Play;
constructor(aPerformance: Performance, aPlay: Play) {
this.performance = aPerformance;
this.play = aPlay;
}
get amount(): number {
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(`unknown type: ${this.play.type}`);
}
return result;
}
}
同様に、volumeCreditsの計算もPerformanceCalculatorにもってきます。
get volumeCredits(): number {
let result = 0;
result += Math.max(this.performance.audience - 30, 0);
if (this.play.type === "comedy") {
result += Math.floor(this.performance.audience / 5);
}
return result;
}
これによって、PerformanceCalculatorをポリモーフィックにする準備ができました。
ポリモーフィズムの利用
演劇(Play)の種類に応じて別クラスで計算できるようにしたいので、まずそれぞれの計算機を用意します。
tragedyの計算機は以下のような実装になります。
class TragedyCalculator extends PerformanceCalculator {
get amount(): number {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience - 30);
}
return result;
}
get volumeCredits(): number {
return Math.max(this.performance.audience - 30, 0);
}
}
同様に、comedyはこうなります。
class ComedyCalculator extends PerformanceCalculator {
get amount(): number {
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}
get volumeCredits(): number {
let result = 0;
result += Math.max(this.performance.audience - 30, 0);
result += Math.floor(this.performance.audience / 5);
return result;
}
}
計算処理の多くがサブクラスに委譲されたので、PerformanceCalculatorはほぼ何もしないクラスになりました。そこで、PerformanceCalculatorは抽象クラスにしておきます(なお、JavaScriptには抽象クラスはありません。抽象クラスはTypeScriptの独自拡張です)。
abstract class PerformanceCalculator {
performance: Performance;
constructor(aPerformance: Performance) {
this.performance = aPerformance;
}
abstract get amount(): number;
abstract get volumeCredits(): number;
}
最後に、演劇(Play)に応じた計算機を作成するファクトリ関数を用意します。
export function createPerformanceCalculator(
aPerformance: Performance,
aPlay: Play
) {
switch (aPlay.type) {
case "tragedy":
return new TragedyCalculator(aPerformance);
case "comedy":
return new ComedyCalculator(aPerformance);
default:
throw new Error(`unknown type: ${aPlay.type}`);
}
}
これによって、新しい演劇の種類を足すときは、新しいCalculatorを足せばよくなりました。
まとめ
ここまで使用してきたリファクタリングのテクニックは以下の通りです。
- 関数の抽出
- 変数のインライン化
- 関数の移動
- ポリモーフィズムによる条件分岐の置き換え
ここまでのリファクタリングでも、決して完全な状態のプログラムに到達したわけではありません。リファクタリングは、開発を通して、プログラムをより良い状態へ導くためのテクニックといえます。