---
# System prepended metadata

title: Flyweight 享元模式
tags: [基礎進階, Java 設計模式]

---

---
title: 'Flyweight 享元模式'
disqus: kyleAlien
---

Flyweight 享元模式
===

## Overview of Content

使用共享對象有效的減少類的創建，增加物件的覆用率


如有引用參考請詳註出處，感謝 :cat:

:::success
* 如果喜歡讀更好看一點的網頁版本，可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
    
    本篇文章對應的是 [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/)
:::

[TOC]

## Flyweight 使用場景

1. **需要`緩衝池`的場景**、**存在大量相似對向**

2. 細粒度的物件都具備相近的外部狀態，而內部狀態與環境無關（物件沒有特定身份，可以相互替換，也就是這個物件更像是「值語意」物件）

### Flyweight 定義 & UML：內部、外部狀態

* **Flyweight 定義**：使用共享物件去支援，大量的細粒度物件

    :::success
    * 細粒度物件：物件的每個成員可以區分為 **內部狀態 (`intrinsic`)**、**外部狀態 (`extrinsic`)**

        * **內部狀態 (`intrinsic`)**：代表它是一個物件中的 **可共享的成員**，是屬於「值物件」類型
            
            > 儲存在 **享元對象** 內部並且不會隨著環境改變而改變

        * **外部狀態 (`extrinsic`)**：代表它是一個物件中的 **不可共享的成員**，用來當作對象的唯一標誌（不可變更修改）
        
            > 用來作為標示物件，重在「唯一性」的特質
    :::

