享元模式

<image>

在電腦編程中，享元模式（flyweight pattern），是一種軟體設計模式。它使用物件用來儘可能減少記憶體使用量；於相似物件中分享儘可能多的資訊。當大量物件近乎重複方式存在，因而使用大量記憶體時，此法適用。通常物件中的部分狀態 (電腦科學)能夠共享。常見做法是把它們放在資料結構外部，當需要使用時再將它們傳遞給享元。

## 概述

<image>

典型的享元模式的例子，是文書處理器中用來表示字符（character，亦譯為「字元」）的數據結構。在樸素的觀念上，文檔中的每個字符都有一個字形（glyph）物件，它含有字體（typeface或font family）、字型（font或font face）的重量與大小（比如點數）和其它格式資訊，但這會使得為每個字符都耗用成百上千個位元組。取而代之的是，每個字符參照到一個共享字形物件，此物件會被其它有共同特質的字符所分享；只有每個字符（文件中或頁面中）的位置才需要另外儲存。

享元對象可以做到：

- 存儲內在與外在性質（intrinsic）狀態 (電腦科學)，它是不變化的、上下文無關的並且可共享的，例如在一個給定的字符集中的字符`A`的代碼。
- 提供一個接口用來傳入內在與外在性質（extrinsic）狀態，它是變化的、上下文依賴的並且不能被共享的，例如在一個文本文檔中字符`A`的位置。

客戶可以重複使用`Flyweight`對象並在需要時傳入外在狀態，減小免在物理上創建的對象的數目。

## 結構

<image>

上面左側的UML類圖展示了：

- `Client`類，它使用享元模式。
- `FlyweightFactory`類，它是創建和共享`Flyweight`對象的工廠。
- `Flyweight`接口，接入外在狀態並在其上進行運算。
- `Flyweight1`類，它實現`Flyweight`並存儲內在狀態。

上面右側的序列圖展示了運行時交互：

- `Client`對象在`FlyweightFactory`之上調用其`getFlyweight(key)`，`FlyweightFactory`返回一個`Flyweight1`對象。
- `Client`在返回的`Flyweight1`對象之上調用其`operation(extrinsicState)`。
- `Client`再次在`FlyweightFactory`之上調用其`getFlyweight(key)`，`FlyweightFactory`返回已經存在了的`Flyweight1`對象。

## 示例

### C++

C++標準模板庫提供了允許從鍵映射到唯一性對象的多種容器。使用了這種容器，通過移除創建臨時對象的需要，能有助於減少內存使用。

```c++
import std;

template <typename K, typename V>
using TreeMap = std::map<K, V>;
template <typename K, typename V>
using HashMap = std::unordered_map<K, V>;

// 租户类Tenant的实例是享元
class Tenant {
private:
    const std::string name;
public:
    Tenant(std::string_view name): 
        name{name} {}

    [[nodiscard]]
    std::string getName() const noexcept {
        return name;
    }
};

// 注册类Registry充当Tenant享元对象的工厂
// 并在租户容器中将租户名字映射到唯一性的租户对象
class Registry {
private:
    HashMap<std::string, Tenant> tenants;
public:
    Registry() = default;

    [[nodiscard]]
    Tenant& findByName(std::string_view name) {
        if (!tenants.contains(name)) {
            tenants[name] = Tenant{name};
        }
        return tenants[name];
    }
};

// 公寓类Apartment在房客容器中将房间编号映射到唯一性的租户对象
class Apartment {
private:
    TreeMap<int, Tenant*> occupants;
    Registry registry;
public:
    Apartment() = default;

    void addOccupant(std::string_view name, int room) {
        occupants[room] = &registry.findByName(name);
    }

    void printTenants() {
        for (const std::pair<int, Tenant>& i : occupants) {
            const int& room = i.first;
            const Tenant& tenant = i.second;
            std::println("{} occupies room {}", tenant.name(), room);
        }
    }
};

int main(int argc, char* argv[]) {
    Apartment apartment;
    apartment.addOccupant("David", 1);
    apartment.addOccupant("Sarah", 3);
    apartment.addOccupant("George", 2);
    apartment.addOccupant("Sarah", 12);
    apartment.addOccupant("Michael", 10);
    apartment.printTenants();

    return 0;
}
```

### Java

以下Java程式用來解釋上述的文字。這個例子用來解釋享元模式利用只加載執行任務時所必需的最少資料，因而減少記憶體使用量。

