1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaエンジニアがRubyでデザインパターンを学ぶ - Decorator Pattern

Posted at

概要

「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。

今までに整理したもの

Template Method Pattern
Strategy Pattern
Observer Pattern
Composite Pattern
Command Pattern
Proxy Pattern

Decorator Pattern

png-2.png

  • あるクラス(ConcreteComponent)とそれをラップしたクラス(Decorator)が同じインターフェース(Component)を実装することで同一視できるようにする、というもの
  • Decorator は内部に持っている Component に処理を移譲しつつ、必要な処理を追加する

実装例

某コーヒーショップでオリジナルのドリンク(ラテ)にトッピング(エスプレッソショット と バニラシロップ)を追加することを考えてみます。

Java での実装

Component

単純に値段と商品名を返すインターフェースにします。

Menu.java
public interface Menu {
  public int getPrice();
  public String getName();
}

ConcreteComponent

これも簡単のため常に定数を返すようにします。

Latte.java
class Latte implements Menu {
  @Override
  public int getPrice() {
    return 300;
  }

  @Override
  public String getName() {
    return "Latte";
  }
}

Decorator

Topping.java
public abstract class Topping implements Menu {
  protected Menu base;

  public Topping(Menu base) {
    this.base = base;
  }
}

ConcreteDecorator

EspressoShot.java
public class EspressoShot extends Topping {
  public EspressoShot(Menu base) {
    super(base);
  }

  @Override
  public int getPrice() {
    return base.getPrice() + 50; // ベースとなるメニューの料金に 50 円プラス
  }

  @Override
  public String getName() {
    return base.getName() + " with Espresso Shot"; // 名称も追記
  }
}
VanillaSyrup.java
public class VanillaSyrup extends Topping {
  public VanillaSyrup(Menu base) {
    super(base);
  }

  @Override
  public int getPrice() {
    return base.getPrice() + 50;
  }

  @Override
  public String getName() {
    return base.getName() + " with Vanilla Syrup";
  }
}

呼び出し

Main.java
import java.util.*;

class CashRegister {
  public static final String INVOICE_FORMAT = "  %-50s ¥%4d%n";

  private List<Menu> orders;

  public CashRegister() {
    this.orders = new ArrayList<>();
  }

  public void order(Menu menu) {
    orders.add(menu);
  }

  /**
   * order の一覧と合計金額を表示します
   */
  public void printInvoice() {
    for (Menu order : orders) {
      System.out.printf(INVOICE_FORMAT, order.getName(), order.getPrice());
    }
    System.out.println("────────────────────────────────────────────────────────────");
    System.out.printf(INVOICE_FORMAT, "Total", orders.stream().mapToInt(Menu::getPrice).sum());
  }
}


/**
 * Main
 */
public class Main {
  public static void main(String[] args) {
    CashRegister register = new CashRegister();

    register.order(new Latte());
    register.order(new VanillaSyrup(new EspressoShot(new Latte()))); // Decorator は再帰的な構造になっているので複数の Decorator を組み合わせることも可能
    register.order(new VanillaSyrup(new Latte()));

    register.printInvoice();
  }
}

実行結果

  Latte                                              ¥ 300
  Latte with Espresso Shot with Vanilla Syrup        ¥ 400
  Latte with Vanilla Syrup                           ¥ 350
────────────────────────────────────────────────────────────
  Total                                              ¥1050

Ruby での実装

interface がないだけで基本的には Java と同じようになります。

Component

不要

ConcreteComponent

Latte
class Latte
  def name
    "Latte"
  end

  def price
    300
  end
end

Decorator

Topping
class Topping
  def initialize(base)
    @base = base
  end
end

ConcreteDecorator

EspressoShot
class EspressoShot < Topping
  def price
    @base.price + 50
  end

  def name
    @base.name + " with Espresso Shot"
  end
end
VanillaSyrup
class VanillaSyrup < Topping
  def price
    @base.price + 50
  end

  def name
    @base.name + " with Vanilla Syrup"
  end
end

呼び出し

class CashRegister
  INVOICE_FORMAT = "  %-50s ¥%4d"

  def initialize
    @orders = []
  end

  def order(menu)
    @orders << menu
  end

  # order の一覧と合計金額を表示します
  def print_invoice
    @orders.each do |order|
      puts INVOICE_FORMAT % [order.name, order.price]
    end
    puts "────────────────────────────────────────────────────────────"
    puts INVOICE_FORMAT % ['Total', @orders.map(&:price).inject(&:+)]
  end
end


register = CashRegister.new

register.order(Latte.new)
register.order(VanillaSyrup.new(EspressoShot.new(Latte.new)))
register.order(VanillaSyrup.new(Latte.new))

register.print_invoice

Ruby での実装(より Ruby らしく)

Decorator で付加したい処理がシンプル、かつあまり使い回ししないのであれば、クラスを定義せずに動的にオブジェクトを変更してしまうことでも対応可能。

alias を使う

alias
latte = Latte.new

class << latte
  # 元のメソッドに別名を付けて退避
  alias old_price price
  alias old_name name

  # Latte#price を上書きするが、元のメソッドは old_price で参照可
  def price
    old_price + 50
  end

  # 同上
  def name
    old_name + " with Espresso Shot"
  end
end


register = CashRegister.new
register.order(latte)
register.print_invoice

実行結果

  Latte with Espresso Shot                           ¥ 350
────────────────────────────────────────────────────────────
  Total                                              ¥ 350

module を使う

オブジェクトに対して機能を追加する、という意味では module を使った方が Ruby っぽいかも。

module
module EspressoShot
  def price
    super + 50
  end

  def name
    super + " with Espresso Shot"
  end
end

module VanillaSyrup
  def price
    super + 50
  end

  def name
    super + " with Vanilla Syrup"
  end
end

latte = Latte.new
latte.extend(EspressoShot) # オブジェクトに module を取り込む
latte.extend(VanillaSyrup) # 特にこのあたりは latte に対して Decoration を追加している、というのが分かりやすい

register = CashRegister.new
register.order(latte)
register.print_invoice

実行結果

  Latte with Espresso Shot with Vanilla Syrup        ¥ 400
────────────────────────────────────────────────────────────
  Total                                              ¥ 400

alias, module を使う場合の注意

  • 処理内容が複雑になってくると alias は可読性が落ちる
  • alias, module の場合どちらもオブジェクトを直接変更しているので、元の(Latteクラスの)オブジェクトに戻すことはできない

Decorator Pattern を使わないと何が問題なのか?

既存のクラスに処理を追加するのであれば継承を用いて次のようにしても問題は解決できるが、この方法では必要なトッピングの組み合わせの全てのクラスを定義しなければならず、自由度が低い。

継承による場合
class LatteWithEspressoShot < Latte
  def price
    super + 50
  end

  def name
    super + " with Espresso Shot"
  end
end

「Decorator クラス自体も Component」という再帰的な構造にしておくことで、Decorator の組み合わせを利用者が自由かつ動的に選択できる。

Proxy Pattern との違い

クラス構造的には同じようなものだが、Decorator Pattern は文字通りオブジェクトを Decorate することが目的。
Proxy Pattern はあくまでも内包しているオブジェクトのコントロール(アクセス権のチェックやインスタンス生成のタイミング制御など)を目的として、利用者と実オブジェクトとの間に仲介役として Proxy を挟む。

参考

Olsen, R. 2007. Design Patterns in Ruby

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?