iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 14
0

[Design Pattern] Flyweight 輕量模式

今天要介紹的 design pattern 是 Flyweight 輕量模式,又稱作享元模式—沒關係,為什麼中文會是這麼不明覺厲的兩個字我也不是很明白,但是我們先繼續看下去。在拳擊的世界裡,Flyweight 代表的是最小的那個重量等級,翻作蠅量級。而在 design patterns 中,Flyweight 代表的是「多個物件共享同一份狀態」,利用這個共享的特徵,達到節省記憶體(RAM)的效果。

有人(比如說 Dive into Design Patterns 的作者)會說,這個特性跟**快取(cache)**的原理很像:將物件共通的部分存起來,下次需要使用相同的部分時就可以直接把存起來的資料拿來用,而不用浪費記憶體重新產生一個一模一樣的東西。不過快取基本上是「以空間換取時間」,將一次次計算的結果存放在額外的空間裡,等到下次要做同樣的計算時就可以直接拿算好的結果。Flyweight 則是比較像是「以彈性換取空間」,它的目的是節省記憶體,但是這會讓程式碼比較難做調整。

一個故事

從前從前,有個人叫做小明。有一天,小明突然心血來潮,想到了驚人的絕世好遊戲點子:遊戲的一開始,玩家會有很多隻小雞,而任務就是好好養這些小雞,讓這些小雞變得越來越多,直到世界上都充滿了小雞。一旦世界上都充滿了小雞,就會所有人都開心,遊戲也就結束了。小明越想越興奮,馬上不眠不休地花了三天三夜將遊戲寫出來,但是正將他心滿意足地開啟遊戲,準備開始好好養小雞時,遊戲就在五分鐘之後當機了...小明重新啟動了幾次都出現同樣的情況,絕望的他心灰意冷地去翻找 log 紀錄,結果發現,系統在每次死掉之前,都會留下一個「out of memory」的錯誤訊息。

小明覺得非常不甘心,要失敗也不是輸給記憶體吧?於是他馬上上網訂了一台有超大 RAM 的電腦回去檢查他的程式碼,並且發現佔據他的記憶體大部分空間的不是別的,正是那為數眾多的小雞們。下面是他的 Chicken 類別。

chicken class original

在遊戲的設定中,小雞其實都長得差不多,也就是說,他們有很多共通的屬性。比如說,所有小雞都是 isCute == true。但是每隻小雞都會保有自己的一套屬性,這表示在這個程式中大量消耗記憶體的很多都是重複的資料,這麼冗的事情當然是不被允許的,但是要如何才能有效節省記憶體空間呢?

眼睛快要閉上的小明決定用僅剩的最後一點力氣將 Design Patterns 的書翻開。這時,從窗外進來的風將書頁翻到了 Flyweight 的章節,小明看到了它的描述,滿足地笑了。

使用 Flyweight Pattern

既然 Chicken 類別中大部分的 attributes 都是重複的,那麼就將這些容易重複的特徵抽出來變成另一個物件,並讓所有需要使用到這組特徵的 Chicken 實體去指涉(reference)。在這個簡單的 Chicken 類別中,isCutecolor 都是很容易重複的狀態,因此他們就很適合被抽出來,變成所謂的 flyweight。所以,原本的 Chicken 類別中的三個屬性(color, isCute, name)會被分為兩個部分: Intrinsic State 內存狀態Extrinsic State 外存狀態。內存狀態是存在於 flyweight 中的、多個物件共享的狀態,而外存狀態就是從外部傳入、非共享的那些屬性們。

Context 就是將一個物件的 intrinsic state 與 extrinsic state 結合,變成一個有完整屬性的物件的地方,是給外部呼叫的介面,並且從外面來看,context 就是一個完整的物件。

對於 Client,外部使用 context 的人/物而言,介面是不會改變的,有沒有使用 flyweight 這個模式對於 client 不會造成任何影響,如此才會符合物件導向設計原則。

transforming chicken class

要將這個概念實現,我們除了存放共通狀態的 flyweight,還會需要一個容器來裝已經建立好的 flyweight 們。我們可以直接在 flyweight 類別裡面用 class variable 來存放,或是使用一個 flyweight factory(TODO: link to factory method)。下面例子裡我們用 flyweight 的 class variable 當容器。

chicken class final version

用 Ruby 實作 Flyweight

首先,我們先來建立一個 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 都會使用 ChickenTypefor() 方法來呼叫它需要的 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 優缺點

Flyweight 的優點,顯而易見地,就是節省記憶體。它能夠減少物件儲存狀態所需要佔據的空間。但是它的代價就是會破壞原本物件導向 encapsulation 的特性—每個物件自己本身的狀態不再被封裝起來,而是需要從外部引入。並且,這也會讓程式碼變得比較不直觀—同一個物件中的狀態必須要從不同的類別中去看—一不小心就會降低程式碼的可讀性。

Flyweight 與 Singleton

Flyweight 與 Singleton(TODO: 放 singleton 連結)的概念有一點像。Singleton 是一種讓一個類別只會有一個實體(object)的設計,可以將它類比成當 Flyweight 物件只有一個的情況。不過 Flyweight 與 Singleton 有兩個不同的地方:

  1. Singleton 是只會有一個實體,也就是說從頭到尾只會有一個物件被產生。而 Flyweight 則是可以有很多個不同的實體(instance)。
  2. Singleton 物件的狀態是可以被改變的(immutable)(不過只要一改變就會影響到所有使用它的地方,所以必須謹慎再謹慎),而 Flyweight 物件(存放共通狀態的地方)是不應該被改變的(immutable)。

所以說

Flyweight 叫做「輕量模式」,因為它可以讓你的應用程式「變輕」但同時又力量不減(功能相同)。如果今天我們的記憶體有限,必須要最大化的節省記憶體的空間,並且每個物件都有許多可以被共存的狀態,那麼 flyweight 或許會是一個好選擇。

對了,小明在使用了 flyweight pattern 之後,終於順利讓遊戲跑起來了,並且也快樂地養著他許許多多的小雞們。

作者:Jenny


上一篇
[Design Pattern] Proxy 代理模式
下一篇
[Design Pattern] Abstract Factory 抽象工廠模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言