享元模式

<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 （一种形式塑模语言）中的享元模式
- " 使你的程式飞起来 - 实作享元以改善效能 "
- " 享元模式"
- 结构模式 - 享元模式