概述

        在开发过程中,我们经常会遇到创建大量具有相似属性的对象的情况。比如:在一个图形编辑器中,可能有成千上万的小图标或文字字符;在一个游戏中,可能有大量的敌人、子弹等重复元素。如果每个这样的对象都独立存储其所有信息,将会占用大量的内存空间,并可能导致性能问题。

        为了优化这种情况,我们可以考虑只创建一份包含共同属性的数据副本,然后让不同的对象引用这份数据。同时,各自维护自己的独特属性。这就是享元模式的核心思想:共享不可变的数据,以节省内存。

        图书馆的书籍管理是现实生活中运用享元模式的一个典型例子:在图书馆中,虽然每本书可能有多个副本,但它们的内容是相同的;读者借阅时,实际上是在借阅同一个内容的不同实体;我们可以将书籍的内容视为内部状态,而将每个副本的具体位置、是否被借出等信息视为外部状态。图书馆只需要存储一份书籍内容(比如:书名、作者、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

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