* **Flyweight UML 物件角色**

    | 物件角色 | 說明 |
    | - | - |
    | Flyweight(抽象) | 定義產品的可共享（內部狀態）、不可共享（外部狀態）的實現或接口 |
    | ConcreteFlyweight | 具體享元（產品）角色；可共享的成員處理須與環境無間，不應該出現一個操作改變可共享成員，又同時修改不可共享成員 |
    | UnsharedConcreteFlyweight | 不可共享的享元（產品）角色；**不存在外部狀態、安全需求導致無法共享的角色** |
    | FlyweightFactory | **內部創建一個池容器，用來除存 Flyweight 對象** |

    > ![](https://i.imgur.com/4PbYLIP.png)

### Flyweight 設計 - 優缺點

* **Flyweight 設計優點**：

    * 節省類的創建，也加快速度 ([**Java 創建類**](https://hackmd.io/kKOqFUXpRhyYTchh98xUUQ?view#虛擬機堆中對象的創建)十分耗資源)

    * 降低內存消耗、減少 GC 回收

* **Flyweight 設計缺點**：
    
    * 提高系統的複雜性！（要分離出外部狀態、內部狀態）
    

## Flyweight 實現


### Flyweight 標準

1. **`Flyweight` 抽象類**：持有不可修改的外部對象、可修改的內部對象

    ```kotlin=
    // 建構函數有不可修改的外對象
    abstract class Flyweight constructor(val extrinsic: String) {
        
        // 可修改的內部對象
        var intrinsic : Any? = null
        
        abstract fun operation()

    }
    ```
    
2. **`ConcreteFlyweight` 類**：實做 Flyweight 抽象類、其操作函數

    ```kotlin=
    // 實做 Flyweight
    class ConcreteFlyweight constructor(extrinsic: String): 
        Flyweight(extrinsic) 
    
        override fun operation() {
            println("Show intrinsic date: $intrinsic")
        }

    }
    ```

3. **`FlyweightFactory` 類**：享元工廠，內部持有可覆用的 `Flyweight` 對象，透過外部特徵（不可修改的成員）儲存 Flyweight 對象

    > 可以透過外部特徵（狀態）去判斷該物件是否共享

    ```kotlin=
    object FlyweightFactory {

        // 儲存共享物件
        private val pool = HashMap<String, Flyweight>()

        fun getFlyweightObject(extrinsic: String) : Flyweight {
            if (!pool.containsKey(extrinsic)) {
                pool[extrinsic] = ConcreteFlyweight(extrinsic)
            }

            return pool.getValue(extrinsic)
        }

    }
    ```

* 使用享元設計：

    ```kotlin=
    fun main() {
        FlyweightFactory.run {
            
            // "Hello" 代表了外部狀態
            val instance1 = getFlyweightObject("Hello")
            val instance2 = getFlyweightObject("Hello")

            println("instance1 == instance2? ${instance1 == instance2}")

        }
    }
    ```
    > ![](https://i.imgur.com/VcFyt7X.png)


## Flyweight 模式的考量


### Thread 安全性

* 我們知道 Java、Kotlin 天生是多線程程式，由於多線程的問題，我們在使用多線程訪問同一個函數創建對象並設定成員總會造成 **不安全設定**
    

* 基於這個問題，我們在創建對象時要使用 **鎖** 才安全；以下提出幾個我們在做併發程式時會使用到的多線程（多執行序）鎖，它可以保證線程操作安全性

    1. **使用 `@Synchronized` 註解同步方法**

        ```kotlin=
        object FlyweightFactory {

            private val pool = HashMap<String, Flyweight>()

            @Synchronized
            fun getFlyweightObject(extrinsic: String) : Flyweight {

                if (!pool.containsKey(extrinsic)) {
                    pool[extrinsic] = ConcreteFlyweight(extrinsic)
                }

                return pool.getValue(extrinsic)
            }

        }
        ```
        
    2. **使用 `ReentrantLock` 同步方法**
        
        ```kotlin=
        object FlyweightFactory {

            private val pool = HashMap<String, Flyweight>()

            private val lock = ReentrantLock()

            fun getFlyweightObject(extrinsic: String) : Flyweight {
                lock.lock()

                if (!pool.containsKey(extrinsic)) {
                    pool[extrinsic] = ConcreteFlyweight(extrinsic)
                }

                lock.unlock()
                return pool.getValue(extrinsic)
            }

        }
        ```

### 外部狀態 - 數量 & 性能

* **物件數量**：在使用享元模式時，物件池 (`Pool`) 中的**享元物件不能太少**，要到足夠滿足業務為止！
    
    :::info
    * **外部狀態**
        
        控制享元物件的創建重點是在 **外部狀態**，外部狀態過少過於簡單會導致物件的創建數量便少，從而更容易產生 Thread 問題
        
        > 外部狀態控制了內部的物件數量
    
    * **內部狀態**
        
        內部狀態是被管理的物件的特徵；與物件本身較有關係
    :::

* **性能**：外部狀態要使用 Java 內建還是自己創建？都可以～ **但依照性能來講建議還是使用 Java/Kotlin 內建物件**

    * 如果要自己創建物件請務必複寫 
        
        1. `equals` 方法（影響 `Map#Contains(Object)` ）
        
        2. `hashCode` 方法（影響 `Map#get(Object)` ）


        ```kotlin=
        class SelfObject(private val extrinsic1: String, private val extrinsic2: String) {

            var intrinsic : Any? = null
            override fun hashCode(): Int {
                return extrinsic1.hashCode() + extrinsic2.hashCode()
            }

            override fun equals(other: Any?): Boolean {
                if (other == null || other !is SelfObject) return false

                return other.extrinsic1.equals(extrinsic1) && 
                        other.extrinsic2.equals(extrinsic2)
            }
        }
        ```

## Android source Message 覆用

我們知道 Android 的 UI 事件是仰賴於「**事件驅動模型**」，它的設計概念是透過事件（可以是使用者發出，或是程式邏輯發出）來驅動 UI 給予不同的反應；

:::warning
而物件導向程式，在這種設計之下… 就會導致一個問題點的產生，就是「事件的這個物件過多」的問題… 如果沒有正視考慮到這個問題，那就很容易導致 OOM 的發生
:::

常見的用法如下…

```java=
// 範例

void handlerSendMsgToUpdateUI() {
    Handler handler = new Handler(Looper.getMainLooper());

    Message message = Message.obtain(handler, () -> {
        // 更新 UI
    });

    handler.sendMessage(message);
}
```

:::success
* 以下分析的是 Android 10 的 [**Message.java**](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/os/Message.java)
:::


### [Message](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/os/Message.java) 覆用機制 obtain：在池中獲取 Message

* 當然，Android 框架的設計者在決定使用「驅動模型」時就有考慮到物件過多的問題，所以它也有設計置一個 **Flyweight 享元模式** 的設計，讓事件的物件可以循環被系統覆用
    
* Android 的「**事件**」就是由 Message 類來表達，它是一個值物件類型，**對應我們前面所說的，它具有 ++內部狀態++ 的特性**；在這個小節中，我們主要關注「**從池中取值**」的行為
 
    Message 的覆用（從池中取值）入口函數為 `obtain`，接下來我們就從這裡開始分析

    ```java=
    // Message.java

    public static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;

    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }
    ```
    
    從以上程式，我們來抓取 Message 設計時的幾個重點
    
    1. **同步鎖 `sPoolSync` 物件**：Message 類中使用一個 **靜態的 `Object` 物件** 作為同步鎖的 Key，**它表達的是對於 Pool 的執行序安全操作**

        ```java=
        // Message.java

        public static final Object sPoolSync = new Object();
        ```
    
    2. **物件池 `sPool` 物件**：在上面的享元模式範例中，我們使用 Map 作為享元模式的池（用來保存已經使用過得物件），而 Android Message 的設計則更為特別
        
        我們可以看到 `sPool` 物件的類型是 `Message`，並且在操作時可以透過 `Message`#`next` 成員切換靜態的 `sPool` 物件，從這裡我們可以看出它是使用了 [**責任鏈 Chain 的設計**](https://devtechascendancy.com/object-oriented_design_chain_framework/) 來表達池，將一系列的 Message 串接起來作為「池」
        
        ```java=
        // Message.java

        public static final Object sPoolSync = new Object();
        private static Message sPool;
        private static int sPoolSize = 0;

        public static Message obtain() {
            synchronized (sPoolSync) {
                if (sPool != null) {
                    // 將原先的指向暫存
                    Message m = sPool;
                    // 並將 sPool 賦予到原先 Message 的「前方」，也就是新 Message 會作為 Link Header
                    sPool = m.next;        
                    m.next = null;
                    m.flags = 0; // clear in-use flag
                    sPoolSize--;
                    return m;
                }
            }
            return new Message();
        }
        ```
        ```mermaid
        graph LR
        
        subgraph chain as pool
        m1(新 Message)
        m2(原先 Message)
        m3(Message)
        end
        
        m1 --> |next| m2 --> |next| m3
        
        subgraph Message object
        sPool -.-> |當前指向| m1
        end
        ```
        
        :::info
        * `sPool` 靜態成員是在哪裡被賦予值的呢？
            
            這個我們等等會看到，它是在 `recycleUnchecked` 方法中被賦予值的
        :::


### [Message](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/os/Message.java) 覆用機制 recycle：把 Message 放入池中 

* 在這個小節中，我們主要關注「**把物件放入池中**」的行為，目的是為了要保存值物件（`Message` 類）的實例，來避免 JVM 重複創建物件

* `Message` 類，把物件放入池中的方法是 `recycle`：

    使用該函數時有個條件，就是當 Messsage 物件已經呼叫過 `recycle` 代表它已經被放入池中，處於一個可使用的狀態（`FLAG_IN_USE`），不可以重複呼叫 `recycle` 否則會拋出 `IllegalStateException` 異常
    ```java=
    // Message.java

    public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

    @UnsupportedAppUsage
    void recycleUnchecked() {
        flags = FLAG_IN_USE;        // 設定該物件是可用狀態
        
        // 清除物件內的共享資訊
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }
    ```
    
    我們同樣還看一下 `recycleUnchecked` 函數的重點：
    
    1. **同步鎖 `sPoolSync` 物件**：與上個小節介紹的相同，**它表達的是對於 Pool 的執行序安全操作**
        
        但這裡我們在特別強調它（鎖）的一個特性，它是「靜態」物件，代表了所有的 Message 實體共享這把鎖，也就是說在 把「Message 放入池中」的行為是安全，但也消耗一定效能的

        ```java=
        // Message.java

        public static final Object sPoolSync = new Object();
        ```
    
    2. **池大小的控制** `MAX_POOL_SIZE`：
    
        如果我們不對池中物件的數量做管控，那相當於無限制的物件（會有記憶體遺漏），仍會導致 OOM，所以 Android Message 有設定可回收的池的數量，而這個數量上限就是 50 個

        ```java=
        // Message.java

        private static final int MAX_POOL_SIZE = 50;

        void recycleUnchecked() {
            ... 省略部份

            synchronized (sPoolSync) {
                if (sPoolSize < MAX_POOL_SIZE) {
                    ...
                }
            }
        }
        ```
    
    3. **將物件放入池（鏈表）中**：
        
        Message 類中有一個 `next` 成員，用來指向下一個 Message 物件（從這裡也可以很清楚的看到這是個鏈表設計），所以在回收 Message 物件時會有兩個行為，^1.^ 它會將當前 `sPool` 指向的物件作為下一個物件，^2.^ 再將自身（`this`）設定為當前物件 `sPool`

        ```java=
        // Message.java

        /*package*/ Message next;

        void recycleUnchecked() {
            ... 省略部份

            synchronized (sPoolSync) {
                if (sPoolSize < MAX_POOL_SIZE) {
                    // 1. 它會將當前指向的物件作為下一個物件
                    next = sPool;
                    
                    // 2. 將自身（`this`）設定為當前物件 `sPool`
                    sPool = this;
                    sPoolSize++;
                }
            }
        }
        ```
        
### 總結 Android Message 如何規劃 Flyweight 角色

* **Flyweight UML 物件角色對應 Android Message 的設計** 如下表
    
    可以看到 Android 將 Flyweight UML 物件角色幾乎完全融入到一個 Message 中，這是由於它用了「靜態成員」、「鏈表操作池」的原因！

    | 物件角色 | 說明 | Android Message 哪個類所承擔 |
    | - | - | - |
    | Flyweight(抽象) | 定義產品的可共享（內部狀態）、不可共享（外部狀態）的實現或接口 | Message |
    | ConcreteFlyweight | 具體享元（產品）角色；可共享的成員處理須與環境無間，不應該出現一個操作改變可共享成員，又同時修改不可共享成員 | Message |
    | UnsharedConcreteFlyweight | 不可共享的享元（產品）角色；**不存在外部狀態、安全需求導致無法共享的角色** | 由於使用「鏈表」操作，所以不用外部狀態 |
    | FlyweightFactory | **內部創建一個池容器，用來除存 Flyweight 對象** | Message |
 
        
## 更多的物件導向設計

物件導向的設計基礎如下，如果是初學者或是不熟悉的各位，建議可以從這些基礎開始認識，打好基底才能走個更穩（在學習的時候也需要不斷回頭看）！

:::info
* [**設計建模 2 大概念- UML 分類、使用**](https://devtechascendancy.com/introduction-to-uml-and-diagrams/)

* [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/)

* [**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/)
:::


### 創建模式 - Creation Patterns

* [**創建模式 PK**](https://devtechascendancy.com/pk-design-patterns-factory-builder-best/)

* **創建模式 - `Creation Patterns`**：
    
    創建模式用於「**物件的創建**」，它關注於如何更靈活、更有效地創建對象。這些模式可以隱藏創建對象的細節，並提供創建對象的機制，例如單例模式、工廠模式… 等等，詳細解說請點擊以下連結

    :::success
    
    * [**Singleton 單例模式 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/)

    * [**Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer**](https://devtechascendancy.com/object-oriented_design_abstract-factory/)

    * [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework/)

    * [**Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗**](https://devtechascendancy.com/object-oriented_design_builder_dialog/)
    
    * [**Clone 原型模式 | 解說實現 | Android Framework Intent**](https://devtechascendancy.com/object-oriented_design_clone_framework/)

    * [**Object Pool 設計模式 | 實現與解說 | 利用 JVM**](https://devtechascendancy.com/object-oriented_design_object-pool/)

    * [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/)
    :::


### 行為模式 - Behavioral Patterns

* [**行為模式 PK**](https://devtechascendancy.com/pk-design-patterns-cmd-strat-state-obs-chain/)

* **行為模式 - `Behavioral Patterns`**：
    
    行為模式關注物件之間的「**通信**」和「**職責分配**」。它們描述了一系列對象如何協作，以完成特定任務。這些模式專注於改進物件之間的通信，從而提高系統的靈活性。例如，策略模式、觀察者模式… 等等，詳細解說請點擊以下連結

    :::warning
    * [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/)

    * [**Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService**](https://devtechascendancy.com/object-oriented_design_interpreter_framework/)

    * [**Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞**](https://devtechascendancy.com/object-oriented_design_chain_framework/)

    * [**State 狀態模式 | 實現解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_state/)

    * [**Specification 規格模式 | 解說實現 | Query 語句實做**](https://devtechascendancy.com/object-oriented_design_specification-query/)

    * [**Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_command_servant/)

    * [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/)

    * [**Visitor 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_visitor_dispatch/)

    * [**Template 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_template/)

    * [**Mediator 模式設計 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_mediator/)

    * [**Composite 組合模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_composite/)
    :::


### 結構模式 - Structural Patterns

* [**結構模式 PK**](https://devtechascendancy.com/pk-design-patterns-proxy-decorate-adapter/)

* **結構模式 - `Structural Patterns`**：
    
    結構模式專注於「物件之間的組成」，以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時，不會破壞其整體結構。例如，外觀模式、代理模式… 等等，詳細解說請點擊以下連結

    :::danger
    * [**Bridge 橋接模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_bridge/)

    * [**Decorate 裝飾模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_decorate/)

    * [**Proxy 代理模式 | 解說實現 | 分析動態代理**](https://devtechascendancy.com/object-oriented_design_proxy_dynamic-proxy/)

    * [**Iterator 迭代設計 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_iterator/)

    * [**Facade 外觀、門面模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_facade/)

    * [**Adapter 設計模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_adapter/)
    :::


## Appendix & FAQ
:::info

:::

###### tags: `Java 設計模式` `基礎進階`