```java
public enum FontEffect {
    BOLD, ITALIC, SUPERSCRIPT, SUBSCRIPT, STRIKETHROUGH
}

public final class FontData {
    /**
     * FLY_WEIGHT_DATA是WeakHashMap，它是作为享元的FontData对象的缓存。
     * 键值对的键是FontData，如果这个键对象不再被强引用，则丢弃此项的键值对。
     * 键值对的值是用WeakReference包装的FontData，它不会增加键对象的强引用数目。
     */
    private static final WeakHashMap<FontData, WeakReference<FontData>> FLY_WEIGHT_DATA =
        new WeakHashMap<FontData, WeakReference<FontData>>();
    private final int pointSize;
    private final String fontFace;
    private final Color color;
    private final Set<FontEffect> effects;

    private FontData(int pointSize, String fontFace, Color color, EnumSet<FontEffect> effects) {
        this.pointSize = pointSize;
        this.fontFace = fontFace;
        this.color = color;
        this.effects = Collections.unmodifiableSet(effects);
    }
    
    public static FontData create(int pointSize, String fontFace, Color color,
            FontEffect... effects) {
        EnumSet<FontEffect> effectsSet = EnumSet.noneOf(FontEffect.class);
        for (FontEffect fontEffect : effects) {
            effectsSet.add(fontEffect);
        }
        // 创建FontData对象，如果它与以前创建的对象有关相同的参数值。
        // 那么这个新对象会被舍弃，这种浪费的创建是减少总体内存消耗的代价。
        FontData data = new FontData(pointSize, fontFace, color, effectsSet);

        // 在缓存中检索同新建FontData实例有相同参数值的以前创建的FontData实例。
        // 如果它（仍然）存在，则对这项键值对的值进行解引用从而得到这个以前创建的FontData实例。
        WeakReference<FontData> ref = FLY_WEIGHT_DATA.get(data);
        FontData result = (ref != null) ? ref.get() : null;
        
        // 如果检索结果没有匹配的实例存在，在缓存中存储新的FontData实例。
        if (result == null) {
            FLY_WEIGHT_DATA.put(data, new WeakReference<FontData> (data));
            result = data;
        }
        // 返回具有给定参数值的单一的不可变的FontData实例。
        // FontData对象是不可变的，所以只有getter方法，没有setter方法。
        return result;
    }

    // 覆写进行键值对的键对象检索用到的对象相等比较运算方法。
    // 将其实现为结果真值是两个对象所包含的参数值的逐项比较的合取。
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof FontData) {
            if (obj == this) {
                return true;
            }
            FontData other = (FontData) obj;
            return other.pointSize == pointSize && other.fontFace.equals(fontFace)
                && other.color.equals(color) && other.effects.equals(effects);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (pointSize * 37 + effects.hashCode() * 13) * fontFace.hashCode();
    }
}
```

### Python

下面是Python的例子:

```python
from abc import ABC, abstractmethod
from weakref import WeakValueDictionary
import functools

class invoke_from_class():
    def __init__(self, fn):
        self.fn = fn
        functools.update_wrapper(self, fn)
    def __get__(self, obj, objtype=None):
        self.from_class = True if obj is None else False
        return self
    def __call__(self, *args, **kwargs):
        if self.from_class is True:
            return self.fn(*args, **kwargs)

class Multiton():
    shareable_dict = WeakValueDictionary()
    def __new__(cls, **kwargs):
        key = tuple(f"{v}" for k, v in kwargs.items())
        if key in cls.shareable_dict:
            obj = cls.shareable_dict[key]
        else:
            obj = super().__new__(cls)
            cls.shareable_dict[key] = obj 
            cls.__init__(obj, **kwargs)
        return obj
    @invoke_from_class
    def __init__(self, *args, **kwargs):
        for key, value in kwargs.items():
            self.__dict__[key] = value

all_font_style = {'Regular', 'Bold', 'Italic',
    'Superscript', 'Subscript', 'Strikethrough'}

class GlyphFactory(Multiton):
    def __new__(cls, glyph, font_family,
            font_size, font_style={'Regular'}):
        assert len(glyph) <= 1
        assert font_style <= all_font_style
        obj = super().__new__(cls, 
            glyph=glyph, font_family=font_family, 
            font_size=font_size, font_style=font_style)
        return obj

class Glyph(ABC):
    def __len__(self):
        return len(self.glyph)
    def __getitem__(self, index):
        return self.glyph[index]
    def __setitem__(self, index, value):
        self.glyph[index] = value
    def __delitem__(self, index):
        del self.glyph[index]
    @abstractmethod 
    def draw(self, text_color='Automatic'): pass

class Character(Glyph, GlyphFactory):
    def __repr__(self):
        return ', '.join(f"{k}={v!r}" 
            for k, v in self.__dict__.items())
    def draw(self, text_color='Automatic'): pass

class Paragraph(Glyph):
    def __init__(self, string, font_family, font_size,
            font_style={'Regular'}, *args, **kwargs):
        self.font_family = font_family
        self.font_size = font_size
        self.font_style = font_style
        self.glyph = [Character(char,
            font_family, font_size, font_style)
            for char in string]
    def draw(self, *args, **kwargs):
        for c in self.glyph:
            c.draw(*args, **kwargs)
```

這裡在「蠅量」模式中將實現對象共享的多例模式明確表達了出來。下面是其執行：

```pycon
>>> char1 = Character('A', 'Noto Serif CJK', 24)
>>> char1
glyph='A', font_family='Noto Serif CJK', font_size=24, font_style={'Regular'}
>>> char2 = Character('A', 'Noto Serif CJK', 14, {'Bold'})
>>> char2
glyph='A', font_family='Noto Serif CJK', font_size=14, font_style={'Bold'}
>>> char3 = Character('A', 'Noto Serif CJK', 24)
>>> assert char3 is char1
>>> para = Paragraph('ABC DEF', 'Noto Serif CJK', 14)
>>> para[0]
glyph='A', font_family='Noto Serif CJK', font_size=14, font_style={'Regular'}
```

## 外部連結

- UML 和 LePUS3 （一種形式塑模語言）中的享元模式
- " 使你的程式飛起來 - 實作享元以改善效能 "
- " 享元模式"
- 結構模式 - 享元模式