概要
「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。
今までに整理したもの
Template Method Pattern
Strategy Pattern
Observer Pattern
Composite Pattern
Command Pattern
Proxy Pattern
Decorator Pattern
- あるクラス(ConcreteComponent)とそれをラップしたクラス(Decorator)が同じインターフェース(Component)を実装することで同一視できるようにする、というもの
- Decorator は内部に持っている Component に処理を移譲しつつ、必要な処理を追加する
実装例
某コーヒーショップでオリジナルのドリンク(ラテ)にトッピング(エスプレッソショット と バニラシロップ)を追加することを考えてみます。
Java での実装
Component
単純に値段と商品名を返すインターフェースにします。
public interface Menu {
public int getPrice();
public String getName();
}
ConcreteComponent
これも簡単のため常に定数を返すようにします。
class Latte implements Menu {
@Override
public int getPrice() {
return 300;
}
@Override
public String getName() {
return "Latte";
}
}
Decorator
public abstract class Topping implements Menu {
protected Menu base;
public Topping(Menu base) {
this.base = base;
}
}
ConcreteDecorator
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"; // 名称も追記
}
}
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";
}
}
呼び出し
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
class Latte
def name
"Latte"
end
def price
300
end
end
Decorator
class Topping
def initialize(base)
@base = base
end
end
ConcreteDecorator
class EspressoShot < Topping
def price
@base.price + 50
end
def name
@base.name + " with Espresso Shot"
end
end
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 を使う
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 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