0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptで学べるデザインパターン 〜Decoratorパターン〜 (飾り枠と中身の同一視)

Last updated at Posted at 2024-12-17

Decoratorパターン とは?

一言で言うと、

既存のオブジェクトに新しい機能を動的に追加するパターンです。

詳しく言うと、

Decoratorパターンは、オブジェクトにいろいろな飾りをつけるようなものです。例えば、クリスマスツリーを飾るときに、最初は普通の木だけど、電球やオーナメントを追加していくことで、どんどんきれいになっていくイメージです。同じように、プログラムでも基本的な機能を持つオブジェクトに、新しい機能を次々と追加していくことができます。

日常での具体例

クリスマスツリーの飾り付け

クリスマスツリーの基本構成は以下のような形式になります。

  • 基本コンポーネント: 飾りが何もついていないシンプルなクリスマスツリー。
  • 装飾(デコレーター): ライトやオーナメント、リボン、星などの飾り。
  • 装飾の組み合わせ: 飾りを一つずつ追加していくことで、ツリーの見た目や機能を動的に変化させることができる。

このようにクリスマスツリーは「基本のオブジェクトに対して、柔軟で独立した形で新しい機能を追加する」というDecoratorパターンの考え方が適用できます。

Decoratorパターンのメリット

1. 柔軟性が高い

新しい機能を簡単に追加できるため、システムの拡張が容易です。
例: コーヒーショップのアプリで、新しいトッピングを追加する際に、既存のコードを変更せずに新しいDecoratorクラスを作成するだけで対応できます。

2. 単一責任の原則を守れる

各Decoratorクラスが特定の機能に集中するため、コードの管理がしやすくなります。
例: ログ機能、暗号化機能、圧縮機能など、それぞれを別々のDecoratorクラスとして実装できます。

Decoratorパターンのデメリット

1. 小さなクラスが多くなる

機能ごとに新しいクラスを作成するため、クラスの数が増えてしまいます。
例: コーヒーショップのアプリで、ミルク、砂糖、シロップなど、トッピングの種類が増えるたびに新しいクラスが必要になります。

2. コードが複雑になる可能性がある

Decoratorを重ねすぎると、オブジェクトの構造が分かりにくくなることがあります。
例: new MilkDecorator(new SugarDecorator(new SyrupDecorator(new SimpleCoffee()))) のように、多くのDecoratorを組み合わせると可読性が低下します。

typescriptの@ (デコレーター)との関係

TypeScriptのデコレーターは、クラスやメソッド、プロパティ、パラメーターに付与して、それらの動作を修飾するための構文です。
デコレーターパターンを簡略化して提供する機能です。

デコレーター構文の例

function Log(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
    console.log(`Log: ${propertyKey}`);
}

class Example {
    @Log
    sayHello() {
        console.log("Hello, world!");
    }
}

const example = new Example();
example.sayHello();
// Output:
// Log: sayHello
// Hello, world!

デコレーターパターンとの関係性

1. 目的の共通点

デコレーターパターン: オブジェクトに新しい振る舞いを動的に追加。
TypeScriptのデコレーター: クラスやそのメンバーに動的に機能を追加。

2. 適用方法の違い

デコレーターパターン: 明示的にデコレータークラスや関数を使用して対象を修飾。
TypeScriptのデコレーター: @デコレーター構文を使い、シンプルに適用可能。

3. 拡張性

TypeScriptのデコレーターは、メタプログラミング的な操作(クラスやメソッドのメタデータを操作するなど)にも対応可能で、さらに柔軟です。

Decoratorパターンをコードで説明

今回、想定するケース

コーヒーショップで、基本のコーヒーにトッピングを追加していくシステムを考えます。

クラス図

サンプルコード

interface Coffee {
  getDescription(): string;
  getCost(): number;
}

class SimpleCoffee implements Coffee {
  getDescription(): string {
    return "普通のコーヒー";
  }
  getCost(): number {
    return 300;
  }
}

abstract class CoffeeDecorator implements Coffee {
  protected coffee: Coffee;

  constructor(coffee: Coffee) {
    this.coffee = coffee;
  }

  getDescription(): string {
    return this.coffee.getDescription();
  }

  getCost(): number {
    return this.coffee.getCost();
  }
}

class MilkDecorator extends CoffeeDecorator {
  getDescription(): string {
    return this.coffee.getDescription() + "、ミルク追加";
  }
  getCost(): number {
    return this.coffee.getCost() + 50;
  }
}

class SugarDecorator extends CoffeeDecorator {
  getDescription(): string {
    return this.coffee.getDescription() + "、砂糖追加";
  }
  getCost(): number {
    return this.coffee.getCost() + 30;
  }
}

// 使用例
const simpleCoffee = new SimpleCoffee();
console.log(simpleCoffee.getDescription() + ": " + simpleCoffee.getCost() + "");

const milkCoffee = new MilkDecorator(simpleCoffee);
console.log(milkCoffee.getDescription() + ": " + milkCoffee.getCost() + "");

const sweetMilkCoffee = new SugarDecorator(milkCoffee);
console.log(sweetMilkCoffee.getDescription() + ": " + sweetMilkCoffee.getCost() + "");

