
实战设计模式之享元模式
在开发过程中,我们经常会遇到创建大量具有相似属性的对象的情况。比如:在一个图形编辑器中,可能有成千上万的小图标或文字字符;在一个游戏中,可能有大量的敌人、子弹等重复元素。如果每个这样的对象都独立存储其所有信息,将会占用大量的内存空间,并可能导致性能问题。为了优化这种情况,我们可以考虑只创建一份包含共同属性的数据副本,然后让不同的对象引用这份数据。同时,各自维护自己的独特属性。这就是享元模式的核心思
概述
在开发过程中,我们经常会遇到创建大量具有相似属性的对象的情况。比如:在一个图形编辑器中,可能有成千上万的小图标或文字字符;在一个游戏中,可能有大量的敌人、子弹等重复元素。如果每个这样的对象都独立存储其所有信息,将会占用大量的内存空间,并可能导致性能问题。
为了优化这种情况,我们可以考虑只创建一份包含共同属性的数据副本,然后让不同的对象引用这份数据。同时,各自维护自己的独特属性。这就是享元模式的核心思想:共享不可变的数据,以节省内存。
图书馆的书籍管理是现实生活中运用享元模式的一个典型例子:在图书馆中,虽然每本书可能有多个副本,但它们的内容是相同的;读者借阅时,实际上是在借阅同一个内容的不同实体;我们可以将书籍的内容视为内部状态,而将每个副本的具体位置、是否被借出等信息视为外部状态。图书馆只需要存储一份书籍内容(比如:书名、作者、ISBN等),然后为每个副本维护一个指向该内容的引用,并记录额外的信息(比如:副本编号、所在书架位置)即可。
基本原理
享元模式特别适用于当程序中有大量相似对象时,这些对象消耗了过多的内存资源。享元模式通过将对象的内部状态和外部状态分离,使得多个对象可以共享相同的内部状态,从而减少了内存占用。内部状态指的是对象中不会改变的数据,这些数据可以在多个对象之间共享。外部状态指的是对象中会随着上下文变化的数据,每个对象都有自己独立的外部状态。
通常情况下,会有一个工厂类来负责创建和管理享元对象。这个工厂会检查是否已经存在符合要求的享元,如果有的话,就返回现有的实例,否则就创建新的。享元模式主要由以下四个核心组件构成。
1、享元。定义一个接口或抽象类,规定所有享元对象必须实现的方法。这些方法应该能够接收外部状态作为参数,并根据内部状态和外部状态的组合执行操作。
2、具体享元。实现享元接口的具体类,它们实现了接口中定义的方法。
3、享元工厂。享元工厂内部维护了一个哈希表或其他集合,用来存储已创建的享元。当收到创建请求时,工厂会先查找是否有匹配的享元存在。如果有,则直接返回;如果没有,则新创建一个并将它加入到集合中。
4、客户端。客户端与享元工厂交互以获取享元对象,并提供必要的外部状态信息给享元对象,以便正确地使用它们。
基于上面的核心组件,享元模式的实现主要有以下四个步骤。
1、定义享元接口。享元接口中的方法应该能够接收外部状态作为参数,并根据内部状态和外部状态的组合执行操作。
2、创建具体享元类。每个具体享元类都应该有一个构造函数,用来初始化内部状态。
3、设置享元工厂。建立一个工厂类,用于管理和分发享元对象。
4、应用享元模式。适当地使用享元模式代替直接创建对象的方式,确保每次使用时都能够传递正确的外部状态给享元对象。
实战解析
在下面的实战代码中,我们使用享元模式模拟了图书馆书籍管理的实现。
首先,我们定义了书籍的内部状态TBookIntrinsicState,包括书名和作者。这些信息对于每本书都是固定的,不会随着副本的不同而变化。我们还定义了书籍的外部状态TBookExtrinsicState,包括借阅者和书架位置。这类信息会随具体副本的变化而变化,因此不属于享元对象的一部分。
然后,我们定义了享元接口CBookFlyweight。所有具体的享元类必须实现Operation方法,用于处理与外部状态相关的操作。接下来,我们实现了具体享元接口CConcreteBookFlyweight。它持有一个指向TBookIntrinsicState的指针,并在Operation方法中结合外部状态输出书籍的信息。
作为享元工厂,CBookFactory类负责创建和管理享元对象。它包含一个静态成员变量s_mapFlyweights作为享元池,用来存储已创建的享元对象。GetFlyweight方法确保相同的书籍只创建一次对应的享元对象,Cleanup方法用于清理所有的享元对象。
CLibrary类则模拟了一个图书馆,通过AddBook方法添加书籍到收藏中,并记录每个副本的具体位置。BorrowBook方法允许用户借阅书籍,并打印出借阅信息。如果尝试借阅的书籍已经全部借出,则会通知用户当前没有可用的副本。
最后,在main函数中,我们创建了一个图书馆对象,并展示了添加书籍和借阅书籍的整个过程。
#include <iostream>
#include <string>
#include <map>
#include <vector>
using namespace std;
// 内部状态类,不变的部分
struct TBookIntrinsicState
{
public:
string title;
string author;
TBookIntrinsicState(const string& title, const string& author)
: title(title), author(author) {}
};
// 外部状态,可变的部分
struct TBookExtrinsicState
{
string borrower;
string shelfLocation;
TBookExtrinsicState(const string& borrower, const string& shelfLocation)
: borrower(borrower), shelfLocation(shelfLocation) {}
};
// 书籍享元接口
class CBookFlyweight
{
public:
virtual ~CBookFlyweight() = default;
virtual void Operation(const TBookExtrinsicState& extrinsicState) const = 0;
};
// 具体享元类
class CConcreteBookFlyweight : public CBookFlyweight
{
public:
CConcreteBookFlyweight(TBookIntrinsicState* pState) : m_pInState(pState) {}
~CConcreteBookFlyweight()
{
delete m_pInState;
}
void Operation(const TBookExtrinsicState& extrinsicState) const override
{
cout << "Book \"" << m_pInState->title << "\" by "
<< m_pInState->author << " at location " << extrinsicState.shelfLocation
<< " borrowed by " << extrinsicState.borrower << endl;
}
private:
TBookIntrinsicState* m_pInState;
};
// 享元工厂类
class CBookFactory
{
public:
static CBookFlyweight* GetFlyweight(const string& key)
{
if (s_mapFlyweights.find(key) == s_mapFlyweights.end())
{
// 解析键值,创建新的内在状态对象
size_t pos = key.find_last_of(",");
string title = key.substr(0, pos);
string author = key.substr(pos + 1);
s_mapFlyweights[key] = new CConcreteBookFlyweight(
new TBookIntrinsicState(title, author));
}
return s_mapFlyweights[key];
}
static void Cleanup()
{
for (auto& pair : s_mapFlyweights)
{
delete pair.second;
}
s_mapFlyweights.clear();
}
private:
static map<string, CBookFlyweight*> s_mapFlyweights;
};
map<string, CBookFlyweight*> CBookFactory::s_mapFlyweights;
// 图书馆类
class CLibrary
{
public:
// 添加书籍到收藏,指定书架位置
void AddBook(const string& title, const string& author, const string& shelfLocation)
{
string key = title + "," + author;
CBookFlyweight* book = CBookFactory::GetFlyweight(key);
m_mapBook[key].push_back(shelfLocation);
}
// 借阅书籍
void BorrowBook(const string& title, const string& author, const string& borrower)
{
string key = title + "," + author;
if (m_mapBook.find(key) != m_mapBook.end() && !m_mapBook[key].empty())
{
string shelfLocation = m_mapBook[key].back();
m_mapBook[key].pop_back();
TBookExtrinsicState extrinsicState{borrower, shelfLocation};
CBookFactory::GetFlyweight(key)->Operation(extrinsicState);
}
else
{
cout << "No copies of the book \"" << title << "\" are available" << endl;
}
}
private:
// 存储每本书的所有副本及其位置
map<string, vector<string>> m_mapBook;
};
int main()
{
CLibrary library;
// 添加书籍到图书馆收藏,指定书架位置
library.AddBook("The C++ Programming Language", "Bjarne Stroustrup", "A1-666");
library.AddBook("The C++ Programming Language", "Bjarne Stroustrup", "A2-888");
// 借阅书籍
library.BorrowBook("The C++ Programming Language", "Bjarne Stroustrup", "John");
library.BorrowBook("The C++ Programming Language", "Bjarne Stroustrup", "Mike");
// 尝试借阅已全部借出的书籍
library.BorrowBook("The C++ Programming Language", "Bjarne Stroustrup", "Tom");
CBookFactory::Cleanup();
return 0;
}
总结
享元模式通过共享不可变的数据,可以大幅降低内存使用量。当应用程序中存在大量相似对象时,这种优化尤为明显。由于减少了对象的数量,减少了创建和销毁对象的时间开销,从而提升了系统的整体性能。享元模式鼓励对资源进行复用,这不仅限于内存中的对象,还可以扩展到其他类型的资源,比如:数据库连接、文件句柄等。
但引入享元模式也意味着系统结构变得更加复杂,特别是在处理内外状态的划分时。如果对象的状态经常发生变化,那么享元模式的优势就不明显了,因为每次变化都需要更新外部状态。在这种情况下,维护外部状态可能会变得繁琐且容易出错。为了实现有效的共享,享元对象必须是不可变的,或者至少其内部状态是不可变的。这意味着,不能随意地修改享元对象的状态,否则会影响到所有引用该享元的客户端。
本专栏配套的源代码可从这里进行下载:https://download.csdn.net/download/hope_wisdom/90445403
更多推荐
所有评论(0)