Menu
Coddy logo textTech

Decoratorパターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 93/107。

Decoratorパターンは、オブジェクトを他のオブジェクトでラップすることにより、動的に新しい振る舞いを追加することを可能にします。Adapterはインターフェースを別のものに適合するように変更しますが、Decoratorは同じインターフェースを維持しつつ、元のオブジェクトの周囲にラッパーを重ねることで機能を強化します。

Goでは、基本型とデコレータの両方に同じインターフェースを実装させることで、これを実現します:

type Notifier interface {
    Send(message string) string
}

type BasicNotifier struct{}

func (b BasicNotifier) Send(message string) string {
    return "Sending: " + message
}

デコレータは別の Notifier をラップし、それに委譲する前後に振る舞いを追加します:

type TimestampDecorator struct {
    wrapped Notifier
}

func (t TimestampDecorator) Send(message string) string {
    timestamped := "[2024-01-15] " + message
    return t.wrapped.Send(timestamped)
}

type UppercaseDecorator struct {
    wrapped Notifier
}

func (u UppercaseDecorator) Send(message string) string {
    return strings.ToUpper(u.wrapped.Send(message))
}

Decoratorの真骨頂は、複数のラッパーを積み重ねて振る舞いを組み合わせることにあります。

notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}

fmt.Println(notifier.Send("Hello"))
// 送信中: Hello

fmt.Println(withBoth.Send("Hello"))
// 送信中: [2024-01-15] HELLO

Decoratorは、コードを修正することなくオブジェクトに責務を追加する必要がある場合、特に機能のさまざまな組み合わせが必要な場合に最適です。これは、ロギング、キャッシュ、認証、および圧縮レイヤーによく使用されます。

challenge icon

チャレンジ

簡単

Decorator パターンを使って、コーヒーショップの注文システムを構築しましょう!ベースとなるコーヒー飲料と、ミルク、砂糖、ホイップクリームなどの追加トッピングを加えるデコレータを作成します。各デコレータは前の飲料をラップし、説明と価格の両方を積み上げていきます。これは、デコレータがどのように機能を階層化するかを示す、完璧な実世界の例です。

コードは以下の3つのファイルに分けて構成します:

  • beverage.go: コアとなるインターフェースとベースのコーヒー型を定義します。

    2つのメソッドを持つ Beverage インターフェースを作成します:

    • Description() string — 飲み物の内容を返します
    • Cost() float64 — 価格を返します

    ベースの飲料として Coffee 構造体を実装します。その説明は Coffee、コストは 2.00 とします。

  • decorators.go: 任意の飲料にトッピングを追加するデコレータ型を作成します。

    各デコレータは Beverage をラップし、同じインターフェースを実装します。3つのデコレータを作成してください:

    • MilkDecorator — 説明に , Milk を追加し、コストに 0.50 を追加します
    • SugarDecorator — 説明に , Sugar を追加し、コストに 0.25 を追加します
    • WhipDecorator — 説明に , Whip を追加し、コストに 0.75 を追加します

    各デコレータは、ラップされた飲料のメソッドを呼び出し、その結果を強化する必要があります。

  • main.go: デコレータを積み重ねて、カスタマイズされた飲み物を構築します。

    追加するトッピングの数を読み取ります。次に、各トッピングについて、そのタイプ(milksugar、または whip)を読み取り、現在の飲料を適切なデコレータでラップします。

    すべてのトッピングを適用した後、最終的な飲み物の説明とコストを別々の行に出力します。コストは $X.XX の形式で、小数点以下2桁まで表示してください。

以下の入力が提供されます:

  • 1行目:追加するトッピングの数
  • 続く行:1行につき1つのトッピングタイプ(milksugar、または whip

例えば、以下のように入力された場合:

2
milk
sugar

出力は以下のようになります:

Coffee, Milk, Sugar
$2.75

また、以下のように入力された場合:

3
whip
milk
milk

出力は以下のようになります:

Coffee, Whip, Milk, Milk
$3.75

また、以下のように入力された場合:

0

出力は以下のようになります:

Coffee
$2.00

各デコレータが前の飲料をラップし、説明とコストの両方を積み上げていく様子に注目してください。同じトッピングを複数回追加することもでき(ダブルミルクなど)、デコレータの順序が説明の順序を決定します。ベースのコーヒーはトッピングについて何も知りません。各デコレータは、自分がラップしているものを単に強化するだけです!

チートシート

Decorator パターンは、オブジェクトを他のオブジェクトでラップすることにより、動的に新しい振る舞いを追加します。同じインターフェースを維持しながら、レイヤー化を通じて機能を強化します。

基本型とデコレータの両方が同じインターフェースを実装します:

type Notifier interface {
    Send(message string) string
}

type BasicNotifier struct{}

func (b BasicNotifier) Send(message string) string {
    return "Sending: " + message
}

デコレータは別のオブジェクトをラップし、委譲の前後に振る舞いを追加します:

type TimestampDecorator struct {
    wrapped Notifier
}

func (t TimestampDecorator) Send(message string) string {
    timestamped := "[2024-01-15] " + message
    return t.wrapped.Send(timestamped)
}

type UppercaseDecorator struct {
    wrapped Notifier
}

func (u UppercaseDecorator) Send(message string) string {
    return strings.ToUpper(u.wrapped.Send(message))
}

複数のデコレータを積み重ねて、振る舞いを組み合わせることができます:

notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}

fmt.Println(notifier.Send("Hello"))
// Sending: Hello

fmt.Println(withBoth.Send("Hello"))
// SENDING: [2024-01-15] HELLO

オブジェクトのコードを変更せずに責任を追加するために Decorator を使用します。特に、さまざまな機能の組み合わせに有効です。一般的な用途:ロギング、キャッシュ、認証、圧縮。

自分で試してみよう

package main

import (
	"fmt"
)

func main() {
	// 追加トッピングの数を読み込む
	var count int
	fmt.Scanln(&count)

	// ベースとなるコーヒーから開始する
	var drink Beverage = &Coffee{}

	// 各トッピングを読み込み、適切なデコレータでドリンクをラップする
	for i := 0; i < count; i++ {
		var extra string
		fmt.Scanln(&extra)

		// TODO: トッピングの種類に基づいて、適切なデコレータでdrinkをラップする
		// "milk" の場合は MilkDecorator を使用する
		// "sugar" の場合は SugarDecorator を使用する
		// "whip" の場合は WhipDecorator を使用する
	}

	// TODO: 最終的な説明と価格を出力する
	// 価格は小数点以下2桁まで含めて $X.XX の形式で出力する
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン