デザインパターンは、ソフトウェア設計における再利用可能な解決策のカタログです。1994年のGoF(Gang of Four)本から30年、パターンの本質は変わらずとも、その適用方法は大きく進化しました。本記事では、モダンな開発環境での実践的なパターン活用法を解説します。
デザインパターンの分類
flowchart TB
subgraph DP["デザインパターン"]
subgraph Creational["生成パターン"]
C1["Singleton"]
C2["Factory Method"]
C3["Abstract Factory"]
C4["Builder"]
C5["Prototype"]
end
subgraph Structural["構造パターン"]
S1["Adapter"]
S2["Bridge"]
S3["Composite"]
S4["Decorator"]
S5["Facade"]
S6["Flyweight"]
S7["Proxy"]
end
subgraph Behavioral["振る舞いパターン"]
B1["Strategy"]
B2["Observer"]
B3["Command"]
B4["State"]
B5["Template Method"]
B6["Iterator"]
B7["Mediator"]
B8["Memento"]
B9["Visitor"]
B10["Chain of Resp."]
end
end
生成パターン
Factory Method パターン
オブジェクトの生成をサブクラスに委譲し、生成ロジックを分離します。
// モダンTypeScriptでのFactory Method実装
// 製品インターフェース
interface Notification {
send(message: string): Promise<void>;
}
// 具体的な製品
class EmailNotification implements Notification {
constructor(private email: string) {}
async send(message: string): Promise<void> {
console.log(`Email to ${this.email}: ${message}`);
// 実際のメール送信ロジック
}
}
class SlackNotification implements Notification {
constructor(private webhookUrl: string) {}
async send(message: string): Promise<void> {
console.log(`Slack webhook: ${message}`);
// Slack API呼び出し
}
}
class SMSNotification implements Notification {
constructor(private phoneNumber: string) {}
async send(message: string): Promise<void> {
console.log(`SMS to ${this.phoneNumber}: ${message}`);
// SMS送信API呼び出し
}
}
// ファクトリ(関数ベース - モダンなアプローチ)
type NotificationType = 'email' | 'slack' | 'sms';
interface NotificationConfig {
type: NotificationType;
email?: string;
webhookUrl?: string;
phoneNumber?: string;
}
function createNotification(config: NotificationConfig): Notification {
switch (config.type) {
case 'email':
if (!config.email) throw new Error('Email required');
return new EmailNotification(config.email);
case 'slack':
if (!config.webhookUrl) throw new Error('Webhook URL required');
return new SlackNotification(config.webhookUrl);
case 'sms':
if (!config.phoneNumber) throw new Error('Phone number required');
return new SMSNotification(config.phoneNumber);
default:
throw new Error(`Unknown notification type: ${config.type}`);
}
}
// 使用例
const notification = createNotification({
type: 'email',
email: 'user@example.com'
});
await notification.send('Hello!');
Builder パターン
複雑なオブジェクトの構築を段階的に行います。
// Builderパターン - Fluent API実装
interface HttpRequestConfig {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
headers: Record<string, string>;
body?: unknown;
timeout: number;
retries: number;
}
class HttpRequestBuilder {
private config: Partial<HttpRequestConfig> = {
method: 'GET',
headers: {},
timeout: 30000,
retries: 0,
};
url(url: string): this {
this.config.url = url;
return this;
}
method(method: HttpRequestConfig['method']): this {
this.config.method = method;
return this;
}
header(key: string, value: string): this {
this.config.headers = {
...this.config.headers,
[key]: value,
};
return this;
}
authorization(token: string): this {
return this.header('Authorization', `Bearer ${token}`);
}
contentType(type: string): this {
return this.header('Content-Type', type);
}
json(data: unknown): this {
this.config.body = data;
return this.contentType('application/json');
}
timeout(ms: number): this {
this.config.timeout = ms;
return this;
}
retries(count: number): this {
this.config.retries = count;
return this;
}
build(): HttpRequestConfig {
if (!this.config.url) {
throw new Error('URL is required');
}
return this.config as HttpRequestConfig;
}
// 便利メソッド - 直接実行
async execute<T>(): Promise<T> {
const config = this.build();
// 実際のfetch実行ロジック
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body ? JSON.stringify(config.body) : undefined,
});
return response.json();
}
}
// 使用例
const response = await new HttpRequestBuilder()
.url('https://api.example.com/users')
.method('POST')
.authorization('my-token')
.json({ name: 'John', email: 'john@example.com' })
.timeout(5000)
.retries(3)
.execute<{ id: string }>();
Singleton パターン(モダンな代替案)
従来のSingletonは避け、DIコンテナやモジュールスコープを活用します。
// ❌ 従来のSingleton(避けるべき)
class LegacySingleton {
private static instance: LegacySingleton;
private constructor() {}
static getInstance(): LegacySingleton {
if (!LegacySingleton.instance) {
LegacySingleton.instance = new LegacySingleton();
}
return LegacySingleton.instance;
}
}
// ✅ モジュールスコープでのシングルトン
// database.ts
class DatabaseConnection {
constructor(private connectionString: string) {}
async query<T>(sql: string): Promise<T[]> {
// クエリ実行
return [];
}
}
// モジュールレベルでエクスポート
export const db = new DatabaseConnection(process.env.DATABASE_URL!);
// ✅ DIコンテナを使用(推奨)
// container.ts
import { Container } from 'inversify';
const container = new Container();
container.bind<DatabaseConnection>('Database')
.to(DatabaseConnection)
.inSingletonScope();
export { container };
構造パターン
Adapter パターン
互換性のないインターフェース間を橋渡しします。
// レガシーAPIを新しいインターフェースに適合させる
// 既存のレガシーシステム
interface LegacyPaymentSystem {
processPayment(
amount: number,
cardNumber: string,
expiry: string,
cvv: string
): boolean;
}
class LegacyStripePayment implements LegacyPaymentSystem {
processPayment(
amount: number,
cardNumber: string,
expiry: string,
cvv: string
): boolean {
console.log('Processing via legacy Stripe...');
return true;
}
}
// 新しいインターフェース
interface PaymentGateway {
charge(payment: PaymentDetails): Promise<PaymentResult>;
}
interface PaymentDetails {
amount: number;
currency: string;
card: {
number: string;
expiryMonth: number;
expiryYear: number;
cvc: string;
};
}
interface PaymentResult {
success: boolean;
transactionId: string;
error?: string;
}
// アダプター
class LegacyPaymentAdapter implements PaymentGateway {
constructor(private legacySystem: LegacyPaymentSystem) {}
async charge(payment: PaymentDetails): Promise<PaymentResult> {
const expiry = `${payment.card.expiryMonth}/${payment.card.expiryYear}`;
const success = this.legacySystem.processPayment(
payment.amount,
payment.card.number,
expiry,
payment.card.cvc
);
return {
success,
transactionId: success ? crypto.randomUUID() : '',
error: success ? undefined : 'Payment failed',
};
}
}
// 使用例
const legacyStripe = new LegacyStripePayment();
const paymentGateway: PaymentGateway = new LegacyPaymentAdapter(legacyStripe);
const result = await paymentGateway.charge({
amount: 1000,
currency: 'JPY',
card: {
number: '4242424242424242',
expiryMonth: 12,
expiryYear: 2025,
cvc: '123',
},
});
Decorator パターン
既存オブジェクトに動的に機能を追加します。
// TypeScriptデコレータを使用した実装
// メソッドデコレータ - ログ出力
function Log(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
console.log(`[${propertyKey}] Called with:`, args);
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
console.log(`[${propertyKey}] Returned:`, result, `(${duration}ms)`);
return result;
} catch (error) {
console.error(`[${propertyKey}] Error:`, error);
throw error;
}
};
return descriptor;
}
// キャッシュデコレータ
function Cache(ttlMs: number = 60000) {
const cache = new Map<string, { value: unknown; expiry: number }>();
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache Hit] ${propertyKey}`);
return cached.value;
}
const result = await originalMethod.apply(this, args);
cache.set(key, { value: result, expiry: Date.now() + ttlMs });
return result;
};
return descriptor;
};
}
// リトライデコレータ
function Retry(maxAttempts: number = 3, delayMs: number = 1000) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
console.warn(`[Retry] Attempt ${attempt}/${maxAttempts} failed`);
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw lastError!;
};
return descriptor;
};
}
// デコレータの適用
class UserService {
@Log
@Cache(30000) // 30秒キャッシュ
@Retry(3, 1000)
async getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
Proxy パターン
オブジェクトへのアクセスを制御します。
// ES6 Proxyを使用した実装
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// バリデーションプロキシ
function createValidatedUser(user: User): User {
return new Proxy(user, {
set(target, property, value) {
if (property === 'email') {
if (typeof value !== 'string' || !value.includes('@')) {
throw new Error('Invalid email format');
}
}
if (property === 'role') {
if (!['admin', 'user'].includes(value)) {
throw new Error('Invalid role');
}
}
return Reflect.set(target, property, value);
},
});
}
// 遅延ロードプロキシ
function createLazyLoader<T extends object>(
loader: () => Promise<T>
): T {
let instance: T | null = null;
let loading: Promise<T> | null = null;
return new Proxy({} as T, {
get(target, property) {
if (!instance) {
if (!loading) {
loading = loader().then(loaded => {
instance = loaded;
return loaded;
});
}
// プロミスを返すか、ブロックするか選択
return loading.then(inst => (inst as any)[property]);
}
return (instance as any)[property];
},
});
}
// アクセス制御プロキシ
function createSecureObject<T extends object>(
obj: T,
currentUser: User
): T {
return new Proxy(obj, {
get(target, property) {
const value = Reflect.get(target, property);
// 管理者限定プロパティ
const adminOnlyProps = ['password', 'secretKey', 'apiToken'];
if (adminOnlyProps.includes(String(property))) {
if (currentUser.role !== 'admin') {
throw new Error('Access denied: Admin only');
}
}
return value;
},
set(target, property, value) {
if (currentUser.role !== 'admin') {
throw new Error('Access denied: Read only');
}
return Reflect.set(target, property, value);
},
});
}
振る舞いパターン
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(): string {
return 'Regular';
}
}
class BulkPricing implements PricingStrategy {
constructor(private discountThreshold: number, private discountRate: number) {}
calculatePrice(basePrice: number, quantity: number): number {
if (quantity >= this.discountThreshold) {
return basePrice * quantity * (1 - this.discountRate);
}
return basePrice * quantity;
}
getName(): string {
return `Bulk (${this.discountRate * 100}% off for ${this.discountThreshold}+)`;
}
}
class SubscriberPricing implements PricingStrategy {
constructor(private memberDiscountRate: number) {}
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity * (1 - this.memberDiscountRate);
}
getName(): string {
return `Subscriber (${this.memberDiscountRate * 100}% off)`;
}
}
class SeasonalPricing implements PricingStrategy {
constructor(
private seasonalMultiplier: number,
private seasonName: string
) {}
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity * this.seasonalMultiplier;
}
getName(): string {
return `Seasonal - ${this.seasonName}`;
}
}
// コンテキスト
class ShoppingCart {
private items: Array<{ name: string; price: number; quantity: number }> = [];
private pricingStrategy: PricingStrategy = new RegularPricing();
addItem(name: string, price: number, quantity: number): void {
this.items.push({ name, price, quantity });
}
setPricingStrategy(strategy: PricingStrategy): void {
this.pricingStrategy = strategy;
}
calculateTotal(): number {
return this.items.reduce((total, item) => {
return total + this.pricingStrategy.calculatePrice(item.price, item.quantity);
}, 0);
}
getReceipt(): string {
const lines = this.items.map(item => {
const subtotal = this.pricingStrategy.calculatePrice(item.price, item.quantity);
return `${item.name} x${item.quantity}: ¥${subtotal}`;
});
return [
`Pricing: ${this.pricingStrategy.getName()}`,
'---',
...lines,
'---',
`Total: ¥${this.calculateTotal()}`,
].join('\n');
}
}
// 使用例
const cart = new ShoppingCart();
cart.addItem('Widget', 1000, 5);
cart.addItem('Gadget', 2000, 3);
console.log(cart.getReceipt());
// Pricing: Regular
// Total: ¥11000
cart.setPricingStrategy(new BulkPricing(3, 0.15));
console.log(cart.getReceipt());
// Pricing: Bulk (15% off for 3+)
// Total: ¥9350
Observer パターン
オブジェクト間の一対多の依存関係を定義します。
// TypeScriptでの型安全なObserver実装
type EventMap = {
userCreated: { id: string; email: string };
userUpdated: { id: string; changes: Partial<User> };
userDeleted: { id: string };
orderPlaced: { orderId: string; userId: string; total: number };
};
type EventKey = keyof EventMap;
type EventHandler<K extends EventKey> = (event: EventMap[K]) => void | Promise<void>;
class TypedEventEmitter {
private handlers = new Map<EventKey, Set<EventHandler<any>>>();
on<K extends EventKey>(event: K, handler: EventHandler<K>): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
// 購読解除関数を返す
return () => this.off(event, handler);
}
off<K extends EventKey>(event: K, handler: EventHandler<K>): void {
this.handlers.get(event)?.delete(handler);
}
async emit<K extends EventKey>(event: K, data: EventMap[K]): Promise<void> {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers) return;
const promises = Array.from(eventHandlers).map(handler =>
Promise.resolve(handler(data))
);
await Promise.all(promises);
}
once<K extends EventKey>(event: K, handler: EventHandler<K>): () => void {
const wrappedHandler: EventHandler<K> = async (data) => {
this.off(event, wrappedHandler);
await handler(data);
};
return this.on(event, wrappedHandler);
}
}
// 使用例
const eventBus = new TypedEventEmitter();
// メール送信サービス
eventBus.on('userCreated', async ({ email }) => {
console.log(`Sending welcome email to ${email}`);
});
// 分析サービス
eventBus.on('userCreated', ({ id }) => {
console.log(`Tracking user creation: ${id}`);
});
// 注文処理
eventBus.on('orderPlaced', async ({ orderId, userId, total }) => {
console.log(`Processing order ${orderId} for user ${userId}: ¥${total}`);
});
// イベント発火
await eventBus.emit('userCreated', {
id: 'user-123',
email: 'user@example.com',
});
Command パターン
操作をオブジェクトとしてカプセル化します。
// Undo/Redo機能を持つCommand実装
interface Command {
execute(): Promise<void>;
undo(): Promise<void>;
getDescription(): string;
}
// テキストエディタのコマンド例
class TextEditor {
private content: string = '';
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
}
insertAt(position: number, text: string): void {
this.content =
this.content.slice(0, position) + text + this.content.slice(position);
}
deleteRange(start: number, end: number): string {
const deleted = this.content.slice(start, end);
this.content = this.content.slice(0, start) + this.content.slice(end);
return deleted;
}
}
class InsertTextCommand implements Command {
constructor(
private editor: TextEditor,
private position: number,
private text: string
) {}
async execute(): Promise<void> {
this.editor.insertAt(this.position, this.text);
}
async undo(): Promise<void> {
this.editor.deleteRange(this.position, this.position + this.text.length);
}
getDescription(): string {
return `Insert "${this.text}" at position ${this.position}`;
}
}
class DeleteTextCommand implements Command {
private deletedText: string = '';
constructor(
private editor: TextEditor,
private start: number,
private end: number
) {}
async execute(): Promise<void> {
this.deletedText = this.editor.deleteRange(this.start, this.end);
}
async undo(): Promise<void> {
this.editor.insertAt(this.start, this.deletedText);
}
getDescription(): string {
return `Delete from ${this.start} to ${this.end}`;
}
}
// コマンドマネージャー(Invoker)
class CommandManager {
private history: Command[] = [];
private redoStack: Command[] = [];
async execute(command: Command): Promise<void> {
await command.execute();
this.history.push(command);
this.redoStack = []; // 新しいコマンド実行でredoスタックをクリア
}
async undo(): Promise<boolean> {
const command = this.history.pop();
if (!command) return false;
await command.undo();
this.redoStack.push(command);
return true;
}
async redo(): Promise<boolean> {
const command = this.redoStack.pop();
if (!command) return false;
await command.execute();
this.history.push(command);
return true;
}
getHistory(): string[] {
return this.history.map(cmd => cmd.getDescription());
}
}
// 使用例
const editor = new TextEditor();
const manager = new CommandManager();
await manager.execute(new InsertTextCommand(editor, 0, 'Hello '));
await manager.execute(new InsertTextCommand(editor, 6, 'World!'));
console.log(editor.getContent()); // "Hello World!"
await manager.undo();
console.log(editor.getContent()); // "Hello "
await manager.redo();
console.log(editor.getContent()); // "Hello World!"
現代的なアーキテクチャパターン
Repository パターン
データアクセスロジックを抽象化します。
// Repositoryパターン with TypeScript
interface Entity {
id: string;
}
interface Repository<T extends Entity> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
findBy(criteria: Partial<T>): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: string): Promise<boolean>;
}
interface User extends Entity {
id: string;
email: string;
name: string;
createdAt: Date;
}
// インメモリ実装(テスト用)
class InMemoryUserRepository implements Repository<User> {
private users: Map<string, User> = new Map();
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async findAll(): Promise<User[]> {
return Array.from(this.users.values());
}
async findBy(criteria: Partial<User>): Promise<User[]> {
return Array.from(this.users.values()).filter(user =>
Object.entries(criteria).every(
([key, value]) => user[key as keyof User] === value
)
);
}
async save(entity: User): Promise<User> {
this.users.set(entity.id, entity);
return entity;
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id);
}
}
// Prisma実装(本番用)
class PrismaUserRepository implements Repository<User> {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
}
async findAll(): Promise<User[]> {
return this.prisma.user.findMany();
}
async findBy(criteria: Partial<User>): Promise<User[]> {
return this.prisma.user.findMany({ where: criteria });
}
async save(entity: User): Promise<User> {
return this.prisma.user.upsert({
where: { id: entity.id },
update: entity,
create: entity,
});
}
async delete(id: string): Promise<boolean> {
try {
await this.prisma.user.delete({ where: { id } });
return true;
} catch {
return false;
}
}
}
Unit of Work パターン
トランザクション内の変更を追跡・管理します。
// Unit of Work実装
interface UnitOfWork {
begin(): Promise<void>;
commit(): Promise<void>;
rollback(): Promise<void>;
userRepository: Repository<User>;
orderRepository: Repository<Order>;
}
class PrismaUnitOfWork implements UnitOfWork {
private transaction: Prisma.TransactionClient | null = null;
private _userRepository: Repository<User> | null = null;
private _orderRepository: Repository<Order> | null = null;
constructor(private prisma: PrismaClient) {}
async begin(): Promise<void> {
// Prismaの対話型トランザクション
return new Promise((resolve) => {
this.prisma.$transaction(async (tx) => {
this.transaction = tx;
resolve();
// トランザクションは commit/rollback まで保持
});
});
}
get userRepository(): Repository<User> {
if (!this._userRepository) {
this._userRepository = new PrismaUserRepository(
this.transaction || this.prisma
);
}
return this._userRepository;
}
get orderRepository(): Repository<Order> {
if (!this._orderRepository) {
this._orderRepository = new PrismaOrderRepository(
this.transaction || this.prisma
);
}
return this._orderRepository;
}
async commit(): Promise<void> {
// Prismaトランザクションは自動コミット
this.transaction = null;
}
async rollback(): Promise<void> {
throw new Error('Rollback requested');
}
}
// 使用例
async function createOrderWithUser(uow: UnitOfWork) {
await uow.begin();
try {
const user = await uow.userRepository.save({
id: crypto.randomUUID(),
email: 'new@example.com',
name: 'New User',
createdAt: new Date(),
});
const order = await uow.orderRepository.save({
id: crypto.randomUUID(),
userId: user.id,
total: 5000,
status: 'pending',
});
await uow.commit();
return { user, order };
} catch (error) {
await uow.rollback();
throw error;
}
}
SOLID原則との関係
flowchart LR
subgraph SOLID["SOLID原則"]
S["S - 単一責任原則 (SRP)"]
O["O - オープン・クローズド原則 (OCP)"]
L["L - リスコフの置換原則 (LSP)"]
I["I - インターフェース分離原則 (ISP)"]
D["D - 依存性逆転原則 (DIP)"]
end
subgraph Patterns["関連パターン"]
P1["Factory, Strategy, Command"]
P2["Strategy, Decorator, Template Method"]
P3["Factory Method, Abstract Factory"]
P4["Adapter, Facade"]
P5["Repository, Dependency Injection"]
end
S --> P1
O --> P2
L --> P3
I --> P4
D --> P5
まとめ
デザインパターンは、問題解決のための共通言語であり、チーム開発における認識合わせにも役立ちます。
選定の指針
| 目的 | 推奨パターン |
|---|---|
| オブジェクト生成の柔軟性 | Factory, Builder |
| 既存コードの拡張 | Decorator, Adapter |
| アルゴリズムの切り替え | Strategy |
| イベント駆動 | Observer |
| 操作の取り消し | Command |
| データアクセス抽象化 | Repository |
モダン開発でのベストプラクティス
- パターンの過剰適用を避ける - シンプルな問題にはシンプルな解決策
- 言語機能を活用 - TypeScript/Pythonの型システム、デコレータ
- テスタビリティを重視 - DIを活用し依存を注入可能に
- 関数型アプローチとの併用 - 状態を持たない処理は関数で
デザインパターンは目的ではなく手段です。問題を理解し、適切なパターンを選択することが重要です。