EXERCISE 60分

ストーリー

高橋アーキテクト
生成パターンを4つ学んだ。理論だけでは身につかない。実際に手を動かしてみよう
高橋アーキテクト
ドキュメント管理システムの設計を通じて、各パターンの使い所を体感してもらう

ミッション概要

ミッションテーマ対象パターン目安時間
Mission 1ドキュメント生成Factory Method10分
Mission 2エクスポーターファミリーAbstract Factory15分
Mission 3クエリビルダーBuilder15分
Mission 4テンプレートライブラリPrototype10分
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 のパターンを組み合わせて、ドキュメント管理システムの使用例を記述してください。以下の流れを実装してください:

  1. テンプレートレジストリからテンプレートを取得(Prototype)
  2. テンプレートを元にドキュメントを生成(Factory Method)
  3. 検索クエリを組み立て(Builder)
  4. エクスポーターで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 1Factory Method でドキュメント生成
Mission 2Abstract Factory でエクスポーター
Mission 3Builder でクエリ組み立て
Mission 4Prototype でテンプレート管理
Mission 5総合:パターンの組み合わせ

まとめ

ポイント内容
Factory Method種類に応じたオブジェクト生成の抽象化
Abstract Factory関連するオブジェクト群の一貫した生成
Builder複雑なオブジェクトの段階的構築
Prototypeテンプレートのクローンによる効率的な生成

チェックリスト

  • 各生成パターンを実装できた
  • パターンの使い分けを判断できるようになった
  • 複数のパターンを組み合わせた設計ができた

次のステップへ

次はチェックポイントクイズです。生成パターンの理解度を確認しましょう。


推定読了時間: 60分