Menu
Coddy logo textTech

Decoratorパターン

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

Decoratorパターンを使うと、オブジェクトを別のオブジェクトでラップすることで、新しい振る舞いを動的に追加できます。Adapterがインターフェースを別のインターフェースに合わせて変更するのに対し、Decoratorは同じインターフェースを維持しながら、元のオブジェクトの周囲にラッパーを重ねることで機能を拡張します。

Goでは、base type と decorator の両方に同じ interface を実装させることで、これを実現します。

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は、オブジェクトのコードを変更せずに責務を追加する必要がある場合、特に機能のさまざまな組み合わせが必要な場合に最適です。これは、Logging、キャッシュ、認証、圧縮レイヤーによく使用されます。

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)を読み取り、現在の飲み物を適切なデコレーターでラップします。

    すべての追加要素を適用した後、最終的なドリンクの説明と価格を別々の行に出力します。価格は小数点以下2桁ちょうどの $X.XX 形式で表示します。

次の入力が与えられます。

  • 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

各デコレーターが前の飲み物をラップし、説明と価格の両方を積み重ねていくことに注目してください。同じ追加要素を複数回(ミルクを2倍にする場合など)追加でき、デコレーターの順序によって説明内の順序が決まります。基本のコーヒーは追加要素について何も知りません。各デコレーターは、ラップした対象をそのまま拡張するだけです。

自分で試してみよう

package main

import (
	"fmt"
)

func main() {
	// エクストラの数を読み取る
	var count int
	fmt.Scanln(&count)

	// ベースのコーヒーから始める
	var drink Beverage = &Coffee{}

	// 各エクストラを読み取り、適切なデコレータでdrinkをラップする
	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腕試し

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

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

自分で練習してみよう: Goオンラインコンパイラ