最近はプロジェクトが忙しく、ジムも行けてないし勉強も全然進んでない…
成長を考えると、仕事量はある程度あったほうがいいとは思っています。ただ、忙しすぎると勉強する時間が取れなくなって、逆に成長スピードが落ちる気がします。そう考えると定時で上がって、その分勉強する方が結果的には一番成長が早いんじゃないかなと思う今日この頃。

Strategy pattern
実際にコード書いていて意外だったのが、Strategyパターンが有効な場面が多いことでした。正直「Java言語で学ぶデザインパターン入門」でStrategyパターンを勉強した時は「戦略のクラス?よくわかんないなー」って感じでしたが、実際に使ってみるととても使いやすい!Factoryまで合わせると呼び出しもともスッキリ!綺麗なコードでみんなハッピー!ということで今回はStrategy pattern ( + Factory) についてまとめてみました。
Contents
コード概要
患者が治療を受けた後に、その請求を行う処理を実装していきます。患者は一回の診察で複数の治療・投薬を受ける可能性があります。その一度の治療において、全て保険適応があるなら保険診療として請求を行います。簡単化のために75歳以上の保険診療は患者負担は1割で、それ以外は3割で請求します。一回の診察において、一つでも保険適応のない治療がある場合、その診察における全ての請求を全額請求します(混合診療の禁止)。
実装
Treatment
早速コードに移りましょう。言葉で説明されてもイメージ湧かないですよね。まずは型定義をしていきます。一回の診察は複数の治療”Treatment”によって構成されます。そして治療には治療名”title”, 医療費”baseAmount”, 保険適用の有無”isInsuranceCovered”の3つのプロパティを持ちます。
//types.ts
export type Treatment = {
title: string;
baseAmount: number;
isInsuranceCovered: boolean;
}
Patient
Patientクラスを実装していきます。治療に関するデータはPatientに持たせます。1回の診察における治療を、上記のTreatmentの配列として保持します。そして支払いを表現するpayBillメソッドも実装します。
//patient.ts
import type { Treatment } from "./types.ts";
export class Patient {
name: string;
age: number;
treatments: Treatment[] = [];
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
setTreatments(treatments: Treatment[]): void {
this.treatments = treatments;
}
getTreatments(): Treatment[] {
return this.treatments;
}
appendTreatment(treatment: Treatment): void {
this.treatments.push(treatment);
}
payBill(bill: number): void {
console.log(`${this.name} has paid a bill of amount: ${bill}`);
}
}
Strategy + StrategyFactory
やっぱりコーディングは楽しいですね。続いて本題のStrategyと、ついでにStrategyFactoryを実装していきます。
InterfaceでStrategyが必ずcalculate methodを持つことを確約します。
//strategy.ts
export interface Strategy {
calculate(baseAmount: number): number;
}
具体的な戦略クラスです。今回は保険診療・後期高齢者診療・自費診療の3つの戦略を実装します。
//strategy.ts
export class InsuranceStrategy implements Strategy {
private readonly INSURANCE_RATE = 0.3;
public calculate(baseAmount: number): number {
return baseAmount * this.INSURANCE_RATE;
}
toString(): string {
return "InsuranceStrategy, rate: " + this.INSURANCE_RATE;
}
}
export class FullPaymentStrategy implements Strategy {
public calculate(baseAmount: number): number {
return baseAmount;
}
toString(): string {
return "FullPaymentStrategy";
}
}
export class GeriatricStrategy implements Strategy {
private readonly BURDEN_RATE = 0.1;
public calculate(baseAmount: number): number {
return baseAmount * this.BURDEN_RATE;
}
toString(): string {
return "GeriatricStrategy, rate: " + this.BURDEN_RATE;
}
}
Factoryまで作って、呼び出し元をスッキリさせましょう。Factoryは患者の特性(今回は年齢)と治療内容をもとに、適切な戦略インスタンスを生成します。
import type { Patient } from "./patient.ts";
import type { Treatment } from "./types.ts";
import type { Strategy } from "./strategy.ts";
import {
InsuranceStrategy,
FullPaymentStrategy,
GeriatricStrategy,
} from "./strategy.ts";
export class StrategyFactory {
public static createStrategy(patient: Patient): Strategy {
if (this.isFullyInsuranceCovered(patient.getTreatments()) === false) {
//一つでも自由診療があれば全額自費で請求します。
return new FullPaymentStrategy();
} else if (patient.age >= 75) {
//75歳以上は1割負担です。
return new GeriatricStrategy();
} else {
//簡単のために、それ以外は3割負担で計算します。
return new InsuranceStrategy();
}
}
//治療が全て保険適応があるかどうか調べます。
private static isFullyInsuranceCovered(treatments: Treatment[]): boolean {
return treatments.every(
(treatment) => treatment.isInsuranceCovered === true,
);
}
}
StrategyContext
次は戦略クラスを使って、実際に計算を行うStrategyContextを実装していきます。ContextはStrategy interfaceだけ知っていればよく、Strategyの各クラスの具体的な実装を知らなくて済みます。したがって具体的なStrategyが変更されても、Interfaceさえ変わらなければContextは修正しなくて済みます。
import type { Patient } from "./patient.ts";
import type { Strategy } from "./strategy.ts";
class StrategyContext {
private patient: Patient;
private calcStrategy: Strategy;
constructor(patient: Patient, strategy: Strategy) {
this.patient = patient;
this.strategy = strategy;
}
//治療内容から、治療費総額を計算します。
private calcBaseBill(): number {
let baseBill = 0
for (const treatment of this.patient.treatments) {
baseBill += treatment.baseAmount;
}
return baseBill;
}
//患者に対して請求を行います。
public charge(): void {
const baseBill = this.calcBaseBill();
const bill = this.calcStrategy.calculate(baseBill);
this.patient.payBill(bill);
console.log(`Strategy used: ${this.calcStrategy.toString()}`);
}
}
main.ts
それでは実際に実行してみましょう。
import { Patient } from "./patient.ts";
import { StrategyContext } from "./strategy_context.ts";
import { StrategyFactory } from "./strategy_factory.ts";
const patient1 = new Patient("Alice", 30);
patient1.setTreatments([
{ title: "Treatment A", baseAmount: 1000, isInsuranceCovered: true },
{ title: "Treatment B", baseAmount: 2000, isInsuranceCovered: true },
{ title: "Treatment C", baseAmount: 1500, isInsuranceCovered: false },
]);
const patient2 = new Patient("Bob", 80);
patient2.setTreatments([
{ title: "Treatment D", baseAmount: 3000, isInsuranceCovered: true },
{ title: "Treatment E", baseAmount: 2500, isInsuranceCovered: true },
]);
const patient3 = new Patient("Charlie", 65);
patient3.setTreatments([
{ title: "Treatment F", baseAmount: 4000, isInsuranceCovered: true },
{ title: "Treatment G", baseAmount: 3500, isInsuranceCovered: true },
]);
const strategy1 = StrategyFactory.createStrategy(patient1);
const context1 = new StrategyContext(patient1, strategy1);
context1.charge();
const strategy2 = StrategyFactory.createStrategy(patient2);
const context2 = new StrategyContext(patient2, strategy2);
context2.charge();
const strategy3 = StrategyFactory.createStrategy(patient3);
const context3 = new StrategyContext(patient3, strategy3);
context3.charge();
(base) hiro@mbair strategy % npx tsx main.ts Alice has paid a bill of amount: 4500 Strategy used: FullPaymentStrategy Bob has paid a bill of amount: 550 Strategy used: GeriatricStrategy, rate: 0.1 Charlie has paid a bill of amount: 2250 Strategy used: InsuranceStrategy, rate: 0.3