ストーリー
Command パターン
目的
操作(リクエスト)をオブジェクトとして表現する。これにより、操作のキュー化、ログ記録、Undo/Redo が実現できる。
Command の実装:テキストエディタ
// Command インターフェース
interface Command {
execute(): void;
undo(): void;
describe(): string;
}
// テキストエディタのモデル
class TextDocument {
private content: string = '';
getContent(): string { return this.content; }
insertAt(position: number, text: string): void {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
}
deleteRange(position: number, length: number): string {
const deleted = this.content.slice(position, position + length);
this.content = this.content.slice(0, position) + this.content.slice(position + length);
return deleted;
}
}
// 具体的な Command:文字挿入
class InsertCommand implements Command {
constructor(
private document: TextDocument,
private position: number,
private text: string
) {}
execute(): void {
this.document.insertAt(this.position, this.text);
}
undo(): void {
this.document.deleteRange(this.position, this.text.length);
}
describe(): string {
return `Insert "${this.text}" at position ${this.position}`;
}
}
// 具体的な Command:文字削除
class DeleteCommand implements Command {
private deletedText: string = '';
constructor(
private document: TextDocument,
private position: number,
private length: number
) {}
execute(): void {
this.deletedText = this.document.deleteRange(this.position, this.length);
}
undo(): void {
this.document.insertAt(this.position, this.deletedText);
}
describe(): string {
return `Delete ${this.length} chars at position ${this.position}`;
}
}
// Invoker:コマンドの実行管理
class CommandManager {
private history: Command[] = [];
private undoneCommands: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
this.undoneCommands = []; // 新しい操作をしたらRedo履歴をクリア
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.undoneCommands.push(command);
}
}
redo(): void {
const command = this.undoneCommands.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
getHistory(): string[] {
return this.history.map(cmd => cmd.describe());
}
}
// 使い方
const doc = new TextDocument();
const manager = new CommandManager();
manager.execute(new InsertCommand(doc, 0, 'Hello'));
console.log(doc.getContent()); // 'Hello'
manager.execute(new InsertCommand(doc, 5, ' World'));
console.log(doc.getContent()); // 'Hello World'
manager.undo();
console.log(doc.getContent()); // 'Hello'
manager.redo();
console.log(doc.getContent()); // 'Hello World'
Command の応用:マクロ
// 複数のコマンドをまとめて実行
class MacroCommand implements Command {
private commands: Command[] = [];
add(command: Command): void {
this.commands.push(command);
}
execute(): void {
for (const cmd of this.commands) {
cmd.execute();
}
}
undo(): void {
// 逆順で Undo
for (let i = this.commands.length - 1; i >= 0; i--) {
this.commands[i].undo();
}
}
describe(): string {
return `Macro: [${this.commands.map(c => c.describe()).join(', ')}]`;
}
}
Template Method パターン
目的
処理のアルゴリズムの骨格を基底クラスで定義し、具体的なステップの実装をサブクラスに委ねる。処理の順序は固定しつつ、個々のステップをカスタマイズ可能にする。
Template Method の実装:データ処理パイプライン
// 抽象クラス:処理の骨格を定義
abstract class DataProcessor<TInput, TOutput> {
// Template Method -- 処理の流れを定義(final にしたい)
process(input: TInput): TOutput {
console.log(`Processing started: ${this.getName()}`);
const validated = this.validate(input);
const transformed = this.transform(validated);
const result = this.format(transformed);
this.onComplete(result);
console.log(`Processing completed: ${this.getName()}`);
return result;
}
// サブクラスが実装すべきメソッド
protected abstract getName(): string;
protected abstract validate(input: TInput): TInput;
protected abstract transform(input: TInput): unknown;
protected abstract format(data: unknown): TOutput;
// オプショナルなフック(必要に応じてオーバーライド)
protected onComplete(result: TOutput): void {
// デフォルトでは何もしない
}
}
// CSV処理
class CsvProcessor extends DataProcessor<string, string[][]> {
protected getName(): string { return 'CSV Processor'; }
protected validate(input: string): string {
if (!input.trim()) throw new Error('Empty CSV input');
return input.trim();
}
protected transform(input: string): string[][] {
return input.split('\n').map(line => line.split(','));
}
protected format(data: unknown): string[][] {
const rows = data as string[][];
return rows.map(row => row.map(cell => cell.trim()));
}
}
// JSON処理
class JsonProcessor extends DataProcessor<string, Record<string, unknown>> {
protected getName(): string { return 'JSON Processor'; }
protected validate(input: string): string {
try {
JSON.parse(input);
} catch {
throw new Error('Invalid JSON');
}
return input;
}
protected transform(input: string): Record<string, unknown> {
return JSON.parse(input);
}
protected format(data: unknown): Record<string, unknown> {
return data as Record<string, unknown>;
}
protected onComplete(result: Record<string, unknown>): void {
console.log(`Parsed ${Object.keys(result).length} keys`);
}
}
// 使い方
const csvProcessor = new CsvProcessor();
const result1 = csvProcessor.process('name,age\nAlice,30\nBob,25');
const jsonProcessor = new JsonProcessor();
const result2 = jsonProcessor.process('{"name": "Takahashi", "role": "architect"}');
Template Method のもう1つの例:レポート生成
abstract class ReportGenerator {
// Template Method
generate(): string {
const header = this.buildHeader();
const body = this.buildBody();
const footer = this.buildFooter();
return `${header}\n${body}\n${footer}`;
}
protected abstract buildHeader(): string;
protected abstract buildBody(): string;
// フック:デフォルト実装あり(オーバーライド可能)
protected buildFooter(): string {
return `Generated at: ${new Date().toISOString()}`;
}
}
class SalesReport extends ReportGenerator {
protected buildHeader(): string {
return '=== Monthly Sales Report ===';
}
protected buildBody(): string {
return 'Total Sales: 1,234,567 JPY\nGrowth: +12%';
}
}
class InventoryReport extends ReportGenerator {
protected buildHeader(): string {
return '=== Inventory Status ===';
}
protected buildBody(): string {
return 'Total Items: 5,432\nLow Stock: 23 items';
}
protected buildFooter(): string {
return `${super.buildFooter()}\nNext check: Tomorrow`;
}
}
Command vs Template Method
| 観点 | Command | Template Method |
|---|---|---|
| 目的 | 操作をオブジェクト化 | 処理の骨格を定義 |
| 柔軟性 | 実行時に操作を組み立て | コンパイル時にステップを定義 |
| 拡張方法 | 新しい Command クラス追加 | サブクラスでステップを実装 |
| 典型的な用途 | Undo/Redo、キュー、マクロ | データ処理、レポート生成 |
高橋アーキテクトのアドバイス:
「Command は”操作を記録する”ために使い、Template Method は”処理の流れを統一する”ために使う。どちらもフレームワーク設計で頻出するパターンだ」
まとめ
| ポイント | 内容 |
|---|---|
| Command | 操作をオブジェクトとして表現し、Undo/Redo を実現 |
| Template Method | 処理の骨格を基底クラスで定義し、詳細をサブクラスへ |
| Command の効果 | 操作の記録・再生・取り消しが可能 |
| Template Method の効果 | 処理の流れを統一しつつカスタマイズ可能 |
チェックリスト
- Command パターンで Undo/Redo を実装できる
- Template Method パターンで処理の骨格を定義できる
- 2つのパターンの使い分けを判断できる
次のステップへ
次は演習です。Step 4 で学んだパターン(Adapter、Decorator、Facade、Composite、Strategy、Observer、Command、Template Method)を組み合わせて設計してみましょう。
推定読了時間: 40分