フローチャート

コードの解説

1. Coffeeインターフェース

interface Coffee {
  getDescription(): string;
  getCost(): number;
}
  • コーヒーを表す基本的なインターフェースです。
  • 2つのメソッドを定義しています:
    • getDescription: コーヒーの説明(例えば「普通のコーヒー」など)を返す。
    • getCost: コーヒーの価格を返す。

2. SimpleCoffeeクラス

class SimpleCoffee implements Coffee {
  getDescription(): string {
    return "普通のコーヒー";
  }
  getCost(): number {
    return 300;
  }
}
  • Coffeeインターフェースを実装した具体的なコーヒーのクラスです。
  • シンプルなコーヒー(何も追加していない状態)の説明と価格を提供します。

3. CoffeeDecorator 抽象クラス

abstract class CoffeeDecorator implements Coffee {
  protected coffee: Coffee;

  constructor(coffee: Coffee) {
    this.coffee = coffee;
  }

  getDescription(): string {
    return this.coffee.getDescription();
  }

  getCost(): number {
    return this.coffee.getCost();
  }
}
  • デコレーターパターンの基底クラスです。
  • 他のデコレーター(追加機能を持つクラス)を作る際に共通のロジックを提供します。
  • このクラス自体は直接使われず、サブクラスとして機能を追加します。
  • 重要ポイント
    • protected coffee: Coffee により、デコレーターは他のCoffeeオブジェクトをラップ(包む)します。
    • デフォルトでラップしているオブジェクト(coffee)のメソッドをそのまま呼び出します。

4. MilkDecorator クラス

class MilkDecorator extends CoffeeDecorator {
  getDescription(): string {
    return this.coffee.getDescription() + "、ミルク追加";
  }
  getCost(): number {
    return this.coffee.getCost() + 50;
  }
}
  • デコレーターパターンの1つの実装です。
  • コーヒーに「ミルク追加」の説明を付加し、価格に50円を加算します。

5. SugarDecorator クラス

class SugarDecorator extends CoffeeDecorator {
  getDescription(): string {
    return this.coffee.getDescription() + "、砂糖追加";
  }
  getCost(): number {
    return this.coffee.getCost() + 30;
  }
}
  • もう1つのデコレーターパターンの実装です。
  • コーヒーに「砂糖追加」の説明を付加し、価格に30円を加算します。

6. 使用例

const simpleCoffee = new SimpleCoffee();
console.log(simpleCoffee.getDescription() + ": " + simpleCoffee.getCost() + "");

const milkCoffee = new MilkDecorator(simpleCoffee);
console.log(milkCoffee.getDescription() + ": " + milkCoffee.getCost() + "");

const sweetMilkCoffee = new SugarDecorator(milkCoffee);
console.log(sweetMilkCoffee.getDescription() + ": " + sweetMilkCoffee.getCost() + "");
  • SimpleCoffee オブジェクトの作成:

    const simpleCoffee = new SimpleCoffee();
    console.log(simpleCoffee.getDescription() + ": " + simpleCoffee.getCost() + "");
    
    • 結果:
      普通のコーヒー: 300円
      
  • ミルク追加のデコレーターを使用:

    const milkCoffee = new MilkDecorator(simpleCoffee);
    console.log(milkCoffee.getDescription() + ": " + milkCoffee.getCost() + "");
    
    • SimpleCoffeeに「ミルク」を追加。
    • 結果:
      普通のコーヒー、ミルク追加: 350円
      
  • さらに砂糖を追加:

    const sweetMilkCoffee = new SugarDecorator(milkCoffee);
    console.log(sweetMilkCoffee.getDescription() + ": " + sweetMilkCoffee.getCost() + "");
    
    • ミルク付きコーヒーに「砂糖」を追加。
    • 結果:
      普通のコーヒー、ミルク追加、砂糖追加: 380円
      

ポイント

  1. 柔軟性: デコレーターパターンにより、既存のコードを変更せずに動的に新しい機能を追加可能。
  2. 組み合わせ自由: 複数のデコレーターを順に適用することで、複雑な組み合わせを作れる。
  3. オープン/クローズド原則に従う: 新しいデコレーターを作ることで機能を拡張できるが、既存のクラスには手を加えない。

Decoratorパターンが用いられるケース

機能の動的な追加が必要な場合

具体例: テキストエディタで、基本的なテキスト編集機能に加えて、スペルチェック、自動保存、行番号表示などの機能を必要に応じて追加できるようにする。

継承を使わずに機能拡張をしたい場合

具体例: JavaのI/Oストリームクラス。FileInputStreamBufferedInputStreamDataInputStreamなどのデコレータを重ねることで、様々な機能を持つストリームを作成できる。

まとめ

Decoratorパターンは、オブジェクトに新しい機能を柔軟に追加するための強力なツールです。基本的な機能を持つクラスを作成し、それに様々な「装飾」を加えていくことで、複雑な機能を持つオブジェクトを作り上げることができます。このパターンを使うことで、コードの再利用性が高まり、新しい機能の追加が容易になります。ただし、デコレータを過剰に使用すると、コードが複雑になる可能性があるので注意が必要です。

参考サイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?