设计模式实践:享元模式之围棋棋子高效复用案例解析
在处理大量细粒度对象时,频繁创建和销毁对象会导致内存占用过高、性能下降。享元模式通过“对象共享”的核心思想,有效减少对象创建数量,大幅提升系统资源利用率。本文将通过围棋软件的经典案例,详细拆解享元模式的实现逻辑,同时结合单例模式和简单工厂模式,展现设计模式的组合应用。
一、实验背景与需求
本次实践的核心需求是设计围棋软件的棋子系统,关键要求如下:
-
围棋仅有黑白两种棋子,棋盘上可能存在数百个棋子,但同种颜色的棋子本质属性完全一致
-
系统中仅允许存在一个黑棋对象和一个白棋对象,所有棋盘上的“黑子”“白子”均复用这两个对象
-
棋子的位置(坐标)为动态变化的外部状态,需与棋子的内部状态(颜色)分离
-
用单例模式实现享元工厂,确保工厂唯一;用简单工厂模式管理棋子对象的创建与复用
二、享元模式核心结构
享元模式的关键在于区分“内部状态”(可共享、不变的属性)和“外部状态”(不可共享、动态变化的属性),通过工厂类管理共享对象。本次案例的结构设计如下:
1. 核心组件划分
| 组件类型 | 具体实现 | 职责描述 |
|---|---|---|
| 抽象享元类 | IgoChessman | 定义棋子的核心接口(获取颜色、设置位置),封装内部状态的操作 |
| 具体享元类 | BlackIgoChessman、WhiteIgoChessman | 实现抽象享元接口,存储棋子的内部状态(黑色/白色) |
| 外部状态类 | Coordinates | 存储棋子的位置信息(x、y坐标),为不可共享的动态数据 |
| 享元工厂类 | IgoChessmanFactory | 单例模式+简单工厂模式,管理享元对象的创建、复用和销毁 |
2. 类图结构
┌─────────────────┐
│ Coordinates │ ← 外部状态类(位置信息,不可共享)
├─────────────────┤
│ - x: int │
│ - y: int │
├─────────────────┤
│ + Coordinates(x, y) │
│ + getX(): int │
│ + getY(): int │
└─────────────────┘
┌─────────────────┐
│ IgoChessman │ ← 抽象享元类(定义核心接口)
├─────────────────┤
│ + getColor(): string │ ← 获取内部状态(颜色)
│ + locate(coord: Coordinates): void │ ← 结合外部状态(位置)
└─────────────────┘
▲
│
┌───────────────┬───────────────┐
│BlackIgoChessman│WhiteIgoChessman│ ← 具体享元类(存储内部状态)
├───────────────┤───────────────┤
│ + getColor(): string │ + getColor(): string │
└───────────────┘───────────────┘
┌─────────────────┐
│IgoChessmanFactory│ ← 享元工厂类(单例+简单工厂)
├─────────────────┤
│ - instance: IgoChessmanFactory │ ← 单例实例
│ - hashtable: Map<string, IgoChessman> │ ← 缓存享元对象
├─────────────────┤
│ - IgoChessmanFactory() │ ← 私有构造函数(初始化享元)
│ + getInstance(): IgoChessmanFactory │ ← 获取单例
│ + getIgoChessman(color: string): IgoChessman │ ← 复用享元
│ + releaseInstance(): void │ ← 释放单例(可选)
└─────────────────┘
三、完整实现代码(C++)
1. 外部状态类:Coordinates.h
#include <iostream>
#include <string>
// 坐标类:存储棋子位置(外部状态,不可共享)
class Coordinates {
private:
int x; // x轴坐标
int y; // y轴坐标
public:
// 构造函数:初始化坐标
Coordinates(int x, int y) : x(x), y(y) {}
// 获取x坐标
int getX() const { return x; }
// 设置x坐标
void setX(int x) { this->x = x; }
// 获取y坐标
int getY() const { return y; }
// 设置y坐标
void setY(int y) { this->y = y; }
};
2. 抽象享元类与具体享元类
// 抽象享元类:围棋棋子
class IgoChessman {
public:
virtual ~IgoChessman() = default; // 虚析构函数,确保子类正确析构
virtual std::string getColor() const = 0; // 获取棋子颜色(内部状态)
// 结合外部状态(坐标)显示棋子信息
void locate(const Coordinates& coord) {
std::cout << "棋子颜色:" << getColor()
<< ", 棋子位置:" << coord.getX()
<< "," << coord.getY() << std::endl;
}
};
// 具体享元类:黑色棋子
class BlackIgoChessman : public IgoChessman {
public:
std::string getColor() const override {
return "黑色";
}
};
// 具体享元类:白色棋子
class WhiteIgoChessman : public IgoChessman {
public:
std::string getColor() const override {
return "白色";
}
};
3. 享元工厂类(单例+简单工厂)
#include <unordered_map>
#include <memory>
// 享元工厂类:管理棋子对象的创建与复用(单例模式)
class IgoChessmanFactory {
private:
static IgoChessmanFactory* instance; // 单例实例
// 缓存享元对象:key为颜色标识,value为棋子对象
std::unordered_map<std::string, std::shared_ptr<IgoChessman>> hashtable;
// 私有构造函数:初始化时创建黑白棋子各一个(仅创建一次)
IgoChessmanFactory() {
hashtable["b"] = std::make_shared<BlackIgoChessman>();
hashtable["w"] = std::make_shared<WhiteIgoChessman>();
}
public:
// 禁用拷贝构造和赋值运算符,确保单例唯一性
IgoChessmanFactory(const IgoChessmanFactory&) = delete;
IgoChessmanFactory& operator=(const IgoChessmanFactory&) = delete;
// 获取单例实例(懒汉式)
static IgoChessmanFactory* getInstance() {
if (instance == nullptr) {
instance = new IgoChessmanFactory();
}
return instance;
}
// 获取棋子对象:复用已创建的享元,不重复创建
std::shared_ptr<IgoChessman> getIgoChessman(const std::string& color) {
auto it = hashtable.find(color);
if (it != hashtable.end()) {
return it->second; // 复用已有对象
}
return nullptr; // 无效颜色返回空
}
// 释放单例实例(可选,避免内存泄漏)
static void releaseInstance() {
if (instance != nullptr) {
delete instance;
instance = nullptr;
}
}
};
// 静态成员初始化:单例实例初始为nullptr
IgoChessmanFactory* IgoChessmanFactory::instance = nullptr;
4. 客户端测试代码
#include <iostream>
#include "Coordinates.h"
#include "IgoChessman.h"
#include "IgoChessmanFactory.h"
int main() {
// 1. 获取享元工厂单例实例
IgoChessmanFactory* factory = IgoChessmanFactory::getInstance();
// 2. 多次获取黑棋和白棋对象(复用同一实例)
std::shared_ptr<IgoChessman> black1 = factory->getIgoChessman("b");
std::shared_ptr<IgoChessman> black2 = factory->getIgoChessman("b");
std::shared_ptr<IgoChessman> black3 = factory->getIgoChessman("b");
std::shared_ptr<IgoChessman> white1 = factory->getIgoChessman("w");
std::shared_ptr<IgoChessman> white2 = factory->getIgoChessman("w");
// 3. 验证享元对象是否复用(地址是否相同)
std::cout << "判断两颗黑棋是否相同: "
<< (black1 == black2 ? "true" : "false")
<< std::endl;
std::cout << "判断两颗白棋是否相同: "
<< (white1 == white2 ? "true" : "false")
<< std::endl;
// 4. 结合外部状态(坐标)显示棋子
std::cout << "\n=== 棋子布局展示 ===" << std::endl;
black1->locate(Coordinates(1, 2));
black2->locate(Coordinates(3, 4));
black3->locate(Coordinates(1, 3));
white1->locate(Coordinates(2, 4));
white2->locate(Coordinates(2, 5));
// 5. 释放工厂实例
IgoChessmanFactory::releaseInstance();
return 0;
}
四、运行结果
判断两颗黑棋是否相同: true
判断两颗白棋是否相同: true
=== 棋子布局展示 ===
棋子颜色:黑色, 棋子位置:1,2
棋子颜色:黑色, 棋子位置:3,4
棋子颜色:黑色, 棋子位置:1,3
棋子颜色:白色, 棋子位置:2,4
棋子颜色:白色, 棋子位置:2,5
从运行结果可以看出:
-
多次获取同种颜色的棋子,返回的是同一个对象(享元复用成功)
-
通过
locate方法将外部状态(坐标)与内部状态(颜色)结合,实现不同位置的棋子显示 -
仅创建2个棋子对象,即可支持任意数量的棋盘布局,大幅节省内存
五、享元模式核心优势与设计要点
1. 核心优势
-
内存优化:大幅减少细粒度对象的创建数量,降低内存占用(如围棋仅需2个对象,而非数百个)
-
性能提升:避免频繁创建/销毁对象的开销,提高系统响应速度
-
扩展性强:新增棋子类型(如彩色棋子)时,仅需新增具体享元类,工厂类稍作修改,符合开闭原则
-
分离状态:清晰区分内部状态(可共享)和外部状态(不可共享),职责单一
2. 设计要点
-
内部状态 vs 外部状态:内部状态需满足“不变、可共享”(如棋子颜色),外部状态需“动态、不可共享”(如位置)
-
工厂管理:通过工厂类统一管理享元对象的创建、缓存和复用,避免客户端直接创建
-
单例模式结合:享元工厂通常设计为单例,确保全局只有一个对象缓存池
-
线程安全:懒汉式单例需注意线程安全(可通过加锁优化)
六、适用场景总结
享元模式特别适合以下场景:
-
系统中存在大量相同或相似的细粒度对象(如棋子、文字、图标等)
-
对象的大部分状态可提取为外部状态,且内部状态相对稳定
-
客户端无需关注对象的创建细节,仅需使用对象的功能
-
系统内存占用过高,需优化资源利用率
通过本次围棋棋子的实践案例,深刻体会到享元模式在资源优化中的强大作用。它通过“共享”思想,将看似需要大量对象的场景,简化为少量对象的复用,同时结合单例模式和简单工厂模式,让系统设计更简洁、高效。在实际开发中,当遇到文本编辑器的字符渲染、游戏中的粒子效果、电商系统的商品规格等场景时,享元模式都是一个理想的解决方案。