ストーリー
ミッション概要
| ミッション | テーマ | 対象パターン | 目安時間 |
|---|---|---|---|
| Mission 1 | 外部サービス統合 | Adapter | 15分 |
| Mission 2 | 機能拡張可能なロガー | Decorator | 15分 |
| Mission 3 | イベント駆動通知 | Observer | 15分 |
| Mission 4 | 通知アルゴリズム切り替え | Strategy | 15分 |
| Mission 5 | 操作の記録と取り消し | Command | 15分 |
| Mission 6 | 統合:通知システム | 複数パターン | 15分 |
Mission 1: 外部サービス統合(15分)
要件
以下の2つの外部通知サービスを、統一インターフェースで使えるようにAdapterを実装してください。
// 外部サービス1:Twilio SMS API(変更不可)
class TwilioSmsApi {
sendSms(phoneNumber: string, body: string, from: string): { sid: string } {
console.log(`Twilio: ${from} -> ${phoneNumber}: ${body}`);
return { sid: `SM${Date.now()}` };
}
}
// 外部サービス2:SendGrid Email API(変更不可)
class SendGridApi {
send(msg: { to: string; from: string; subject: string; html: string }): Promise<{ statusCode: number }> {
console.log(`SendGrid: ${msg.from} -> ${msg.to}: ${msg.subject}`);
return Promise.resolve({ statusCode: 202 });
}
}
// 統一インターフェース
interface MessageSender {
send(to: string, content: string): Promise<boolean>;
}
解答
class TwilioAdapter implements MessageSender {
constructor(
private twilioApi: TwilioSmsApi,
private fromNumber: string
) {}
async send(to: string, content: string): Promise<boolean> {
try {
const result = this.twilioApi.sendSms(to, content, this.fromNumber);
return result.sid !== undefined;
} catch {
return false;
}
}
}
class SendGridAdapter implements MessageSender {
constructor(
private sendGridApi: SendGridApi,
private fromEmail: string
) {}
async send(to: string, content: string): Promise<boolean> {
try {
const result = await this.sendGridApi.send({
to,
from: this.fromEmail,
subject: 'Notification',
html: `<p>${content}</p>`,
});
return result.statusCode === 202;
} catch {
return false;
}
}
}
// 使い方
const smsSender: MessageSender = new TwilioAdapter(new TwilioSmsApi(), '+81-90-1234-5678');
const emailSender: MessageSender = new SendGridAdapter(new SendGridApi(), 'noreply@example.com');
await smsSender.send('+81-90-9999-0000', 'Hello via SMS');
await emailSender.send('user@example.com', 'Hello via Email');
Mission 2: 機能拡張可能なロガー(15分)
要件
基本のロガーに、タイムスタンプ追加、ログレベルフィルタリング、JSON出力の機能をDecoratorで追加してください。
interface AppLogger {
log(level: string, message: string): void;
}
class ConsoleAppLogger implements AppLogger {
log(level: string, message: string): void {
console.log(`[${level}] ${message}`);
}
}
期待される動作
const logger = new JsonFormatLogger(
new TimestampLogger(
new LevelFilterLogger(
new ConsoleAppLogger(),
'warn' // warn以上のみ出力
)
)
);
解答
const LOG_LEVELS: Record<string, number> = {
debug: 0, info: 1, warn: 2, error: 3,
};
class TimestampLogger implements AppLogger {
constructor(private wrapped: AppLogger) {}
log(level: string, message: string): void {
const timestamp = new Date().toISOString();
this.wrapped.log(level, `${timestamp} ${message}`);
}
}
class LevelFilterLogger implements AppLogger {
private minLevel: number;
constructor(private wrapped: AppLogger, minLevelName: string) {
this.minLevel = LOG_LEVELS[minLevelName] ?? 0;
}
log(level: string, message: string): void {
const currentLevel = LOG_LEVELS[level] ?? 0;
if (currentLevel >= this.minLevel) {
this.wrapped.log(level, message);
}
}
}
class JsonFormatLogger implements AppLogger {
constructor(private wrapped: AppLogger) {}
log(level: string, message: string): void {
const json = JSON.stringify({ level, message });
this.wrapped.log(level, json);
}
}
// 組み合わせて使う
const logger = new JsonFormatLogger(
new TimestampLogger(
new LevelFilterLogger(
new ConsoleAppLogger(),
'warn'
)
)
);
logger.log('info', 'This will be filtered out');
logger.log('warn', 'This will be shown');
logger.log('error', 'This too');
Mission 3: イベント駆動通知(15分)
要件
ユーザーアクション(登録、ログイン、購入)が発生したときに、複数のハンドラーに通知するObserverパターンを実装してください。
期待される動作
const eventBus = new EventBus();
eventBus.on('user:registered', (data) => sendWelcomeEmail(data));
eventBus.on('user:registered', (data) => trackAnalytics(data));
eventBus.on('user:purchase', (data) => updateInventory(data));
eventBus.emit('user:registered', { userId: '123', email: 'test@example.com' });
解答
type EventData = Record<string, unknown>;
type EventHandler = (data: EventData) => void;
class EventBus {
private handlers: Map<string, EventHandler[]> = new Map();
on(eventName: string, handler: EventHandler): void {
const list = this.handlers.get(eventName) ?? [];
list.push(handler);
this.handlers.set(eventName, list);
}
off(eventName: string, handler: EventHandler): void {
const list = this.handlers.get(eventName) ?? [];
this.handlers.set(eventName, list.filter(h => h !== handler));
}
once(eventName: string, handler: EventHandler): void {
const wrappedHandler: EventHandler = (data) => {
handler(data);
this.off(eventName, wrappedHandler);
};
this.on(eventName, wrappedHandler);
}
emit(eventName: string, data: EventData): void {
const list = this.handlers.get(eventName) ?? [];
for (const handler of list) {
try {
handler(data);
} catch (error) {
console.error(`Error in handler for ${eventName}:`, error);
}
}
}
listenerCount(eventName: string): number {
return (this.handlers.get(eventName) ?? []).length;
}
}
// 使い方
const eventBus = new EventBus();
eventBus.on('user:registered', (data) => {
console.log(`Welcome email sent to ${data.email}`);
});
eventBus.on('user:registered', (data) => {
console.log(`Analytics: New user ${data.userId}`);
});
eventBus.on('user:purchase', (data) => {
console.log(`Inventory updated for order ${data.orderId}`);
});
eventBus.emit('user:registered', { userId: '123', email: 'test@example.com' });
eventBus.emit('user:purchase', { userId: '123', orderId: 'ORD-456' });
Mission 4: 通知アルゴリズム切り替え(15分)
要件
通知の優先度判定を Strategy パターンで切り替え可能にしてください。ビジネス時間内は全通知を送信し、夜間は重要度の高い通知のみ送信するようにしてください。
interface Notification {
title: string;
message: string;
priority: 'low' | 'medium' | 'high' | 'critical';
}
解答
interface NotificationFilterStrategy {
shouldSend(notification: Notification): boolean;
}
class AllNotificationsStrategy implements NotificationFilterStrategy {
shouldSend(_notification: Notification): boolean {
return true;
}
}
class HighPriorityOnlyStrategy implements NotificationFilterStrategy {
shouldSend(notification: Notification): boolean {
return notification.priority === 'high' || notification.priority === 'critical';
}
}
class CriticalOnlyStrategy implements NotificationFilterStrategy {
shouldSend(notification: Notification): boolean {
return notification.priority === 'critical';
}
}
class NotificationDispatcher {
private strategy: NotificationFilterStrategy;
constructor(strategy: NotificationFilterStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: NotificationFilterStrategy): void {
this.strategy = strategy;
}
dispatch(notification: Notification, sender: MessageSender, to: string): void {
if (this.strategy.shouldSend(notification)) {
sender.send(to, `[${notification.priority}] ${notification.title}: ${notification.message}`);
} else {
console.log(`Filtered out: ${notification.title} (${notification.priority})`);
}
}
}
// 時間帯に応じて切り替え
const dispatcher = new NotificationDispatcher(new AllNotificationsStrategy());
const hour = new Date().getHours();
if (hour < 9 || hour >= 22) {
dispatcher.setStrategy(new HighPriorityOnlyStrategy());
}
Mission 5: 操作の記録と取り消し(15分)
要件
通知設定の変更操作をCommandパターンで実装し、Undo/Redoを可能にしてください。
interface NotificationPreferences {
email: boolean;
sms: boolean;
push: boolean;
frequency: 'realtime' | 'daily' | 'weekly';
}
解答
interface PreferenceCommand {
execute(): void;
undo(): void;
describe(): string;
}
class ToggleChannelCommand implements PreferenceCommand {
private previousValue: boolean;
constructor(
private prefs: NotificationPreferences,
private channel: 'email' | 'sms' | 'push'
) {
this.previousValue = prefs[channel];
}
execute(): void {
this.prefs[this.channel] = !this.prefs[this.channel];
}
undo(): void {
this.prefs[this.channel] = this.previousValue;
}
describe(): string {
return `Toggle ${this.channel}: ${this.previousValue} -> ${!this.previousValue}`;
}
}
class ChangeFrequencyCommand implements PreferenceCommand {
private previousFrequency: NotificationPreferences['frequency'];
constructor(
private prefs: NotificationPreferences,
private newFrequency: NotificationPreferences['frequency']
) {
this.previousFrequency = prefs.frequency;
}
execute(): void {
this.prefs.frequency = this.newFrequency;
}
undo(): void {
this.prefs.frequency = this.previousFrequency;
}
describe(): string {
return `Change frequency: ${this.previousFrequency} -> ${this.newFrequency}`;
}
}
class PreferenceManager {
private history: PreferenceCommand[] = [];
private undone: PreferenceCommand[] = [];
execute(cmd: PreferenceCommand): void {
cmd.execute();
this.history.push(cmd);
this.undone = [];
}
undo(): boolean {
const cmd = this.history.pop();
if (!cmd) return false;
cmd.undo();
this.undone.push(cmd);
return true;
}
redo(): boolean {
const cmd = this.undone.pop();
if (!cmd) return false;
cmd.execute();
this.history.push(cmd);
return true;
}
}
// 使い方
const prefs: NotificationPreferences = {
email: true, sms: false, push: true, frequency: 'realtime',
};
const manager = new PreferenceManager();
manager.execute(new ToggleChannelCommand(prefs, 'sms')); // sms: true
manager.execute(new ChangeFrequencyCommand(prefs, 'daily')); // frequency: daily
manager.undo(); // frequency: realtime に戻る
manager.undo(); // sms: false に戻る
Mission 6: 統合:通知システム(15分)
要件
Mission 1-5 のパターンを組み合わせて、通知システムの全体像を設計してください。以下の要件を満たすクラス構成を考えてください。
- 外部サービスは Adapter で統合
- ロガーは Decorator で機能拡張
- ユーザーアクションは Observer で通知
- 通知フィルタリングは Strategy で切り替え
- 設定変更は Command で記録
解答
// === 全体の組み立て ===
// 1. Adapter: 外部サービスの統合
const smsService: MessageSender = new TwilioAdapter(new TwilioSmsApi(), '+81-90-0000-0000');
const emailService: MessageSender = new SendGridAdapter(new SendGridApi(), 'noreply@example.com');
// 2. Decorator: ロガーの機能拡張
const logger: AppLogger = new TimestampLogger(
new LevelFilterLogger(new ConsoleAppLogger(), 'info')
);
// 3. Strategy: 通知フィルタリング
const dispatcher = new NotificationDispatcher(new AllNotificationsStrategy());
// 4. Observer: イベント駆動
const eventBus = new EventBus();
eventBus.on('user:registered', (data) => {
logger.log('info', `New user registered: ${data.userId}`);
dispatcher.dispatch(
{ title: 'Welcome', message: 'Thanks for joining!', priority: 'medium' },
emailService,
data.email as string
);
});
eventBus.on('order:placed', (data) => {
logger.log('info', `Order placed: ${data.orderId}`);
dispatcher.dispatch(
{ title: 'Order Confirmation', message: `Order #${data.orderId}`, priority: 'high' },
smsService,
data.phone as string
);
});
// 5. Command: 設定変更の記録
const prefs: NotificationPreferences = {
email: true, sms: true, push: true, frequency: 'realtime',
};
const prefManager = new PreferenceManager();
// 実行例
eventBus.emit('user:registered', { userId: 'U001', email: 'user@example.com' });
eventBus.emit('order:placed', { orderId: 'ORD-001', phone: '+81-90-1234-5678' });
prefManager.execute(new ChangeFrequencyCommand(prefs, 'daily'));
達成度チェック
| ミッション | テーマ | 完了 |
|---|---|---|
| Mission 1 | Adapter で外部サービス統合 | |
| Mission 2 | Decorator でロガー拡張 | |
| Mission 3 | Observer でイベント駆動 | |
| Mission 4 | Strategy で通知フィルタリング | |
| Mission 5 | Command で設定変更記録 | |
| Mission 6 | 全パターンの統合設計 |
まとめ
| ポイント | 内容 |
|---|---|
| パターンの組み合わせ | 実際のシステムでは複数パターンが共存する |
| Adapter + Strategy | 外部サービスの統合と切り替え |
| Observer + Command | イベント駆動と操作の記録 |
| Decorator | 既存機能への横断的な拡張 |
チェックリスト
- 各パターンを独立して実装できた
- 複数パターンの組み合わせを設計できた
- 実際のシステムでの適用場面をイメージできた
次のステップへ
次はチェックポイントクイズです。構造パターンと振る舞いパターンの理解度を確認しましょう。
推定読了時間: 90分