1.动机

面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时,将导致运行代价过高,带来性能下降等问题。

  • 享元模式正是为解决这一类问题而诞生的。享元模式通过共享技术实现相同或相似对象的重用。
  • 在享元模式中可以共享的相同内容称为内部状态(IntrinsicState),而那些需要外部环境来设置的不能共享的内容称为外部状态(Extrinsic State),由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。
  • 在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)用于存储具有相同内部状态的享元对象
  • 在享元模式中共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。

2.定义

  • 享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

3.结构

clipboard.png

  • Flyweight:声明一个接口,通过这个接口flyweight可以接受并作用于外部状态;
  • ConcreteFlyweight:实现Flyweight接口,并为定义了一些内部状态;ConcreteFlyweight对象必须是可共享的;同时,它所存储的状态必须是内部的;即,它必须独立于ConcreteFlyweight对象的场景;
  • UnsharedConcreteFlyweight:并非所有的Flyweight子类都需要被共享。Flyweight接口使共享成为可能,但它并不强制共享。
  • FlyweightFactory:创建并管理flyweight对象。它需要确保合理地共享flyweight;当用户请求一个flyweight时,FlyweightFactory对象提供一个已创建的实例,如果请求的实例不存在的情况下,就新创建一个实例;
  • Client:维持一个对flyweight的引用;同时,它需要计算或存储flyweight的外部状态。

4.实现要点

在享元模式中,有两个非常重要的概念:内部状态和外部状态。

  • 内部状态存储于flyweight中,它包含了独立于flyweight场景的信息,这些信息使得flyweight可以被共享。而外部状态取决于flyweight场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给flyweight
  • flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部对象则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该状态传递给它。同时,用户不应该直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享;由于共享一个实例,所以在创建这个实例时,就可以考虑使用单例模式来进行实现。
  • 享元模式的工厂类维护了一个实例列表,这个列表中保存了所有的共享实例;当用户从享元模式的工厂类请求共享对象时,首先查询这个实例表,如果不存在对应实例,则创建一个;如果存在,则直接返回对应的实例。

5.代码分析

#include <iostream>
#include <vector>
using namespace std;

//外部状态,棋子的颜色
struct POINT
{
    POINT(){};
    POINT(int a, int b) :x(a), y(b){};

    int x;
    int y;
};

//内部状态,棋子的颜色,black==0,white==1
typedef enum PieceColorTag
{
    BLACK,
    WHITE
}PIECECOLOR;
 
//彻底将内部状态和外部状态分离开
class CPiece
{
public:
    CPiece(PIECECOLOR color) :m_color(color){};
    PIECECOLOR GetColor() { return m_color; }

protected:
    // 内部状态(共享)
    PIECECOLOR m_color;
};


//将共享对象进行包装,由于没有数据成员,则不会造成任何额外的开销
class CGomoku : public CPiece
{
public:
    CGomoku(PIECECOLOR color) : CPiece(color){}
};

//维护一个对象池,若存在直接取,不存在再创建
class CPieceFactory
{
public:
    //根据颜色,返回一个棋子
    CPiece *GetPiece(PIECECOLOR color)
    {
        CPiece *pPiece = nullptr;
        //如果对象池为空
        if (m_vecPiece.empty())
        {
            //创建一个该颜色的棋子,并加入对象池
            pPiece = new CGomoku(color);
            m_vecPiece.push_back(pPiece);
        }
        else
        {
            //对象池不为空
            for (auto it = m_vecPiece.begin(); it != m_vecPiece.end(); ++it)
            {
                //在对象池有对应颜色的棋子,直接返回
                if ((*it)->GetColor() == color)
                {
                    pPiece = *it;
                    break;
                }
            }
            //在对象池没有有对应颜色的棋子
            if (pPiece == nullptr)
            {
                //创建一个该颜色的棋子,并加入对象池
                pPiece = new CGomoku(color);
                m_vecPiece.push_back(pPiece);
            }
        }
        return pPiece;
    }

    //释放对象池
    ~CPieceFactory()
    {
        for (auto it = m_vecPiece.begin(); it != m_vecPiece.end(); ++it)
        {
            if (*it != nullptr)
            {
                delete *it;
                *it = nullptr;
            }
        }
    }

private:
    vector<CPiece* > m_vecPiece;
};

//棋盘,定义设置外部状态的接口
class CChessboard
{
public:
    CChessboard(CPieceFactory* factory) :pPieceFactory(factory){};
    
    void Draw(PIECECOLOR color,POINT point)
    {
        CPiece *piece = pPieceFactory->GetPiece(color);
        //white==1
        if (piece->GetColor())
        {
            cout << "Draw a White" << " at (" << point.x << "," << point.y << ")" << endl;
        }
        else//black==0
        {
            cout << "Draw a Black" << " at (" << point.x << "," << point.y << ")" << endl;
        }
        m_mapPieces.push_back(pair<POINT, CPiece* >(point, piece));
    }

    void ShowAllPieces()
    {
        for (auto it = m_mapPieces.begin(); it != m_mapPieces.end(); ++it)
        {
            //white==1
            if (it->second->GetColor())
            {
                cout << "(" << it->first.x << "," << it->first.y << ") has a White chese." << endl;
            }
            else//black==0
            {
                cout << "(" << it->first.x << "," << it->first.y << ") has a Black chese." << endl;
            }
        }
    }

private:
    vector<pair<POINT, CPiece *>> m_mapPieces;
    CPieceFactory* pPieceFactory;
};

int main()
{
    CPieceFactory *pPieceFactory = new CPieceFactory();
    CChessboard *pCheseboard = new CChessboard(pPieceFactory);

    // The player1 get a white piece from the pieces bowl
    pCheseboard->Draw(WHITE,POINT(2,3));

    // The player2 get a black piece from the pieces bowl
    pCheseboard->Draw(BLACK, POINT(4, 5));

    // The player1 get a white piece from the pieces bowl
    pCheseboard->Draw(WHITE, POINT(2, 4));

    // The player2 get a black piece from the pieces bowl
    pCheseboard->Draw(BLACK, POINT(3, 5));

    //Show all cheses
    cout << "Show all cheses" << endl;
    pCheseboard->ShowAllPieces();

    if (pCheseboard != nullptr)
    {
        delete pCheseboard;
        pCheseboard = nullptr;
    }
    if (pPieceFactory != nullptr)
    {
        delete pPieceFactory;
        pPieceFactory = nullptr;
    }

    system("pause");
    return 0;
}
  • 内部状态包括棋子的颜色,外部状态包括棋子在棋盘上的位置。最终,我们省去了多个实例对象存储棋子颜色的空间,从而达到了空间的节约。
  • 建立了一个CCheseboard用于表示棋盘,棋盘类中保存了放置在不同位置的黑色棋子和白色棋子;这就相当于在外部保存了共享对象的外部状态。

6.模式分析

  • 享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。
  • 享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
  • 享元模式以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。
  • 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
  • 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。

7.优点

  • 享元模式可以避免大量非常相似对象的开销。在程序设计时,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例数据除了几个参数外基本都是相同的,使用享元模式就可以大幅度地减少对象的数量。
  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

8.缺点

  • 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
  • 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。

9.适用场景

  • 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
  • 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式。

燃烧你的梦
238 声望17 粉丝