最近讀書會在讀深入淺出設計模式,趁這個機會複習一下設計模式,試著舉出簡單的例子並且用非模式與模式的方式來實作,比較它們的差異與優缺點。
咖啡廳有買三種輕食(Food):漢堡(Burger)、貝果(Bagel)與三明治(Sandwich),它們有各自的名稱與價錢如下:
另外還提供客製化的服務,可以在輕食中加上配料(Additive),每個配料也有名稱與價錢:
系統必須能列印出客製化輕食的名稱與計算總金額,例如:有一片起司加上蕃茄的漢堡,要顯示名稱: "burger + cheese + tomato",金額則是 30 + 10 + 5 = 45元。
class Food
def initialize
@cheese_count = 0
@tomato_count = 0
@ham_count = 0
end
def name
' + cheese' * @cheese_count +
' + tomato' * @tomato_count +
' + ham' * @ham_count
end
def cost
10 * @cheese_count +
5 * @tomato_count +
15 * @ham_count
end
def add(additive)
if additive == :cheese
@cheese_count += 1
elsif additive == :tomato
@tomato_count += 1
elsif additive == :ham
@ham_count += 1
end
end
end
class Burger < Food
def name
'burger' + super
end
def cost
30 + super
end
end
class Bagel < Food
def name
'bagel' + super
end
def cost
35 + super
end
end
class Sandwich < Food
def name
'sandwich' + super
end
def cost
25 + super
end
end
burger = Burger.new
burger.add(:cheese)
burger.add(:tomato)
p burger.name
p burger.cost
sandwich = Sandwich.new
sandwich.add(:ham)
sandwich.add(:ham)
sandwich.add(:cheese)
p sandwich.name
p sandwich.cost
class Food
def name
fail 'Not implement'
end
def cost
fail 'Not implement'
end
end
class Burger < Food
def name
'burger'
end
def cost
30
end
end
class Bagel < Food
def name
'bagel'
end
def cost
35
end
end
class Sandwich < Food
def name
'sandwich'
end
def cost
25
end
end
class Additive < Food
def initialize(food)
@food = food
end
end
class Cheese < Additive
def name
"#{@food.name} + cheese"
end
def cost
@food.cost + 10
end
end
class Tomato < Additive
def name
"#{@food.name} + tomato"
end
def cost
@food.cost + 5
end
end
class Ham < Additive
def name
"#{@food.name} + ham"
end
def cost
@food.cost + 15
end
end
burger = Burger.new
burger = Tomato.new(Cheese.new(burger))
p burger.name
p burger.cost
sandwich = Sandwich.new
sandwich = Cheese.new(Ham.new(Ham.new(sandwich)))
p sandwich.name
p sandwich.cost
Decorator - 裝飾者模式
將原本的 class(Food) 一些附加的行為(加上 cheese, tomato, ham)封裝成另一個系列的 class(Addtive) ,這些 class(Addtive) 仍然是繼承於原本 class(Food) 而擁有與之前 class 相同的界面。在實作上會將原本的 class(Food) 的 instance(burger, sandwich) 傳入到裝飾者的 class 中並在對應的 method(name, cost) 去做修改,也就是將原本的 instance(burger) 包在一個裝飾的 instance(Cheese.new(burger)) 裡,而使用 instance 的程式實際面對的是裝飾者 class 的 instance 而非原本的 instance。
當某個 class(Food) 擁有附加行為(加上 cheese, tomato, ham)時,這些行為會變動原本的 method(name, cost),另外這些行為在未來有可能需要變動。
下面是我一開始不使用pattern實作的版本,基本上多了一個 CustomizedFood 來處理輕食與配料的組合:
class Food
def name
fail 'Not implement'
end
def cost
fail 'Not implement'
end
end
class Burger < Food
def name
'burger'
end
def cost
30
end
end
class Bagel < Food
def name
'bagel'
end
def cost
35
end
end
class Sandwich < Food
def name
'sandwich'
end
def cost
25
end
end
class Additive < Food
end
class Cheese < Additive
def name
'cheese'
end
def cost
10
end
end
class Tomato < Additive
def name
'tomato'
end
def cost
5
end
end
class Ham < Additive
def name
'ham'
end
def cost
15
end
end
class CustomizedFood < Food
def initialize(food)
@food = food
@additives = []
end
def name
"#{@food.name}#{@additives.map {|ad| " + #{ad.name}"}.join}"
end
def cost
@food.cost + @additives.map(&:cost).inject(:+)
end
def add(additive)
@additives << additive
self
end
end
burger = CustomizedFood.new(Burger.new)
burger.add(Cheese.new).add(Tomato.new)
p burger.name
p burger.cost
sandwich = CustomizedFood.new(Sandwich.new)
sandwich.add(Ham.new).add(Ham.new).add(Cheese.new)
p sandwich.name
p sandwich.cost
class Discount20Off < Additive
def name
"#{@food.name} (20% off)"
end
def cost
@food.cost * 0.8
end
end
在參加讀書會的時候,有人提出了另一種實作的方式,基本上就是將處理name與cost的方式存在array中,再一個個拿出來處理,有點類似備註1的做法:
class Food
def name
fail 'Not implement'
end
def cost
fail 'Not implement'
end
end
class Burger < Food
def name
'burger'
end
def cost
30
end
end
class Bagel < Food
def name
'bagel'
end
def cost
35
end
end
class Sandwich < Food
def name
'sandwich'
end
def cost
25
end
end
class Additive
def name(input_name)
fail 'Not implement'
end
def cost(input_cost)
fail 'Not implement'
end
end
class Cheese < Additive
def name(input_name)
"#{input_name} + cheese"
end
def cost(input_cost)
input_cost + 10
end
end
class Tomato < Additive
def name(input_name)
"#{input_name} + tomato"
end
def cost(input_cost)
input_cost + 5
end
end
class Ham < Additive
def name(input_name)
#{input_name} + ham"
end
def cost(input_cost)
input_cost + 15
end
end
class CustomizedFood < Food
def initialize(food)
@food = food
@additives = []
end
def name
result = @food.name
@additives.each do |ad|
result = ad.name(result)
end
result
end
def cost
result = @food.cost
@additives.each do |ad|
result = ad.cost(result)
end
result
end
def add(additive)
@additives << additive
self
end
end
burger = CustomizedFood.new(Burger.new)
burger.add(Tomato.new)
burger.add(Cheese.new)
p burger.name
p burger.cost
sandwich = CustomizedFood.new(Sandwich.new)
sandwich.add(Cheese.new)
sandwich.add(Ham.new)
sandwich.add(Ham.new)
p sandwich.name
p sandwich.cost
class DisplayCost < Additive
def name
"#{@food.name} (#{@food.cost}元)"
end
def cost
@food.cost
end
end