ストーリー
Adapter パターン
目的
互換性のないインターフェースを持つクラスを、既存のインターフェースに適合させる。海外旅行で使う電源アダプターのようなものです。
ユースケース:外部ログライブラリの統合
// 自分たちのシステムで使うロガーインターフェース
interface Logger {
info(message: string): void;
warn(message: string): void;
error(message: string, error?: Error): void;
}
// 外部ライブラリのロガー(変更できない)
class ThirdPartyLogger {
writeLog(level: number, msg: string, metadata?: Record<string, unknown>): void {
console.log(`[LEVEL:${level}] ${msg}`, metadata ?? '');
}
}
// Adapter:外部ライブラリを自分たちのインターフェースに適合
class ThirdPartyLoggerAdapter implements Logger {
private static readonly LEVELS = { INFO: 1, WARN: 2, ERROR: 3 };
constructor(private adaptee: ThirdPartyLogger) {}
info(message: string): void {
this.adaptee.writeLog(ThirdPartyLoggerAdapter.LEVELS.INFO, message);
}
warn(message: string): void {
this.adaptee.writeLog(ThirdPartyLoggerAdapter.LEVELS.WARN, message);
}
error(message: string, error?: Error): void {
this.adaptee.writeLog(
ThirdPartyLoggerAdapter.LEVELS.ERROR,
message,
error ? { stack: error.stack } : undefined
);
}
}
// 使い方
const externalLogger = new ThirdPartyLogger();
const logger: Logger = new ThirdPartyLoggerAdapter(externalLogger);
logger.info('Application started');
logger.error('Something failed', new Error('Connection timeout'));
Adapter のもう1つの例:データソースの統合
// 社内API
interface UserDataSource {
fetchUsers(): Promise<User[]>;
}
// 外部CRM API(インターフェースが異なる)
class ExternalCrmApi {
async getContacts(apiKey: string, page: number): Promise<CrmContact[]> {
// 外部APIを呼び出す
return [];
}
}
// Adapter
class CrmApiAdapter implements UserDataSource {
constructor(
private crmApi: ExternalCrmApi,
private apiKey: string
) {}
async fetchUsers(): Promise<User[]> {
const contacts = await this.crmApi.getContacts(this.apiKey, 1);
return contacts.map(contact => ({
id: contact.contactId,
name: `${contact.firstName} ${contact.lastName}`,
email: contact.emailAddress,
}));
}
}
Decorator パターン
目的
オブジェクトに動的に新しい機能を追加する。継承を使わずに、ラッピングによって責任を追加する。
ユースケース:HTTPクライアントの機能拡張
// 基本インターフェース
interface HttpClient {
request(url: string, options?: RequestOptions): Promise<Response>;
}
// 基本実装
class BasicHttpClient implements HttpClient {
async request(url: string, options?: RequestOptions): Promise<Response> {
return fetch(url, options);
}
}
// Decorator 1: ログ追加
class LoggingHttpClient implements HttpClient {
constructor(private wrapped: HttpClient) {}
async request(url: string, options?: RequestOptions): Promise<Response> {
console.log(`[HTTP] ${options?.method ?? 'GET'} ${url}`);
const start = Date.now();
const response = await this.wrapped.request(url, options);
console.log(`[HTTP] ${response.status} (${Date.now() - start}ms)`);
return response;
}
}
// Decorator 2: リトライ追加
class RetryHttpClient implements HttpClient {
constructor(
private wrapped: HttpClient,
private maxRetries: number = 3
) {}
async request(url: string, options?: RequestOptions): Promise<Response> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await this.wrapped.request(url, options);
} catch (error) {
lastError = error as Error;
console.log(`Retry ${attempt}/${this.maxRetries}...`);
await this.delay(attempt * 1000);
}
}
throw lastError;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Decorator 3: キャッシュ追加
class CachingHttpClient implements HttpClient {
private cache: Map<string, { response: Response; timestamp: number }> = new Map();
constructor(
private wrapped: HttpClient,
private ttlMs: number = 60000
) {}
async request(url: string, options?: RequestOptions): Promise<Response> {
const method = options?.method ?? 'GET';
if (method !== 'GET') {
return this.wrapped.request(url, options);
}
const cached = this.cache.get(url);
if (cached && Date.now() - cached.timestamp < this.ttlMs) {
return cached.response.clone();
}
const response = await this.wrapped.request(url, options);
this.cache.set(url, { response: response.clone(), timestamp: Date.now() });
return response;
}
}
// Decoratorの組み合わせ:自由に積み重ねられる
const client: HttpClient = new CachingHttpClient(
new RetryHttpClient(
new LoggingHttpClient(
new BasicHttpClient()
),
3
),
60000
);
// リクエストはキャッシュ → リトライ → ログ → 基本 の順で処理される
await client.request('https://api.example.com/users');
Adapter vs Decorator
| 観点 | Adapter | Decorator |
|---|---|---|
| 目的 | インターフェースの変換 | 機能の追加 |
| 元のインターフェース | 変換前後で異なる | 同じインターフェースを維持 |
| 使用場面 | 外部ライブラリの統合 | 機能の動的な追加 |
| 関連原則 | DIP, ISP | OCP, SRP |
高橋アーキテクトのアドバイス:
「Adapter は”違うものを同じに見せる”、Decorator は”同じものに新しい力を加える”。目的を混同しないことが大切だ」
まとめ
| ポイント | 内容 |
|---|---|
| Adapter | 互換性のないインターフェースを接続する変換器 |
| Decorator | 既存の機能にラッピングで責任を追加する |
| Adapter の効果 | 外部ライブラリを安全に統合できる |
| Decorator の効果 | 継承なしで柔軟に機能を組み合わせられる |
チェックリスト
- Adapter パターンを使って外部ライブラリを統合できる
- Decorator パターンで機能を動的に追加できる
- 2つのパターンの違いを説明できる
次のステップへ
次は Facade と Composite パターンを学びます。複雑なサブシステムをシンプルにし、木構造を統一的に扱う方法です。
推定読了時間: 40分