今天要介紹的 design pattern 是 Flyweight 輕量模式,又稱作享元模式—沒關係,為什麼中文會是這麼不明覺厲的兩個字我也不是很明白,但是我們先繼續看下去。在拳擊的世界裡,Flyweight 代表的是最小的那個重量等級,翻作蠅量級。而在 design patterns 中,Flyweight 代表的是「多個物件共享同一份狀態」,利用這個共享的特徵,達到節省記憶體(RAM)的效果。
有人(比如說 Dive into Design Patterns 的作者)會說,這個特性跟**快取(cache)**的原理很像:將物件共通的部分存起來,下次需要使用相同的部分時就可以直接把存起來的資料拿來用,而不用浪費記憶體重新產生一個一模一樣的東西。不過快取基本上是「以空間換取時間」,將一次次計算的結果存放在額外的空間裡,等到下次要做同樣的計算時就可以直接拿算好的結果。Flyweight 則是比較像是「以彈性換取空間」,它的目的是節省記憶體,但是這會讓程式碼比較難做調整。
從前從前,有個人叫做小明。有一天,小明突然心血來潮,想到了驚人的絕世好遊戲點子:遊戲的一開始,玩家會有很多隻小雞,而任務就是好好養這些小雞,讓這些小雞變得越來越多,直到世界上都充滿了小雞。一旦世界上都充滿了小雞,就會所有人都開心,遊戲也就結束了。小明越想越興奮,馬上不眠不休地花了三天三夜將遊戲寫出來,但是正將他心滿意足地開啟遊戲,準備開始好好養小雞時,遊戲就在五分鐘之後當機了...小明重新啟動了幾次都出現同樣的情況,絕望的他心灰意冷地去翻找 log 紀錄,結果發現,系統在每次死掉之前,都會留下一個「out of memory」的錯誤訊息。
小明覺得非常不甘心,要失敗也不是輸給記憶體吧?於是他馬上上網訂了一台有超大 RAM 的電腦回去檢查他的程式碼,並且發現佔據他的記憶體大部分空間的不是別的,正是那為數眾多的小雞們。下面是他的 Chicken
類別。
在遊戲的設定中,小雞其實都長得差不多,也就是說,他們有很多共通的屬性。比如說,所有小雞都是 isCute == true
。但是每隻小雞都會保有自己的一套屬性,這表示在這個程式中大量消耗記憶體的很多都是重複的資料,這麼冗的事情當然是不被允許的,但是要如何才能有效節省記憶體空間呢?
眼睛快要閉上的小明決定用僅剩的最後一點力氣將 Design Patterns 的書翻開。這時,從窗外進來的風將書頁翻到了 Flyweight 的章節,小明看到了它的描述,滿足地笑了。
既然 Chicken
類別中大部分的 attributes 都是重複的,那麼就將這些容易重複的特徵抽出來變成另一個物件,並讓所有需要使用到這組特徵的 Chicken
實體去指涉(reference)。在這個簡單的 Chicken
類別中,isCute
與 color
都是很容易重複的狀態,因此他們就很適合被抽出來,變成所謂的 flyweight。所以,原本的 Chicken
類別中的三個屬性(color
, isCute
, name
)會被分為兩個部分: Intrinsic State 內存狀態 與 Extrinsic State 外存狀態。內存狀態是存在於 flyweight 中的、多個物件共享的狀態,而外存狀態就是從外部傳入、非共享的那些屬性們。
而 Context 就是將一個物件的 intrinsic state 與 extrinsic state 結合,變成一個有完整屬性的物件的地方,是給外部呼叫的介面,並且從外面來看,context 就是一個完整的物件。
對於 Client,外部使用 context 的人/物而言,介面是不會改變的,有沒有使用 flyweight 這個模式對於 client 不會造成任何影響,如此才會符合物件導向設計原則。
要將這個概念實現,我們除了存放共通狀態的 flyweight,還會需要一個容器來裝已經建立好的 flyweight 們。我們可以直接在 flyweight 類別裡面用 class variable 來存放,或是使用一個 flyweight factory(TODO: link to factory method)。下面例子裡我們用 flyweight 的 class variable 當容器。
首先,我們先來建立一個 Chicken
類別,這是我們的 context。它會使用到 flyweight 來放 color
is_cute
,而 name
會是自身的 extrinsic state。
# ---------------------------------------
# context
# ---------------------------------------
class Chicken
def initialize(color, is_cute, name)
@name = name
@chicken_type = ChickenType.for(color, is_cute)
end
def name
@name
end
# attribute from flyweight
def color
@chicken_type.color
end
# attribute from flyweight
def is_cute
@chicken_type.is_cute
end
# some behavior
def scatter
"#{@name} is scattering"
end
def be_cute
"#{@name} is being cute"
end
end
再來,我們實作 Chicken
類別的 flyweight:ChickenType
。這裡的 ChickenType
存了兩種狀態,我們也可以在 flyweight 裡面放行為,端看你怎麼使用。每一個 Chicken
都會使用 ChickenType
的 for()
方法來呼叫它需要的 flyweight。當 for()
被呼叫的時候,則會判斷自幾是否已經有了這個實體,如果沒有就做一個新的。
# ---------------------------------------
# flyweight
# ---------------------------------------
class ChickenType
@@types = {}
def self.for(color, is_cute)
if type = @@types[key(color, is_cute)]
type
else
@@types[key(color, is_cute)] = self.new(color, is_cute)
end
end
def initialize(color, is_cute)
@color = color
@is_cute = is_cute
end
def color
@color
end
def is_cute
@is_cute
end
def self.key(color, is_cute)
if is_cute
"cute_#{color}_chicken"
else
"#{color}_chicken"
end
end
end
最後,我們來看看外部(client)可以怎麼使用 Chicken
:
# ---------------------------------------
# client
# ---------------------------------------
class Pasture
def initialize
@chickens = []
end
def add_cute_chicken_to_pasture(color, is_cute, name)
puts "A new cute #{color} chicken named #{name}!\n"
chicken = Chicken.new(color, is_cute, name)
@chickens << chicken
# do other stuff
# ...
end
def list_chickens
puts 'Say hello to'
puts @chickens.map(&:name)
puts ':)'
end
end
Pasture
只知道它會有 Chicken
,並且可以得到每隻 Chicken
的 attributes,但是不會知道 Chicken
內部是怎麼被組成的。同理,Chicken
只會知道它有使用 ChickenType
作為 flyweight,但是這些 flyweight 是如何存放、管理的,Chicken
也不會/不用知道—這是我們想要的 encapsulation。
好的,來玩玩看!
# INPUT
pasture = Pasture.new
pasture.add_cute_chicken_to_pasture('green', true, 'Greeny')
pasture.add_cute_chicken_to_pasture('yellow', true, 'Yellowy')
pasture.add_cute_chicken_to_pasture('yellow', true, 'Yet Another Yellowy')
pasture.list_chickens
# OUTPUT
# A new cute green chicken named Greeny!
# A new cute yellow chicken named Yellowy!
# A new cute yellow chicken named Yet Another Yellowy!
# Say hello to
# Greeny
# Yellowy
# Yet Another Yellowy
# :)
?
Flyweight 的優點,顯而易見地,就是節省記憶體。它能夠減少物件儲存狀態所需要佔據的空間。但是它的代價就是會破壞原本物件導向 encapsulation 的特性—每個物件自己本身的狀態不再被封裝起來,而是需要從外部引入。並且,這也會讓程式碼變得比較不直觀—同一個物件中的狀態必須要從不同的類別中去看—一不小心就會降低程式碼的可讀性。
Flyweight 與 Singleton(TODO: 放 singleton 連結)的概念有一點像。Singleton 是一種讓一個類別只會有一個實體(object)的設計,可以將它類比成當 Flyweight 物件只有一個的情況。不過 Flyweight 與 Singleton 有兩個不同的地方:
Flyweight 叫做「輕量模式」,因為它可以讓你的應用程式「變輕」但同時又力量不減(功能相同)。如果今天我們的記憶體有限,必須要最大化的節省記憶體的空間,並且每個物件都有許多可以被共存的狀態,那麼 flyweight 或許會是一個好選擇。
對了,小明在使用了 flyweight pattern 之後,終於順利讓遊戲跑起來了,並且也快樂地養著他許許多多的小雞們。
作者:Jenny