目次
1. なぜ2025年でもデザインパターンが重要(じゅうよう)なのか
1994年にGoF(Gang of Four)が発表(はっぴょう)した23のデザインパターンは、30年以上経(た)った今(いま)でもソフトウェア設計(せっけい)の基盤(きばん)を成(な)している。言語(げんご)は進化(しんか)しパラダイムは変(か)わったが、パターンが解決(かいけつ)する**根本的(こんぽんてき)な問題(もんだい)**は変(か)わっていない。
モダン言語でパターンが依然(いぜん)として重要な理由(りゆう):
- 共通(きょうつう)の語彙(ごい) — 「ここにStrategyパターンを適用(てきよう)しよう」と言えば、チーム全体(ぜんたい)が即座(そくざ)に理解(りかい)する
- 実証済(じっしょうず)みの解決策(かいけつさく) — 数十年(すうじゅうねん)にわたり数百万(すうひゃくまん)のプロジェクトで検証(けんしょう)された設計手法(しゅほう)
- フレームワーク理解(りかい) — Spring、NestJS、Djangoなどすべての主要(しゅよう)フレームワークがパターンに基(もと)づいて設計されている
- リファクタリングの指針(ししん) — コードスメルを発見(はっけん)した時(とき)、どのパターンで改善(かいぜん)するか方向(ほうこう)を示(しめ)す
ただし、すべてのパターンを無条件(むじょうけん)に適用するのは逆効果(ぎゃくこうか)である。パターンは問題がある時に適用するツールであり、コードを複雑(ふくざつ)にする儀式(ぎしき)ではない。
GoF 23パターン分類
生成(Creational) 5個: オブジェクト生成メカニズム
├── Factory Method
├── Abstract Factory
├── Builder
├── Singleton
└── Prototype
構造(Structural) 7個: オブジェクトの合成/構造化
├── Adapter
├── Bridge
├── Composite
├── Decorator
├── Facade
├── Flyweight
└── Proxy
振る舞い(Behavioral) 11個: オブジェクト間の相互作用
├── Chain of Responsibility
├── Command
├── Iterator
├── Mediator
├── Memento
├── Observer
├── State
├── Strategy
├── Template Method
├── Visitor
└── Interpreter
本記事(ほんきじ)では、実務(じつむ)で最(もっと)もよく使(つか)われる15のGoFパターン + 5つのモダンパターンをTypeScript、Python、Goで実装(じっそう)しながら学(まな)ぶ。
2. SOLID原則(げんそく): パターンの基盤(きばん)
デザインパターンを理解する前(まえ)に、SOLID原則を確認(かくにん)しよう。ほとんどのパターンは、これらの原則を実現(じつげん)するための具体的(ぐたいてき)な方法(ほうほう)である。
2.1 SRP — 単一責任(たんいつせきにん)の原則
クラスは変更(へんこう)の理由(りゆう)が一(ひと)つだけであるべきだ。
// Bad: 複数の責任が混在
class UserService {
createUser(data: UserData): User { /* ... */ }
sendWelcomeEmail(user: User): void { /* ... */ }
generateReport(users: User[]): PDF { /* ... */ }
}
// Good: 責任を分離
class UserService {
createUser(data: UserData): User { /* ... */ }
}
class EmailService {
sendWelcomeEmail(user: User): void { /* ... */ }
}
class ReportService {
generateReport(users: User[]): PDF { /* ... */ }
}
2.2 OCP — 開放閉鎖(かいほうへいさ)の原則
拡張(かくちょう)に対(たい)して開(ひら)かれ、修正(しゅうせい)に対(たい)して閉(と)じていなければならない。Strategy、Decorator、Observerパターンがこの原則を実現する。
# Bad: 新しい割引タイプごとにコード修正が必要
def calculate_discount(order, discount_type):
if discount_type == "percentage":
return order.total * 0.1
elif discount_type == "fixed":
return 10.0
# 新しい割引追加時にここを修正する必要がある
# Good: 新しい割引タイプを追加するだけ
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, order) -> float:
pass
class PercentageDiscount(DiscountStrategy):
def __init__(self, rate: float):
self.rate = rate
def calculate(self, order) -> float:
return order.total * self.rate
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def calculate(self, order) -> float:
return self.amount
2.3 LSP — リスコフの置換原則(ちかんげんそく)
サブタイプはスーパータイプを置換(ちかん)できなければならない。正方形(せいほうけい)が長方形(ちょうほうけい)を継承(けいしょう)すべきでない理由がまさにこれである。
2.4 ISP — インターフェース分離(ぶんり)の原則
クライアントは使用(しよう)しないメソッドに依存(いぞん)してはならない。
// Bad: 巨大なインターフェース
type Worker interface {
Work()
Eat()
Sleep()
}
// Good: 分離されたインターフェース
type Workable interface {
Work()
}
type Eatable interface {
Eat()
}
// Goではインターフェース合成が自然
type HumanWorker interface {
Workable
Eatable
}
2.5 DIP — 依存性逆転(いぞんせいぎゃくてん)の原則
高水準(こうすいじゅん)モジュールは低水準(ていすいじゅん)モジュールに依存してはならない。両方(りょうほう)とも抽象(ちゅうしょう)に依存すべきである。Dependency Injectionの理論的(りろんてき)根拠(こんきょ)である。
3. 生成パターン (Creational Patterns)
3.1 Factory Methodパターン
問題: オブジェクト生成ロジックがクライアントコードに直接埋(う)め込(こ)まれていると、新(あたら)しいタイプ追加時(ついかじ)にすべての生成コードを修正する必要がある。
解決: オブジェクト生成をサブクラスまたはファクトリ関数(かんすう)に委譲(いじょう)する。
// TypeScript Factory Method
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[Console] ${message}`);
}
}
class FileLogger implements Logger {
log(message: string): void {
console.log(`[File] ${message}`);
}
}
class CloudLogger implements Logger {
log(message: string): void {
console.log(`[Cloud] ${message}`);
}
}
type LoggerType = "console" | "file" | "cloud";
function createLogger(type: LoggerType): Logger {
const loggers: Record<LoggerType, () => Logger> = {
console: () => new ConsoleLogger(),
file: () => new FileLogger(),
cloud: () => new CloudLogger(),
};
const factory = loggers[type];
if (!factory) {
throw new Error(`Unknown logger type: ${type}`);
}
return factory();
}
const logger = createLogger("cloud");
logger.log("Application started");
# Python Factory Method
from abc import ABC, abstractmethod
from enum import Enum
class NotificationType(Enum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
SLACK = "slack"
class Notification(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> bool:
pass
class EmailNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending email to {recipient}: {message}")
return True
class SMSNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending SMS to {recipient}: {message}")
return True
class PushNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending push to {recipient}: {message}")
return True
class SlackNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending Slack to {recipient}: {message}")
return True
class NotificationFactory:
_registry: dict[NotificationType, type[Notification]] = {
NotificationType.EMAIL: EmailNotification,
NotificationType.SMS: SMSNotification,
NotificationType.PUSH: PushNotification,
NotificationType.SLACK: SlackNotification,
}
@classmethod
def register(cls, ntype: NotificationType, klass: type[Notification]):
cls._registry[ntype] = klass
@classmethod
def create(cls, ntype: NotificationType) -> Notification:
klass = cls._registry.get(ntype)
if not klass:
raise ValueError(f"Unknown notification type: {ntype}")
return klass()
notifier = NotificationFactory.create(NotificationType.SLACK)
notifier.send("team-channel", "Deploy complete!")
使用(しよう)するタイミング: 生成するオブジェクトタイプを実行時(じっこうじ)に決定(けってい)する必要がある時、生成ロジックを集約(しゅうやく)したい時。
使用しない方がよい時: オブジェクトタイプが一つしかなく変更の可能性(かのうせい)がない時。不必要(ふひつよう)な抽象化は複雑性を増(ま)すだけである。
3.2 Abstract Factoryパターン
問題: 関連(かんれん)するオブジェクト群(ぐん)を一貫性(いっかんせい)を持(も)って生成する必要がある。
解決: 関連オブジェクトのファクトリを抽象化する。
// TypeScript Abstract Factory — UIテーマシステム
interface Button {
render(): string;
}
interface Input {
render(): string;
}
interface UIFactory {
createButton(label: string): Button;
createInput(placeholder: string): Input;
}
class LightButton implements Button {
constructor(private label: string) {}
render() { return `<button class="bg-white text-black">${this.label}</button>`; }
}
class LightInput implements Input {
constructor(private placeholder: string) {}
render() { return `<input class="border-gray-300" placeholder="${this.placeholder}" />`; }
}
class LightThemeFactory implements UIFactory {
createButton(label: string) { return new LightButton(label); }
createInput(placeholder: string) { return new LightInput(placeholder); }
}
class DarkButton implements Button {
constructor(private label: string) {}
render() { return `<button class="bg-gray-800 text-white">${this.label}</button>`; }
}
class DarkInput implements Input {
constructor(private placeholder: string) {}
render() { return `<input class="border-gray-600 bg-gray-700" placeholder="${this.placeholder}" />`; }
}
class DarkThemeFactory implements UIFactory {
createButton(label: string) { return new DarkButton(label); }
createInput(placeholder: string) { return new DarkInput(placeholder); }
}
// テーマ切り替えが1行で可能
function buildUI(factory: UIFactory) {
const btn = factory.createButton("Submit");
const input = factory.createInput("Enter name...");
return { btn, input };
}
const ui = buildUI(new DarkThemeFactory());
3.3 Builderパターン
問題: コンストラクタパラメータが多(おお)いオブジェクトの生成時、どのパラメータが何(なに)を意味(いみ)するか分(わ)かりにくい(Telescoping Constructor問題)。
解決: 段階的(だんかいてき)にオブジェクトを構築(こうちく)するビルダーを提供(ていきょう)する。
// TypeScript Builder
interface HttpRequest {
method: string;
url: string;
headers: Record<string, string>;
body?: string;
timeout: number;
retries: number;
}
class HttpRequestBuilder {
private request: Partial<HttpRequest> = {
method: "GET",
headers: {},
timeout: 30000,
retries: 0,
};
setMethod(method: string): this {
this.request.method = method;
return this;
}
setUrl(url: string): this {
this.request.url = url;
return this;
}
addHeader(key: string, value: string): this {
this.request.headers![key] = value;
return this;
}
setBody(body: string): this {
this.request.body = body;
return this;
}
setTimeout(ms: number): this {
this.request.timeout = ms;
return this;
}
setRetries(count: number): this {
this.request.retries = count;
return this;
}
build(): HttpRequest {
if (!this.request.url) {
throw new Error("URL is required");
}
return this.request as HttpRequest;
}
}
// 使用: 可読性が優れている
const request = new HttpRequestBuilder()
.setMethod("POST")
.setUrl("https://api.example.com/users")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer token123")
.setBody(JSON.stringify({ name: "John" }))
.setTimeout(5000)
.setRetries(3)
.build();
# Python Builder — SQLクエリビルダー
from dataclasses import dataclass, field
from typing import Optional
class QueryBuilder:
def __init__(self, table: str):
self._table = table
self._columns: list[str] = ["*"]
self._conditions: list[str] = []
self._order_by: Optional[str] = None
self._limit: Optional[int] = None
self._offset: int = 0
def select(self, *columns: str) -> "QueryBuilder":
self._columns = list(columns)
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def order_by(self, column: str, desc: bool = False) -> "QueryBuilder":
direction = "DESC" if desc else "ASC"
self._order_by = f"{column} {direction}"
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
return self
def offset(self, n: int) -> "QueryBuilder":
self._offset = n
return self
def to_sql(self) -> str:
cols = ", ".join(self._columns)
sql = f"SELECT {cols} FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit:
sql += f" LIMIT {self._limit}"
if self._offset:
sql += f" OFFSET {self._offset}"
return sql
query = (
QueryBuilder("users")
.select("id", "name", "email")
.where("status = 'active'")
.where("age > 18")
.order_by("created_at", desc=True)
.limit(20)
.offset(40)
)
print(query.to_sql())
3.4 Singletonパターン
問題: 特定(とくてい)のクラスのインスタンスが一つだけ存在(そんざい)しなければならない。
解決: 生成を制限(せいげん)し、グローバルアクセスポイントを提供する。
// TypeScript Singleton
class ConfigManager {
private static instance: ConfigManager | null = null;
private config: Map<string, unknown> = new Map();
private constructor() {}
static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
get<T>(key: string): T | undefined {
return this.config.get(key) as T | undefined;
}
set(key: string, value: unknown): void {
this.config.set(key, value);
}
}
const config = ConfigManager.getInstance();
config.set("database.host", "localhost");
// Go Singleton — sync.Once活用
package config
import "sync"
type AppConfig struct {
DatabaseURL string
RedisURL string
Port int
}
var (
instance *AppConfig
once sync.Once
)
func GetConfig() *AppConfig {
once.Do(func() {
instance = &AppConfig{
DatabaseURL: "postgres://localhost:5432/app",
RedisURL: "redis://localhost:6379",
Port: 8080,
}
})
return instance
}
注意(ちゅうい): Singletonはグローバル状態(じょうたい)を作るためテストが困難(こんなん)になる。DIコンテナで「シングルスコープ」として管理(かんり)する方(ほう)がモダンなアプローチである。
3.5 Prototypeパターン
問題: 複雑なオブジェクトをゼロから生成するコストが大(おお)きい。
解決: 既存(きそん)のオブジェクトを複製(ふくせい)(clone)して新しいオブジェクトを作る。
// TypeScript Prototype
interface Cloneable<T> {
clone(): T;
}
class GameCharacter implements Cloneable<GameCharacter> {
constructor(
public name: string,
public health: number,
public attack: number,
public defense: number,
public skills: string[],
public inventory: Map<string, number>,
) {}
clone(): GameCharacter {
return new GameCharacter(
this.name,
this.health,
this.attack,
this.defense,
[...this.skills],
new Map(this.inventory),
);
}
}
const archetypes = {
warrior: new GameCharacter("Warrior", 200, 30, 50, ["Slash", "Shield"], new Map([["potion", 3]])),
mage: new GameCharacter("Mage", 100, 60, 20, ["Fireball", "Heal"], new Map([["mana_potion", 5]])),
};
const myWarrior = archetypes.warrior.clone();
myWarrior.name = "Aragorn";
myWarrior.skills.push("Charge");
4. 構造パターン (Structural Patterns)
4.1 Adapterパターン
問題: インターフェースが合(あ)わない既存(きそん)クラスを使用する必要がある。
解決: 変換(へんかん)レイヤー(アダプター)を間(あいだ)に置(お)く。
// TypeScript Adapter — 外部決済ライブラリ統合
interface PaymentProcessor {
charge(amount: number, currency: string): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
}
interface PaymentResult {
success: boolean;
transactionId: string;
}
interface RefundResult {
success: boolean;
refundId: string;
}
// 外部ライブラリ(変更不可)
class StripeSDK {
async createCharge(params: {
amount_cents: number;
currency: string;
}): Promise<{ id: string; status: string }> {
return { id: "ch_123", status: "succeeded" };
}
async createRefund(params: {
charge: string;
amount_cents: number;
}): Promise<{ id: string; status: string }> {
return { id: "re_456", status: "succeeded" };
}
}
// Adapter
class StripeAdapter implements PaymentProcessor {
private stripe: StripeSDK;
constructor() {
this.stripe = new StripeSDK();
}
async charge(amount: number, currency: string): Promise<PaymentResult> {
const result = await this.stripe.createCharge({
amount_cents: Math.round(amount * 100),
currency: currency.toLowerCase(),
});
return {
success: result.status === "succeeded",
transactionId: result.id,
};
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const result = await this.stripe.createRefund({
charge: transactionId,
amount_cents: Math.round(amount * 100),
});
return {
success: result.status === "succeeded",
refundId: result.id,
};
}
}
const processor: PaymentProcessor = new StripeAdapter();
const result = await processor.charge(29.99, "USD");
4.2 Decoratorパターン
問題: オブジェクトに動的(どうてき)に新しい責任を追加する必要があるが、サブクラス化(か)は柔軟(じゅうなん)ではない。
解決: ラッパーオブジェクトで包(つつ)んで機能(きのう)を追加する。
# Python Decorator — ロギング、キャッシュ、リトライの合成
import functools
import time
import logging
from typing import Callable, TypeVar, ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
def with_logging(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
logging.info(f"Calling {func.__name__}")
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
logging.info(f"{func.__name__} returned in {elapsed:.3f}s")
return result
return wrapper
def with_retry(max_retries: int = 3, delay: float = 1.0):
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_error: Exception | None = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
if attempt < max_retries - 1:
time.sleep(delay * (2 ** attempt))
raise last_error # type: ignore
return wrapper
return decorator
def with_cache(ttl_seconds: int = 300):
cache: dict[str, tuple[float, object]] = {}
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
key = f"{args}-{kwargs}"
if key in cache:
cached_time, cached_value = cache[key]
if time.time() - cached_time < ttl_seconds:
return cached_value # type: ignore
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
return result
return wrapper
return decorator
@with_logging
@with_retry(max_retries=3, delay=0.5)
@with_cache(ttl_seconds=60)
def fetch_user_profile(user_id: int) -> dict:
import requests
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
4.3 Facadeパターン
問題: 複雑なサブシステムを使用するために多(おお)くのクラスとメソッドを知(し)る必要がある。
解決: 単純化(たんじゅんか)されたインターフェースを提供するFacadeを作る。
// TypeScript Facade — 注文処理システム
class InventoryService {
checkStock(productId: string): boolean { return true; }
reserveStock(productId: string, qty: number): void { /* ... */ }
}
class PaymentService {
processPayment(userId: string, amount: number): string { return "txn_789"; }
}
class ShippingService {
calculateShipping(address: string): number { return 5.99; }
createShipment(orderId: string, address: string): string { return "ship_101"; }
}
class NotificationService {
sendOrderConfirmation(email: string, orderId: string): void { /* ... */ }
}
// Facade
class OrderFacade {
private inventory = new InventoryService();
private payment = new PaymentService();
private shipping = new ShippingService();
private notification = new NotificationService();
async placeOrder(order: {
userId: string;
email: string;
productId: string;
quantity: number;
address: string;
amount: number;
}): Promise<{ orderId: string; trackingId: string }> {
if (!this.inventory.checkStock(order.productId)) {
throw new Error("Out of stock");
}
this.inventory.reserveStock(order.productId, order.quantity);
const shippingCost = this.shipping.calculateShipping(order.address);
this.payment.processPayment(order.userId, order.amount + shippingCost);
const orderId = `ORD-${Date.now()}`;
const trackingId = this.shipping.createShipment(orderId, order.address);
this.notification.sendOrderConfirmation(order.email, orderId);
return { orderId, trackingId };
}
}
const orderService = new OrderFacade();
4.4 Proxyパターン
問題: オブジェクトへのアクセスを制御(せいぎょ)する必要がある(遅延(ちえん)ロード、アクセス制御、ロギングなど)。
解決: 実際(じっさい)のオブジェクトを包むプロキシを提供する。
// TypeScript Proxy — 遅延ロード + アクセス制御
interface Database {
query(sql: string): Promise<unknown[]>;
execute(sql: string): Promise<void>;
}
class PostgresDatabase implements Database {
constructor(connectionString: string) {
console.log("Establishing DB connection pool...");
}
async query(sql: string): Promise<unknown[]> {
console.log(`Executing query: ${sql}`);
return [];
}
async execute(sql: string): Promise<void> {
console.log(`Executing: ${sql}`);
}
}
class DatabaseProxy implements Database {
private db: PostgresDatabase | null = null;
constructor(
private connectionString: string,
private userRole: string,
) {}
private getDb(): PostgresDatabase {
if (!this.db) {
this.db = new PostgresDatabase(this.connectionString);
}
return this.db;
}
async query(sql: string): Promise<unknown[]> {
return this.getDb().query(sql);
}
async execute(sql: string): Promise<void> {
if (this.userRole !== "admin") {
throw new Error("Only admin can execute write operations");
}
return this.getDb().execute(sql);
}
}
4.5 Compositeパターン
問題: ツリー構造(こうぞう)のオブジェクトを個別(こべつ)オブジェクトと同(おな)じように扱(あつか)いたい。
解決: 単一オブジェクトと複合(ふくごう)オブジェクトが同じインターフェースを実装する。
# Python Composite — ファイルシステム
from abc import ABC, abstractmethod
class FileSystemItem(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def display(self, indent: int = 0) -> str:
pass
class File(FileSystemItem):
def __init__(self, name: str, size: int):
super().__init__(name)
self.size = size
def get_size(self) -> int:
return self.size
def display(self, indent: int = 0) -> str:
return " " * indent + f"File: {self.name} ({self.size} bytes)"
class Directory(FileSystemItem):
def __init__(self, name: str):
super().__init__(name)
self.children: list[FileSystemItem] = []
def add(self, item: FileSystemItem) -> "Directory":
self.children.append(item)
return self
def get_size(self) -> int:
return sum(child.get_size() for child in self.children)
def display(self, indent: int = 0) -> str:
lines = [" " * indent + f"Dir: {self.name} ({self.get_size()} bytes)"]
for child in self.children:
lines.append(child.display(indent + 2))
return "\n".join(lines)
root = Directory("project")
src = Directory("src")
src.add(File("main.ts", 2048))
src.add(File("utils.ts", 1024))
root.add(src)
root.add(File("package.json", 512))
print(root.display())
5. 振(ふ)る舞(ま)いパターン (Behavioral Patterns)
5.1 Strategyパターン
問題: アルゴリズムを実行時に交換(こうかん)する必要がある。if-elseやswitchチェーンが増(ふ)え続(つづ)ける。
解決: アルゴリズムをカプセル化して交換可能にする。
// TypeScript Strategy — 価格計算戦略
interface PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number;
getName(): string;
}
class RegularPricing implements PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity;
}
getName() { return "Regular"; }
}
class BulkPricing implements PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number {
if (quantity >= 100) return basePrice * quantity * 0.7;
if (quantity >= 50) return basePrice * quantity * 0.8;
if (quantity >= 10) return basePrice * quantity * 0.9;
return basePrice * quantity;
}
getName() { return "Bulk"; }
}
class SubscriptionPricing implements PricingStrategy {
constructor(private discountRate: number) {}
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity * (1 - this.discountRate);
}
getName() { return "Subscription"; }
}
class ShoppingCart {
private items: Array<{ name: string; price: number; quantity: number }> = [];
private strategy: PricingStrategy = new RegularPricing();
setPricingStrategy(strategy: PricingStrategy): void {
this.strategy = strategy;
}
addItem(name: string, price: number, quantity: number): void {
this.items.push({ name, price, quantity });
}
calculateTotal(): number {
return this.items.reduce(
(total, item) => total + this.strategy.calculatePrice(item.price, item.quantity),
0,
);
}
}
const cart = new ShoppingCart();
cart.addItem("Widget", 10, 100);
cart.setPricingStrategy(new RegularPricing());
console.log(`Regular: $${cart.calculateTotal()}`); // 1000
cart.setPricingStrategy(new BulkPricing());
console.log(`Bulk: $${cart.calculateTotal()}`); // 700
5.2 Observerパターン
問題: あるオブジェクトの状態変更を他(ほか)のオブジェクトが自動的(じどうてき)に検知(けんち)する必要がある。
解決: パブリッシュ・サブスクライブ(Pub/Sub)メカニズムを実装する。
// TypeScript Observer — 型安全なイベントシステム
type EventMap = {
"user:created": { userId: string; email: string };
"user:updated": { userId: string; changes: Record<string, unknown> };
"order:placed": { orderId: string; userId: string; total: number };
};
type EventHandler<T> = (data: T) => void | Promise<void>;
class EventEmitter {
private handlers = new Map<string, Set<EventHandler<any>>>();
on<K extends keyof EventMap>(event: K, handler: EventHandler<EventMap[K]>): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
return () => { this.handlers.get(event)?.delete(handler); };
}
async emit<K extends keyof EventMap>(event: K, data: EventMap[K]): Promise<void> {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers) return;
const promises = Array.from(eventHandlers).map((h) => Promise.resolve(h(data)));
await Promise.allSettled(promises);
}
}
const events = new EventEmitter();
events.on("user:created", async (data) => {
console.log(`Send welcome email to ${data.email}`);
});
events.on("order:placed", async (data) => {
console.log(`Process payment for order ${data.orderId}: $${data.total}`);
});
await events.emit("user:created", { userId: "u_1", email: "user@example.com" });
5.3 Commandパターン
問題: リクエストをオブジェクトとしてカプセル化し、キューイング、ロギング、元(もと)に戻(もど)す(undo)機能を実装する必要がある。
解決: 各操作(そうさ)をCommandオブジェクトにする。
# Python Command — Undo/Redo対応テキストエディタ
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
@abstractmethod
def describe(self) -> str:
pass
class TextEditor:
def __init__(self):
self.content: list[str] = []
@property
def text(self) -> str:
return "".join(self.content)
def insert_at(self, position: int, text: str) -> None:
for i, char in enumerate(text):
self.content.insert(position + i, char)
def delete_range(self, start: int, length: int) -> str:
deleted = self.content[start:start + length]
del self.content[start:start + length]
return "".join(deleted)
class InsertCommand(Command):
def __init__(self, editor: TextEditor, position: int, text: str):
self.editor = editor
self.position = position
self.text = text
def execute(self) -> None:
self.editor.insert_at(self.position, self.text)
def undo(self) -> None:
self.editor.delete_range(self.position, len(self.text))
def describe(self) -> str:
return f"Insert '{self.text}' at position {self.position}"
@dataclass
class CommandHistory:
undo_stack: list[Command] = field(default_factory=list)
redo_stack: list[Command] = field(default_factory=list)
def execute(self, command: Command) -> None:
command.execute()
self.undo_stack.append(command)
self.redo_stack.clear()
def undo(self) -> str | None:
if not self.undo_stack:
return None
command = self.undo_stack.pop()
command.undo()
self.redo_stack.append(command)
return command.describe()
def redo(self) -> str | None:
if not self.redo_stack:
return None
command = self.redo_stack.pop()
command.execute()
self.undo_stack.append(command)
return command.describe()
editor = TextEditor()
history = CommandHistory()
history.execute(InsertCommand(editor, 0, "Hello World"))
print(editor.text) # "Hello World"
history.undo()
print(editor.text) # ""
5.4 Stateパターン
問題: オブジェクトの振る舞いが内部(ないぶ)状態によって完全(かんぜん)に異(こと)なる。巨大(きょだい)なif-else/switchが生(う)まれる。
解決: 各状態を別々(べつべつ)のクラスに分離する。
// TypeScript State — 注文状態マシン
interface OrderState {
name: string;
confirm(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
class PendingState implements OrderState {
name = "Pending";
confirm(order: Order) { order.setState(new ConfirmedState()); }
ship(_order: Order) { throw new Error("Cannot ship a pending order"); }
deliver(_order: Order) { throw new Error("Cannot deliver a pending order"); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ConfirmedState implements OrderState {
name = "Confirmed";
confirm(_order: Order) { throw new Error("Already confirmed"); }
ship(order: Order) { order.setState(new ShippedState()); }
deliver(_order: Order) { throw new Error("Cannot deliver before shipping"); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ShippedState implements OrderState {
name = "Shipped";
confirm() { throw new Error("Already shipped"); }
ship() { throw new Error("Already shipped"); }
deliver(order: Order) { order.setState(new DeliveredState()); }
cancel() { throw new Error("Cannot cancel shipped order"); }
}
class DeliveredState implements OrderState {
name = "Delivered";
confirm() { throw new Error("Already delivered"); }
ship() { throw new Error("Already delivered"); }
deliver() { throw new Error("Already delivered"); }
cancel() { throw new Error("Cannot cancel delivered order"); }
}
class CancelledState implements OrderState {
name = "Cancelled";
confirm() { throw new Error("Cancelled"); }
ship() { throw new Error("Cancelled"); }
deliver() { throw new Error("Cancelled"); }
cancel() { throw new Error("Already cancelled"); }
}
class Order {
private state: OrderState = new PendingState();
setState(state: OrderState): void {
console.log(`${this.state.name} -> ${state.name}`);
this.state = state;
}
getState(): string { return this.state.name; }
confirm() { this.state.confirm(this); }
ship() { this.state.ship(this); }
deliver() { this.state.deliver(this); }
cancel() { this.state.cancel(this); }
}
const order = new Order();
order.confirm(); // Pending -> Confirmed
order.ship(); // Confirmed -> Shipped
order.deliver(); // Shipped -> Delivered
5.5 Template Methodパターン
問題: アルゴリズムの骨格(こっかく)は同一だが、一部(いちぶ)のステップだけが異なる。
解決: 基本(きほん)アルゴリズムを親(おや)クラスで定義し、変(か)わるステップだけを子クラスでオーバーライドする。
# Python Template Method — データ処理パイプライン
from abc import ABC, abstractmethod
from typing import Any
import json
class DataProcessor(ABC):
def process(self, source: str) -> dict[str, Any]:
raw_data = self.extract(source)
validated = self.validate(raw_data)
transformed = self.transform(validated)
result = self.load(transformed)
return result
@abstractmethod
def extract(self, source: str) -> list[dict]:
pass
def validate(self, data: list[dict]) -> list[dict]:
return [row for row in data if row]
@abstractmethod
def transform(self, data: list[dict]) -> list[dict]:
pass
def load(self, data: list[dict]) -> dict[str, Any]:
return {"records": len(data), "data": data}
class JSONProcessor(DataProcessor):
def extract(self, source: str) -> list[dict]:
return json.loads(source)
def transform(self, data: list[dict]) -> list[dict]:
return [{k.lower(): v for k, v in row.items()} for row in data]
6. モダンパターン (Modern Patterns)
6.1 Repositoryパターン
データアクセスロジックをビジネスロジックから分離する。
// TypeScript Repository
interface Repository<T, ID> {
findById(id: ID): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(entity: Omit<T, "id">): Promise<T>;
update(id: ID, data: Partial<T>): Promise<T>;
delete(id: ID): Promise<boolean>;
}
interface User {
id: string;
name: string;
email: string;
role: string;
}
// インメモリ実装 — テスト用
class InMemoryUserRepository implements Repository<User, string> {
private users = new Map<string, User>();
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async findAll(filter?: Partial<User>): Promise<User[]> {
let results = Array.from(this.users.values());
if (filter?.role) results = results.filter((u) => u.role === filter.role);
return results;
}
async create(data: Omit<User, "id">): Promise<User> {
const user: User = { id: crypto.randomUUID(), ...data } as User;
this.users.set(user.id, user);
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = this.users.get(id);
if (!user) throw new Error("User not found");
const updated = { ...user, ...data };
this.users.set(id, updated);
return updated;
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id);
}
}
6.2 CQRS (Command Query Responsibility Segregation)
読(よ)み取(と)り(Query)と書(か)き込(こ)み(Command)モデルを分離する。
// TypeScript CQRS
interface Command { type: string; }
interface CreateOrderCommand extends Command {
type: "CreateOrder";
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
}
interface CancelOrderCommand extends Command {
type: "CancelOrder";
orderId: string;
reason: string;
}
type OrderCommand = CreateOrderCommand | CancelOrderCommand;
class OrderCommandHandler {
async handle(command: OrderCommand): Promise<void> {
switch (command.type) {
case "CreateOrder":
const total = command.items.reduce((s, i) => s + i.price * i.quantity, 0);
console.log(`Order created, total: ${total}`);
break;
case "CancelOrder":
console.log(`Order ${command.orderId} cancelled: ${command.reason}`);
break;
}
}
}
// Query — 読み取り専用の最適化モデル
interface OrderSummary {
orderId: string;
userName: string;
totalAmount: number;
status: string;
}
class OrderQueryService {
async getOrderSummary(orderId: string): Promise<OrderSummary | null> {
return null;
}
}
6.3 Circuit Breakerパターン
外部(がいぶ)サービスの障害(しょうがい)がシステム全体(ぜんたい)に伝播(でんぱ)するのを防(ふせ)ぐ。
// TypeScript Circuit Breaker
enum CircuitState {
CLOSED = "CLOSED",
OPEN = "OPEN",
HALF_OPEN = "HALF_OPEN",
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime = 0;
private successCount = 0;
constructor(
private readonly failureThreshold: number = 5,
private readonly recoveryTimeout: number = 30000,
private readonly halfOpenMaxAttempts: number = 3,
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new Error("Circuit is OPEN");
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenMaxAttempts) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
}
} else {
this.failureCount = 0;
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === CircuitState.HALF_OPEN) {
this.state = CircuitState.OPEN;
} else if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
}
}
}
const breaker = new CircuitBreaker(3, 10000);
6.4 Specificationパターン
ビジネスルールを再利用(さいりよう)可能なオブジェクトにカプセル化する。
# Python Specification
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
class Specification(ABC, Generic[T]):
@abstractmethod
def is_satisfied_by(self, candidate: T) -> bool:
pass
def and_spec(self, other: "Specification[T]") -> "Specification[T]":
return AndSpecification(self, other)
def or_spec(self, other: "Specification[T]") -> "Specification[T]":
return OrSpecification(self, other)
class AndSpecification(Specification[T]):
def __init__(self, left: Specification[T], right: Specification[T]):
self.left = left
self.right = right
def is_satisfied_by(self, candidate: T) -> bool:
return (self.left.is_satisfied_by(candidate)
and self.right.is_satisfied_by(candidate))
class OrSpecification(Specification[T]):
def __init__(self, left: Specification[T], right: Specification[T]):
self.left = left
self.right = right
def is_satisfied_by(self, candidate: T) -> bool:
return (self.left.is_satisfied_by(candidate)
or self.right.is_satisfied_by(candidate))
@dataclass
class Product:
name: str
price: float
in_stock: bool
rating: float
class InStockSpec(Specification[Product]):
def is_satisfied_by(self, p: Product) -> bool:
return p.in_stock
class PriceRangeSpec(Specification[Product]):
def __init__(self, min_p: float, max_p: float):
self.min_p = min_p
self.max_p = max_p
def is_satisfied_by(self, p: Product) -> bool:
return self.min_p <= p.price <= self.max_p
class HighRatedSpec(Specification[Product]):
def __init__(self, min_rating: float = 4.0):
self.min_rating = min_rating
def is_satisfied_by(self, p: Product) -> bool:
return p.rating >= self.min_rating
affordable_and_good = (
PriceRangeSpec(10, 50)
.and_spec(HighRatedSpec(4.5))
.and_spec(InStockSpec())
)
products = [
Product("Widget A", 25.0, True, 4.7),
Product("Widget B", 15.0, True, 3.2),
Product("Widget D", 45.0, True, 4.9),
]
filtered = [p for p in products if affordable_and_good.is_satisfied_by(p)]
print([p.name for p in filtered]) # ['Widget A', 'Widget D']
6.5 Unit of Workパターン
複数(ふくすう)のリポジトリにまたがる変更を一(ひと)つのトランザクションにまとめる。
# Python Unit of Work
from contextlib import contextmanager
from typing import Generator
class UnitOfWork:
def __init__(self, session_factory):
self.session_factory = session_factory
@contextmanager
def transaction(self) -> Generator:
session = self.session_factory()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
7. アンチパターン (Anti-Patterns)
避(さ)けるべき悪(わる)い設計パターン。
7.1 God Object(ゴッドオブジェクト)
すべてを知(し)り、すべてを行(おこな)う巨大クラス。SRPに完全に違反(いはん)する。
7.2 Spaghetti Code(スパゲッティコード)
フローを追跡(ついせき)しにくい絡(から)み合(あ)ったコード。
7.3 Golden Hammer(ゴールデンハンマー)
すべての問題に一(ひと)つのツール(パターン)だけを適用すること。
7.4 Premature Optimization(早(はや)すぎる最適化(さいてきか))
まだボトルネックではない箇所(かしょ)を最適化してコードを複雑にすること。
7.5 Copy-Paste Programming
コードをコピーして少(すこ)しだけ修正すること。Template MethodやStrategyで解決できる。
8. パターン選択(せんたく)ガイド
問題状況 推奨パターン
──────────────────────────────────────────────────
オブジェクト生成が複雑 Builder
複数アルゴリズムから選択 Strategy
状態によって振る舞いが変わる State
インターフェース不一致の統合 Adapter
機能を動的に追加 Decorator
複雑なシステムの単純化 Facade
イベントベースの通信 Observer
操作の取り消しが必要 Command
データアクセスの抽象化 Repository
外部サービス障害の隔離 Circuit Breaker
9. クイズ
Q1. Factory MethodとAbstract Factoryの違いは?
Factory Methodは一つの製品(せいひん)を作るメソッドをオーバーライドするパターンで、Abstract Factoryは関連する製品群(ぐん)(ファミリー)を一貫性を持って作るパターンである。UIテーマシステムのようにボタン、インプット、モーダルをセットで作る必要がある時はAbstract Factoryが適している。
Q2. StrategyパターンとStateパターンの違いは?
両方とも振る舞いを委譲するが、意図(いと)が異なる。Strategyはクライアントが選択してアルゴリズムを外部から交換するもので、Stateは**内部状態遷移(せんい)**によって自動的に振る舞いが変わるものである。
Q3. DecoratorパターンとProxyパターンの違いは?
両方ともラッパーオブジェクトだが目的(もくてき)が異なる。Decoratorは機能を追加(装飾)し、Proxyはアクセスを制御する。Decoratorは複数層(そう)のネストが一般的だが、Proxyは通常一層である。
Q4. CQRSはいつ使うべきか?
読み取りと書き込みの負荷(ふか)パターンが大(おお)きく異なる時に適している。例えば書き込みは複雑なビジネスロジックが必要だが読み取りは単純な照会がほとんどの場合、それぞれを独立(どくりつ)して最適化できる。ただし複雑性が大きく増加するため、単純なCRUDアプリには過剰(かじょう)である。
Q5. Circuit Breakerの3つの状態と遷移条件は?
CLOSED(正常): リクエストが正常に通過。失敗回数が閾値(しきいち)を超えるとOPENに遷移。 OPEN(遮断): すべてのリクエストを即座(そくざ)に失敗処理。一定時間(recovery timeout)経過後HALF_OPENに遷移。 HALF_OPEN(試験): 制限された数のリクエストを許可。成功すればCLOSEDに、失敗すれば再びOPENに遷移。
10. 参考資料(さんこうしりょう)
- Gamma, E. et al. — Design Patterns: Elements of Reusable Object-Oriented Software (GoF Book)
- Martin, R.C. — Clean Architecture: A Craftsman's Guide to Software Structure and Design
- Fowler, M. — Patterns of Enterprise Application Architecture
- Refactoring Guru — https://refactoring.guru/design-patterns
- Head First Design Patterns (2nd Edition, 2020)
- Martin, R.C. — SOLID Principles Explained
- Microsoft — Cloud Design Patterns — https://learn.microsoft.com/en-us/azure/architecture/patterns/
- Nygard, M. — Release It! Design and Deploy Production-Ready Software
- Evans, E. — Domain-Driven Design: Tackling Complexity in the Heart of Software
- Fowler, M. — https://martinfowler.com/articles/enterprisePatterns.html
- Go Design Patterns — https://github.com/tmrts/go-patterns
- TypeScript Design Patterns — https://github.com/torokmark/design_patterns_in_typescript
- Python Design Patterns — https://python-patterns.guide/
현재 단락 (1/1147)
1994年にGoF(Gang of Four)が発表(はっぴょう)した23のデザインパターンは、30年以上経(た)った今(いま)でもソフトウェア設計(せっけい)の基盤(きばん)を成(な)している。言語...