ストーリー
ミッション概要
| ミッション | テーマ | 対象パターン | 目安時間 |
|---|---|---|---|
| Mission 1 | ドキュメント生成 | Factory Method | 10分 |
| Mission 2 | エクスポーターファミリー | Abstract Factory | 15分 |
| Mission 3 | クエリビルダー | Builder | 15分 |
| Mission 4 | テンプレートライブラリ | Prototype | 10分 |
| Mission 5 | 総合:ドキュメント管理システム | 複数パターン | 10分 |
Mission 1: ドキュメント生成(10分)
要件
ドキュメントの種類(Report、Invoice、Letter)に応じて適切なドキュメントオブジェクトを生成する Factory Method を実装してください。
// このインターフェースを使ってください
interface Document {
readonly type: string;
getTitle(): string;
getContent(): string;
}
期待される動作
const report = DocumentFactory.create('report', 'Monthly Report');
console.log(report.type); // 'report'
console.log(report.getTitle()); // 'Monthly Report'
const invoice = DocumentFactory.create('invoice', 'INV-001');
console.log(invoice.type); // 'invoice'
解答
interface Document {
readonly type: string;
getTitle(): string;
getContent(): string;
}
class Report implements Document {
readonly type = 'report';
constructor(private title: string) {}
getTitle(): string { return this.title; }
getContent(): string { return `Report: ${this.title}\n[Report Content]`; }
}
class Invoice implements Document {
readonly type = 'invoice';
constructor(private invoiceNumber: string) {}
getTitle(): string { return `Invoice ${this.invoiceNumber}`; }
getContent(): string { return `Invoice #${this.invoiceNumber}\n[Line Items]`; }
}
class Letter implements Document {
readonly type = 'letter';
constructor(private subject: string) {}
getTitle(): string { return this.subject; }
getContent(): string { return `Dear Sir/Madam,\nRe: ${this.subject}`; }
}
type DocumentType = 'report' | 'invoice' | 'letter';
class DocumentFactory {
private static creators: Record<DocumentType, (title: string) => Document> = {
report: (title) => new Report(title),
invoice: (title) => new Invoice(title),
letter: (title) => new Letter(title),
};
static create(type: DocumentType, title: string): Document {
const creator = this.creators[type];
if (!creator) throw new Error(`Unknown document type: ${type}`);
return creator(title);
}
static registerType(type: string, creator: (title: string) => Document): void {
this.creators[type as DocumentType] = creator;
}
}
Mission 2: エクスポーターファミリー(15分)
要件
ドキュメントを異なるフォーマット(PDF、HTML)でエクスポートするための Abstract Factory を実装してください。各フォーマットにはヘッダー、ボディ、フッターのレンダラーが必要です。
interface HeaderRenderer {
render(title: string): string;
}
interface BodyRenderer {
render(content: string): string;
}
interface FooterRenderer {
render(pageNumber: number): string;
}
interface ExportFactory {
createHeaderRenderer(): HeaderRenderer;
createBodyRenderer(): BodyRenderer;
createFooterRenderer(): FooterRenderer;
}
期待される動作
const pdfFactory = new PdfExportFactory();
const htmlFactory = new HtmlExportFactory();
const exporter = new DocumentExporter(pdfFactory);
console.log(exporter.export('Title', 'Content', 1));
// PDF形式のヘッダー + ボディ + フッター
解答
// PDF実装
class PdfHeaderRenderer implements HeaderRenderer {
render(title: string): string {
return `[PDF HEADER] ===== ${title} =====`;
}
}
class PdfBodyRenderer implements BodyRenderer {
render(content: string): string {
return `[PDF BODY] ${content}`;
}
}
class PdfFooterRenderer implements FooterRenderer {
render(pageNumber: number): string {
return `[PDF FOOTER] --- Page ${pageNumber} ---`;
}
}
class PdfExportFactory implements ExportFactory {
createHeaderRenderer(): HeaderRenderer { return new PdfHeaderRenderer(); }
createBodyRenderer(): BodyRenderer { return new PdfBodyRenderer(); }
createFooterRenderer(): FooterRenderer { return new PdfFooterRenderer(); }
}
// HTML実装
class HtmlHeaderRenderer implements HeaderRenderer {
render(title: string): string {
return `<header><h1>${title}</h1></header>`;
}
}
class HtmlBodyRenderer implements BodyRenderer {
render(content: string): string {
return `<main><p>${content}</p></main>`;
}
}
class HtmlFooterRenderer implements FooterRenderer {
render(pageNumber: number): string {
return `<footer><span>Page ${pageNumber}</span></footer>`;
}
}
class HtmlExportFactory implements ExportFactory {
createHeaderRenderer(): HeaderRenderer { return new HtmlHeaderRenderer(); }
createBodyRenderer(): BodyRenderer { return new HtmlBodyRenderer(); }
createFooterRenderer(): FooterRenderer { return new HtmlFooterRenderer(); }
}
// エクスポーター(ファクトリーに依存)
class DocumentExporter {
private headerRenderer: HeaderRenderer;
private bodyRenderer: BodyRenderer;
private footerRenderer: FooterRenderer;
constructor(factory: ExportFactory) {
this.headerRenderer = factory.createHeaderRenderer();
this.bodyRenderer = factory.createBodyRenderer();
this.footerRenderer = factory.createFooterRenderer();
}
export(title: string, content: string, page: number): string {
return [
this.headerRenderer.render(title),
this.bodyRenderer.render(content),
this.footerRenderer.render(page),
].join('\n');
}
}
Mission 3: クエリビルダー(15分)
要件
ドキュメント検索用のクエリビルダーを実装してください。メソッドチェーンで条件を組み立てられるようにしてください。
期待される動作
const query = SearchQuery.builder()
.keyword('TypeScript')
.author('Takahashi')
.dateRange(new Date('2024-01-01'), new Date('2024-12-31'))
.sortBy('created_at', 'DESC')
.limit(20)
.offset(0)
.build();
console.log(query.toString());
// keyword=TypeScript, author=Takahashi, from=2024-01-01, to=2024-12-31, sort=created_at DESC, limit=20, offset=0
解答
class SearchQuery {
private constructor(
readonly keywordText: string | undefined,
readonly authorName: string | undefined,
readonly fromDate: Date | undefined,
readonly toDate: Date | undefined,
readonly sortField: string,
readonly sortDirection: 'ASC' | 'DESC',
readonly limitCount: number,
readonly offsetCount: number
) {}
static builder(): SearchQueryBuilder {
return new SearchQueryBuilder();
}
toString(): string {
const parts: string[] = [];
if (this.keywordText) parts.push(`keyword=${this.keywordText}`);
if (this.authorName) parts.push(`author=${this.authorName}`);
if (this.fromDate) parts.push(`from=${this.fromDate.toISOString().split('T')[0]}`);
if (this.toDate) parts.push(`to=${this.toDate.toISOString().split('T')[0]}`);
parts.push(`sort=${this.sortField} ${this.sortDirection}`);
parts.push(`limit=${this.limitCount}`);
parts.push(`offset=${this.offsetCount}`);
return parts.join(', ');
}
}
class SearchQueryBuilder {
private _keyword?: string;
private _author?: string;
private _fromDate?: Date;
private _toDate?: Date;
private _sortField = 'created_at';
private _sortDirection: 'ASC' | 'DESC' = 'DESC';
private _limit = 10;
private _offset = 0;
keyword(text: string): this {
this._keyword = text;
return this;
}
author(name: string): this {
this._author = name;
return this;
}
dateRange(from: Date, to: Date): this {
this._fromDate = from;
this._toDate = to;
return this;
}
sortBy(field: string, direction: 'ASC' | 'DESC' = 'DESC'): this {
this._sortField = field;
this._sortDirection = direction;
return this;
}
limit(count: number): this {
if (count < 0) throw new Error('Limit must be non-negative');
this._limit = count;
return this;
}
offset(count: number): this {
if (count < 0) throw new Error('Offset must be non-negative');
this._offset = count;
return this;
}
build(): SearchQuery {
return new (SearchQuery as any)(
this._keyword,
this._author,
this._fromDate,
this._toDate,
this._sortField,
this._sortDirection,
this._limit,
this._offset
);
}
}
Mission 4: テンプレートライブラリ(10分)
要件
ドキュメントテンプレートのPrototypeレジストリを実装してください。テンプレートをクローンしてカスタマイズできるようにしてください。
期待される動作
const registry = new TemplateRegistry();
registry.register('weekly-report', new DocumentTemplate('Weekly Report', ['Summary', 'Details', 'Action Items'], 'formal'));
const myReport = registry.get('weekly-report');
myReport.title = 'Week 42 Report';
myReport.sections[0] = 'This Week Summary';
// 元のテンプレートは変わらない
解答
class DocumentTemplate {
constructor(
public title: string,
public sections: string[],
public style: string
) {}
clone(): DocumentTemplate {
return new DocumentTemplate(
this.title,
[...this.sections], // 配列の深いコピー
this.style
);
}
}
class TemplateRegistry {
private templates: Map<string, DocumentTemplate> = new Map();
register(name: string, template: DocumentTemplate): void {
this.templates.set(name, template);
}
get(name: string): DocumentTemplate {
const template = this.templates.get(name);
if (!template) throw new Error(`Template not found: ${name}`);
return template.clone(); // クローンを返す(元を保護)
}
list(): string[] {
return Array.from(this.templates.keys());
}
unregister(name: string): void {
this.templates.delete(name);
}
}
// 使い方
const registry = new TemplateRegistry();
registry.register('weekly-report', new DocumentTemplate(
'Weekly Report',
['Summary', 'Details', 'Action Items'],
'formal'
));
registry.register('meeting-notes', new DocumentTemplate(
'Meeting Notes',
['Attendees', 'Agenda', 'Discussion', 'Next Steps'],
'casual'
));
const myReport = registry.get('weekly-report');
myReport.title = 'Week 42 Report';
myReport.sections[0] = 'This Week Summary';
const original = registry.get('weekly-report');
console.log(original.title); // 'Weekly Report'(変わっていない)
console.log(original.sections[0]); // 'Summary'(変わっていない)
Mission 5: 総合:ドキュメント管理システム(10分)
要件
Mission 1-4 のパターンを組み合わせて、ドキュメント管理システムの使用例を記述してください。以下の流れを実装してください:
- テンプレートレジストリからテンプレートを取得(Prototype)
- テンプレートを元にドキュメントを生成(Factory Method)
- 検索クエリを組み立て(Builder)
- エクスポーターでPDF出力(Abstract Factory)
解答
// 1. テンプレートからクローン
const template = templateRegistry.get('weekly-report');
template.title = 'Q4 Final Report';
// 2. ドキュメント生成
const doc = DocumentFactory.create('report', template.title);
// 3. 検索クエリの組み立て
const query = SearchQuery.builder()
.keyword(template.title)
.dateRange(new Date('2024-10-01'), new Date('2024-12-31'))
.sortBy('created_at', 'DESC')
.limit(5)
.build();
console.log(`Search: ${query.toString()}`);
// 4. PDF出力
const pdfExporter = new DocumentExporter(new PdfExportFactory());
const output = pdfExporter.export(
doc.getTitle(),
doc.getContent(),
1
);
console.log(output);
達成度チェック
| ミッション | テーマ | 完了 |
|---|---|---|
| Mission 1 | Factory Method でドキュメント生成 | |
| Mission 2 | Abstract Factory でエクスポーター | |
| Mission 3 | Builder でクエリ組み立て | |
| Mission 4 | Prototype でテンプレート管理 | |
| Mission 5 | 総合:パターンの組み合わせ |
まとめ
| ポイント | 内容 |
|---|---|
| Factory Method | 種類に応じたオブジェクト生成の抽象化 |
| Abstract Factory | 関連するオブジェクト群の一貫した生成 |
| Builder | 複雑なオブジェクトの段階的構築 |
| Prototype | テンプレートのクローンによる効率的な生成 |
チェックリスト
- 各生成パターンを実装できた
- パターンの使い分けを判断できるようになった
- 複数のパターンを組み合わせた設計ができた
次のステップへ
次はチェックポイントクイズです。生成パターンの理解度を確認しましょう。
推定読了時間: 60分