TypeScriptにおける主要エンティティ設計と戦略パターン適用のコード設計手順書
うの,•4 min read
TypeScript における主要エンティティ設計と戦略パターン適用のコード設計手順書
1. 主要エンティティの設計
1.1. 主要エンティティの定義と選定基準
主要エンティティ(Core Entity)とは、アプリケーションのビジネスロジックの中核を担い、固有のルールや状態管理を行うオブジェクトを指します。
選定基準(以下の2つ以上を満たすこと):
- アイデンティティを持つか? (
id
などの一意な識別子がある) - 固有のビジネスロジックを持つか? (単なるデータコンテナではない)
- 状態の変化を管理するか? (ライフサイクルや状態遷移がある)
1.2. エンティティと値オブジェクトの区別
- エンティティ(Entity): 一意な ID を持ち、状態を変更できる
- 値オブジェクト(Value Object): 一意な ID を持たず、不変である
値オブジェクトの例:
type OrderItem = {
readonly productId: string;
readonly price: number;
readonly quantity: number;
};
function updateQuantity(item: OrderItem, newQuantity: number): OrderItem {
return { ...item, quantity: newQuantity };
}
1.3. エンティティのライフサイクルと状態管理
エンティティは状態を持ち、遷移することがあります。その際、状態パターン(State Pattern) を適用すると管理が容易になります。
interface OrderState {
processOrder(order: Order): void;
}
class PendingState implements OrderState {
processOrder(order: Order): void {
order.setState(new ShippedState());
}
}
class ShippedState implements OrderState {
processOrder(order: Order): void {
order.setState(new DeliveredState());
}
}
class DeliveredState implements OrderState {
processOrder(order: Order): void {
console.log("Order already delivered.");
}
}
class Order {
private state: OrderState = new PendingState();
constructor(public id: string) {}
setState(state: OrderState) {
this.state = state;
}
process() {
this.state.processOrder(this);
}
}
2. 戦略パターンの適用
2.1. 戦略の選択基準(関数 vs クラス)
- 状態を持たない戦略 → 関数で実装
- 状態を持つ戦略 → クラスで実装
2.2. 戦略パターンの実装
関数を使う場合:
type ShippingStrategy = (orderTotal: number) => number;
const standardShipping: ShippingStrategy = (orderTotal) => (orderTotal > 5000 ? 0 : 500);
クラスを使う場合:
interface DiscountStrategy {
applyDiscount(price: number): number;
}
class LimitedDiscount implements DiscountStrategy {
private remainingUses: number;
constructor(private discountRate: number, maxUses: number) {
this.remainingUses = maxUses;
}
applyDiscount(price: number): number {
if (this.remainingUses <= 0) return price;
this.remainingUses--;
return price * (1 - this.discountRate);
}
}
3. 不変オブジェクトの設計
3.1. クラス vs 型
- シンプルなデータ構造 →
type
/interface
- 振る舞いを持つオブジェクト →
class
例(type
を使う場合):
type OrderItem = {
readonly productId: string;
readonly price: number;
readonly quantity: number;
};
例(class
を使う場合):
class OrderItem {
constructor(
public readonly productId: string,
public readonly price: number,
public readonly quantity: number
) {}
updateQuantity(newQuantity: number): OrderItem {
return new OrderItem(this.productId, this.price, newQuantity);
}
}
4. 依存性の管理と DI(Dependency Injection)
4.1. DI なしの設計(コンストラクタ注入)
class ShippingService {
calculateShipping(orderTotal: number): number {
return orderTotal > 5000 ? 0 : 500;
}
}
class Order {
constructor(public total: number, private shippingService: ShippingService) {}
getShippingCost(): number {
return this.shippingService.calculateShipping(this.total);
}
}
4.2. DI コンテナ(tsyringe)の導入
import { injectable, inject, container } from "tsyringe";
@injectable()
class ShippingService {
calculateShipping(orderTotal: number): number {
return orderTotal > 5000 ? 0 : 500;
}
}
@injectable()
class Order {
constructor(
public total: number,
@inject(ShippingService) private shippingService: ShippingService
) {}
getShippingCost(): number {
return this.shippingService.calculateShipping(this.total);
}
}
5. まとめ
- 主要エンティティは「アイデンティティ」「ビジネスロジック」「状態管理」の3軸で選定
- 値オブジェクトは
type
/interface
で表現し、不変性を担保する - 状態パターンを適用し、状態管理を明確にする
- 戦略パターンを利用し、実装の柔軟性を確保
- 依存関係はコンストラクタ注入を基本とし、必要に応じて DI コンテナを導入