游戏开发工具

享元模式介绍

一、享元模式介绍

享元模式介绍

迷雾散尽,露出了古朴庄严的森林,古老的铁杉,在头顶编成绿色穹顶。 阳光在树叶间破碎成金色顶棚,从树干间远眺,远处的森林渐渐隐去。


1.jpg

这是我们游戏开发者梦想的超凡场景,这样的场景通常由一个模式支撑着,它的名字低调至极:享元模式


享元是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。


计算机世界中无穷无尽的可能,其本质都是由1和0两个“元”的组合变化而产生的。元,顾名思义,始也,有本初、根源的意思。“享元”则是共享元件的意思。


享元模式的英文flyweight是轻量级的意思,这就意味着享元模式能使程序变得更加轻量化。当系统存在大量的对象,并且这些对象又具有相同的内部状态时,我们就可以用享元模式共享相同的元件对象,以避免对象泛滥造成资源浪费。


享元模式(FlyWeight Pattern)主要用来减少创建对象的数量,以减少内存占用,达到提高性能目的,这种模式也属于结构型设计模式,享元模式尝试复用现有的同类对象,如果未找到匹配对象,则创建新对象,此模式是一种专门为提升系统性能而生的设计模式。 


二、森林之树

森林之树

用几句话就能描述一片巨大的森林,但是在实时游戏中做这件事就完全是另外一件事了,当屏幕上需要显示一整个森林时,图形程序员看到的是每秒需要送到GPU六十次的百万多边形。


我们讨论的是成千上万的树,每棵都由上千的多边形组成,就算有足够的内存描述森林,渲染的过程中,CPU到GPU的部分也太过繁忙了。

每棵树都有一系列与之相关的位:

1、定义树干,树枝和树叶形状的多边形网格

2、树皮和树叶的纹理

3、在森林中树的位置和朝向

4、大小和色彩之类的调节参数,让每棵树都看起来与众不同


如果用代码表示,那么会得到这样的东西:

class Tree
{
private:
  Mesh mesh_;
  Texture bark_;
  Texture leaves_;
  Vector position_;
  double height_;
  double thickness_;
  Color barkTint_;
  Color leafTint_;
};


这是一大堆数据,多边形网格和纹理体积非常大,描述整个森林的对象在一帧的时间就交给GPU实在是太过了,幸运的是,有一种老办法来处理它。

关键点在于,哪怕森林里有千千万万的树,它们大多数长得一模一样,它们使用了相同的网格和纹理,这意味着这些树的实例的大部分字段是一样的。


2.jpg

你要么是疯了,要么是亿万富翁,才能让美术给森林里每棵树建立独立模型。

一行树,每棵都有自己的网格、纹理、树叶,调节参数和位置朝向。

注意每一棵树的小盒子中的东西都是一样的。

我们可以通过显式地将对象切为两部分来更加明确地模拟。 第一,将树共有的数据拿出来分离到另一个类中:

class TreeModel
{
private:
  Mesh mesh_;
  Texture bark_;
  Texture leaves_;
};


游戏只需要一个这种类, 因为没有必要在内存中把相同的网格和纹理重复一千遍,游戏世界中每个树的实例只需有一个对这个共享TreeModel的引用,留在Tree中的是那些实例相关的数据:

class Tree
{
private:
  TreeModel* model_;

  Vector position_;
  double height_;
  double thickness_;
  Color barkTint_;
  Color leafTint_;
};


你可以将其想象成这样:

3.jpg


一行树,每个都有自己的参数和位置朝向,指向另一个有网格、纹理、树叶的树模型。


这有点像类型对象模式,两者都涉及将一个类中的状态委托给另外的类,来达到在不同实例间分享状态的目的,但是,这两种模式背后的意图不同。


使用类型对象,目标是通过将类型引入对象模型,减少需要定义的类,伴随而来的内容分享是额外的好处,享元模式则是纯粹的为了效率。


把所有的东西都存在主存里没什么问题,但是这对渲染也毫无帮助,在森林到屏幕上之前,它得先到GPU,我们需要用显卡可以识别的方式共享数据。


三、一千个实例

一千个实例

为了减少需要推送到GPU的数据量,我们想把共享的数据——TreeModel——只发送一次,然后,我们分别发送每个树独特的数据——位置,颜色,大小。 最后,我们告诉GPU,“使用同一模型渲染每个实例”。


幸运的是,今日的图形接口和显卡正好支持这一点,这些细节很繁琐且超出了这部书的范围,但是Direct3D和OpenGL都可以做实例渲染。


在这些API中,你需要提供两部分数据流:

第一部分是一块需要渲染多次的共同数据——在例子中是树的网格和纹理

第二部分是实例的列表以及绘制第一部分时需要使用的参数。 然后调用一次渲染,绘制整个森林


这个API是由显卡直接实现的,意味着享元模式也许是唯一的有硬件支持的GoF设计模式。


好了,我们已经看了一个具体的例子,下面我介绍模式的通用部分,享元,就像它的名字暗示的那样, 当你需要共享类时使用,通常是因为你有太多这种类了。


实例渲染时,每棵树通过总线送到GPU消耗的更多是时间而非内存,但是基本要点是一样的。


这个模式通过将对象的数据分为两种来解决这个问题:

第一种数据没有特定指明是哪个对象的实例,因此可以在它们间分享

Gof称之为固有状态,但是我更喜欢将其视为“上下文无关”部分,在这里的例子中,是树的网格纹理


第二种数据的剩余部分是变化状态,那些每个实例独一无二的东西

在这个例子中,是每棵树的位置,拉伸和颜色。 就像这里的示例代码块一样,这种模式通过在每个对象出现时共享一份固有状态来节约内存。


就目前而言,这看上去像是基础的资源共享,很难被称为一种模式,部分原因是在这个例子中,我们可以为共享状态划出一个清晰的身份:TreeModel。


我发现,当共享对象没有有效定义的实体时,使用这种模式就不那么明显(使用它也就越发显得精明),在那些情况下,这看上去是一个对象被魔术般地同时分配到了多个地方,让我展示给你另外一个例子。



四、扎根之地

扎根之地

这些树长出来的地方也需要在游戏中表示,这里可能有草,泥土,丘陵,湖泊,河流,以及其它任何你可以想到的地形,我们基于区块建立地表:世界的表面被划分为由微小区块组成的巨大网格,每个区块都由一种地形覆盖。

每种地形类型都有一系列特性会影响游戏玩法:

1、决定了玩家能够多快地穿过它的移动开销

2、表明能否用船穿过的水域标识

3、用来渲染它的纹理


因为我们游戏程序员偏执于效率,我们不会在每个区块中保存这些状态,相反,一个通用的方式是为每种地形使用一个枚举。

再怎么样,我们也已经从树的例子吸取教训了。

enum Terrain
{
  TERRAIN_GRASS,
  TERRAIN_HILL,
  TERRAIN_RIVER
  // 其他地形
};


然后,世界管理巨大的网格:

class World
{
private:
  Terrain tiles_[WIDTH][HEIGHT];
};


这里我使用嵌套数组存储2D网格,在C/C++中这样是很有效率的,因为它会将所有元素打包在一起,在Java或者其他内存管理语言中,那样做会实际给你一个数组,其中每个元素都是对数组的列的引用,那就不像你想要的那样内存友好了。


反正,隐藏2D网格数据结构背后的实现细节,能使代码更好地工作,我这里这样做只是为了让其保持简单。

为了获得区块的实际有用的数据,我们做了一些这样的事情:

int World::getMovementCost(int x, int y)
{
  switch (tiles_[x][y])
  {
    case TERRAIN_GRASS: return 1;
    case TERRAIN_HILL:  return 3;
    case TERRAIN_RIVER: return 2;
      // 其他地形……
  }
}

bool World::isWater(int x, int y)
{
  switch (tiles_[x][y])
  {
    case TERRAIN_GRASS: return false;
    case TERRAIN_HILL:  return false;
    case TERRAIN_RIVER: return true;
      // 其他地形……
  }
}


你知道我的意思了,这可行,但是我觉得很丑,移动开销和水域标识是区块的数据,但在这里它们散布在代码中。 更糟的是,简单地形的数据被众多方法拆开了,如果能够将这些包裹起来就好了,毕竟,那是我们设计对象的目的。


如果我们有实际的地形类就好了,像这样:

class Terrain
{
public:
  Terrain(int movementCost,
          bool isWater,
          Texture texture)
  : movementCost_(movementCost),
    isWater_(isWater),
    texture_(texture)
  {}

  int getMovementCost() const { return movementCost_; }
  bool isWater() const { return isWater_; }
  const Texture& getTexture() const { return texture_; }

private:
  int movementCost_;
  bool isWater_;
  Texture texture_;
};


你会注意这里所有的方法都是const,这不是巧合。 由于同一对象在多处引用,如果你修改了它, 改变会同时在多个地方出现。

这也许不是你想要的,通过分享对象来节约内存的这种优化,不应该影响到应用的显性行为,因此,享元对象几乎总是不可变的。


但是我们不想为每个区块都保存一个实例,如果你看看这个类内部,你会发现里面实际上什么也没有, 唯一特别的是区块在哪里,用享元的术语讲,区块的所有状态都是“固有的”或者说“上下文无关的”。


鉴于此,我们没有必要保存多个同种地形类型,地面上的草区块两两无异,我们不用地形区块对象枚举构成世界网格,而是用Terrain对象指针组成网格:

class World
{
private:
  Terrain* tiles_[WIDTH][HEIGHT];

  // 其他代码……
};


每个相同地形的区块会指向相同的地形实例。


1.jpg

一行区块,每个区块指向共享的草、河、山丘对象。

由于地形实例在很多地方使用,如果你想要动态分配,它们的生命周期会有点复杂,因此,我们直接在游戏世界中存储它们。

class World
{
public:
  World()
  : grassTerrain_(1, false, GRASS_TEXTURE),
    hillTerrain_(3, false, HILL_TEXTURE),
    riverTerrain_(2, true, RIVER_TEXTURE)
  {}

private:
  Terrain grassTerrain_;
  Terrain hillTerrain_;
  Terrain riverTerrain_;

  // 其他代码……
};


然后我们可以像这样来描绘地面:

void World::generateTerrain()
{
  // 将地面填满草皮.
  for (int x = 0; x < WIDTH; x++)
  {
    for (int y = 0; y < HEIGHT; y++)
    {
      // 加入一些丘陵
      if (random(10) == 0)
      {
        tiles_[x][y] = &hillTerrain_;
      }
      else
      {
        tiles_[x][y] = &grassTerrain_;
      }
    }
  }

  // 放置河流
  int x = random(WIDTH);
  for (int y = 0; y < HEIGHT; y++) {
    tiles_[x][y] = &riverTerrain_;
  }
}


我承认这不是世界上最好的地形生成算法。

现在不需要World中的方法来接触地形属性,我们可以直接暴露出Terrain对象。

const Terrain& World::getTile(int x, int y) const
{
  return *tiles_[x][y];
}


用这种方式,World不再与各种地形的细节耦合。 如果你想要某一区块的属性,可直接从那个对象获得:

int cost = world.getTile(2, 3).getMovementCost();


我们回到了操作实体对象的API,几乎没有额外开销——指针通常不比枚举大。



五、性能如何?

性能如何?

我在这里说几乎,是因为性能偏执狂肯定会想要知道它和枚举比起来如何,通过解引用指针获取地形需要一次间接跳转。 为了获得移动开销这样的地形数据,你首先需要跟着网格中的指针找到地形对象, 然后再找到移动开销,跟踪这样的指针会导致缓存不命中,降低运行速度。


需要更多指针追逐和缓存不命中的相关信息,看看数据局部性这章。


就像往常一样,优化的金科玉律是需求优先,现代计算机硬件过于复杂,性能只是游戏的一个考虑方面。 在我这章做的测试中,享元较枚举没有什么性能上的损失,享元实际上明显更快。但是这完全取决于内存中的事物是如何排列的。


我可以自信地说使用享元对象不会搞到不可收拾。 它给了你面向对象的优势,而且没有产生一堆对象,如果你创建了一个枚举,又在它上面做了很多分支跳转,考虑一下这个模式吧。 如果你担心性能,那么至少在把代码编程为难以维护的风格之前先做些性能分析。



六、享元模式游戏运用

享元模式游戏运用

我们先一起来理解一下这个词的意思,然后再说这个模式解决的问题,希望我的解释能帮你快速的了解这个设计模式的意图。


我是这样理解的,享元,共享单元。什么意思呢,将一些资源共享,以减少一些不必要的资源消耗。我接着举几个例子说明一下,为了代入感更强,我就拿游戏举例了。


声明:以下内容只为学习类比使用,并不代表游戏设计方案,游戏如何设计实现,我未参与,也未研究,感兴趣的可自行了解。


游戏地图

我们都知道我的世界是一个自由度超高的沙盒游戏,进到游戏之后我们应该会看到一个画面,就是地图在不断的渲染。这里可能以前玩的时候大家都没有注意过,只是觉得游戏好大,但是不怎么卡。不卡的原因有很多。我们今天要说的就是如何通过享元模式来减少资源负担。

1.jpg

假如我的世界地图中每个单位格子的内容大小为1kb,粗略估计一个画面内格子的数量为1,000,000,此时加载地图需要 1GB 的内存,如果每个格子2kb则 2GB。如果一个单元格内容所用的贴画是 10kb 呢。目前来看 10G 内存也都能接受,可这款游戏放在当年的话,估计不会有人玩了。


如何解决

其实这个方案非常的正常,也非常的简单。

首先我们可以这样做,事先将需要用到的格子贴画统计好,然后一次加载到内存中,记录一下内存的地址,需要用的时候,直接取出来渲染就好了。他们的样子都差不多,只是摆放的位置不同。

还有一种方式呢,就是我用一个先去我的 资源库 找,找不到就创建一个放到资源库中,如果能够找到,就直接返回。

这两种方式都可以。第一种方式将压力放在了启动过程,第二种的方式将压力放在第一次渲染的过程。而一般情况下,游戏的开发都是用第一种方式,也就是我们所说的“过图”,”地图加载“。这个时候去做的,因为一次卡顿加载完和你走着走着卡一下当然第一种更容易接受。


英雄联盟

英雄联盟这款游戏大家应该并不陌生,S10 刚刚结束(10月31日全球总决赛),SN来年再战,加油。

2.jpg


“兵线”

游戏中一共有 3 路兵线,每次出现几只我不清楚,8只好了,3路乘以2(双方)然后在乘以8,这应该是48个对象。而且他们还包含各自的动作,比如魔法兵吐得“口水“,炮车喝奶茶吐的“珍珠”等等,如果是你在开发兵线系统的时候,内存爆炸了,比如有的玩家搞怪,不杀小兵,积攒了很多小兵,然后他卡了,说你游戏垃圾。你该如何去做呢。


其实我们分析下来的话,这里只会出现三种不同的兵种,步兵、魔法兵、炮车。然后再分为红蓝两方。在加上两个子弹。是不是就只有这8个对象呢,至于他们的轨迹,那些是每个对象的“外部状态”


如何构成

知道了这种设计模式思路,就要继续了解一下享元模式具体的构成角色都有哪些了,比如以英雄联盟的兵线为例吧。


客户端

首先有一个客户端,负责获取对象,然后渲染,这里我们通过#get、#draw(x,y)来表示获取和画来代替这步动作,(x,y)表示渲染出来的对象坐标。


享元工厂

然后还有一个为我们提供小兵的统一接口,这里使用的就是我们前面学习的工厂方法,小兵工厂。这里顺便复习一下之前的工厂和抽象工厂两个设计模式。如果我通过一个工厂来实现小兵对象的创建,那么就是一个工厂模式,但是我现在想在应用的时候,在灵活一些,我们可以从小兵身上抽取特征,比如步兵、魔法兵、炮车、这是小兵类别,但我们有两个不同的作战方,红方和蓝方,所以此时可以使用抽象工厂模式来生产小兵,红方小兵工厂生产出来的都是红方的步兵、魔法兵、炮车。蓝方生产出来的就是蓝方的步兵、魔法兵、炮车。


享元对象

有了工厂之后,我们就要有具体的共享对象了,共享对象就是我们上面所说的那 8 个。


下面这个类图顺便复习了一下 抽象工厂模式

3.jpg


享元模式类图 

4.jpg


七、再来一个实例

再来一个实例

我们将创建一个 Shape 接口和实现了 Shape 接口的实体类 Circle,下一步是定义工厂类 ShapeFactory。

1.jpg

ShapeFactory 有一个 Circle 的 HashMap,其中键名为 Circle 对象的颜色,无论何时接收到请求,都会创建一个特定颜色的圆。ShapeFactory 检查它的 HashMap 中的 circle 对象,如果找到 Circle 对象,则返回该对象,否则将创建一个存储在 hashmap 中以备后续使用的新对象,并把该对象返回到客户端。


FlyWeightPatternDemo 类使用 ShapeFactory 来获取 Shape 对象,它将向 ShapeFactory 传递信息(red / green / blue/ black / white),以便获取它所需对象的颜色。

步骤 1

创建一个接口。

Shape.java
public interface Shape 
{
   void draw();
}


步骤 2

创建实现接口的实体类。

Circle.java
public class Circle implements Shape 
{
   private String color;
   private int x;
   private int y;
   private int radius;
 
   public Circle(String color)
   {
      this.color = color;     
   }
 
   public void setX(int x) 
   {
      this.x = x;
   }
 
   public void setY(int y) 
   {
      this.y = y;
   }
 
   public void setRadius(int radius) 
   {
      this.radius = radius;
   }
 
   @Override
   public void draw() 
   {
      System.out.println("Circle: Draw() [Color : " + color 
         +", x : " + x +", y :" + y +", radius :" + radius);
   }
}


步骤 3

创建一个工厂,生成基于给定信息的实体类的对象。

ShapeFactory.java
import java.util.HashMap;
 
public class ShapeFactory 
{
   private static final HashMap<String, Shape> circleMap = new HashMap<>();
 
   public static Shape getCircle(String color) 
   {
      Circle circle = (Circle)circleMap.get(color);
 
      if(circle == null) 
      {
         circle = new Circle(color);
         circleMap.put(color, circle);
         System.out.println("Creating circle of color : " + color);
      }
      return circle;
   }
}


步骤 4

使用该工厂,通过传递颜色信息来获取实体类的对象。

FlyweightPatternDemo.java
public class FlyweightPatternDemo 
{
   private static final String colors[] = 
      { "Red", "Green", "Blue", "White", "Black" };
   public static void main(String[] args) 
   {
 
      for(int i=0; i < 20; ++i) 
      {
         Circle circle = 
            (Circle)ShapeFactory.getCircle(getRandomColor());
         circle.setX(getRandomX());
         circle.setY(getRandomY());
         circle.setRadius(100);
         circle.draw();
      }
   }
   
   private static String getRandomColor() 
   {
      return colors[(int)(Math.random()*colors.length)];
   }
   
   private static int getRandomX() 
   {
      return (int)(Math.random()*100 );
   }
   
   private static int getRandomY() 
   {
      return (int)(Math.random()*100);
   }
}


步骤 5

执行程序,输出结果:

Creating circle of color : Black
Circle: Draw() [Color : Black, x : 36, y :71, radius :100
Creating circle of color : Green
Circle: Draw() [Color : Green, x : 27, y :27, radius :100
Creating circle of color : White
Circle: Draw() [Color : White, x : 64, y :10, radius :100
Creating circle of color : Red
Circle: Draw() [Color : Red, x : 15, y :44, radius :100
Circle: Draw() [Color : Green, x : 19, y :10, radius :100
Circle: Draw() [Color : Green, x : 94, y :32, radius :100
Circle: Draw() [Color : White, x : 69, y :98, radius :100
Creating circle of color : Blue
Circle: Draw() [Color : Blue, x : 13, y :4, radius :100
Circle: Draw() [Color : Green, x : 21, y :21, radius :100
Circle: Draw() [Color : Blue, x : 55, y :86, radius :100
Circle: Draw() [Color : White, x : 90, y :70, radius :100
Circle: Draw() [Color : Green, x : 78, y :3, radius :100
Circle: Draw() [Color : Green, x : 64, y :89, radius :100
Circle: Draw() [Color : Blue, x : 3, y :91, radius :100
Circle: Draw() [Color : Blue, x : 62, y :82, radius :100
Circle: Draw() [Color : Green, x : 97, y :61, radius :100
Circle: Draw() [Color : Green, x : 86, y :12, radius :100
Circle: Draw() [Color : Green, x : 38, y :93, radius :100
Circle: Draw() [Color : Red, x : 76, y :82, radius :100
Circle: Draw() [Color : Blue, x : 95, y :82, radius :100



八、享元模式归纳

享元模式归纳

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。


享元模式(Flyweight Pattern) 也称为轻量级模式 是对象池的一种实现,类似于线程池,线程池可以避免不停的创建和销毁对象消耗性能,减少了对象数量从而改善应用所需的对象结构的方式。


其宗旨是共享细粒度对象,将多个对同一对象的访问集中起来,不必为每个访问者创建一个独立的对象,以此来降低内存的消耗,属于结构型模式。享元模式把一个对象的状态分成内部状态和外部状态,内部状态即是不变的,外部状态是变化的,然后通过共享不变的部分,达到减少对象数量并节约内存的目的,其本质是缓存共享对象,降低内存消耗。


九、享元模式模式特点

享元模式模式特点

意图:运用共享技术有效地支持大量细粒度的对象。

主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

何时使用: 

1、系统中有大量对象。 

2、这些对象消耗大量内存。 

3、这些对象的状态大部分可以外部化。 

4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 

5、系统不依赖于这些对象身份,这些对象是不可分辨的。

如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。

关键代码:用 HashMap 存储这些对象。

应用实例: 

1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 

2、数据库的连接池。

注意事项: 

1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。

2、这些类必须有一个工厂对象加以控制。


十、享元模式的角色组成

享元模式的角色组成

抽象享元角色(Flyweight)

享元对象抽象基类或者接口,同时定义出对象的外部状态和内部状态的接口或者实现。


具体享元角色(Concrete Flyweight)

实现抽象角色定义的业务,该角色的内部状态处理应该与环境无关,不能出现一个操作改变内部状态,同时修改了外部状态。


享元工厂(Flyweight Factory)

负责管理享元对象池和创建享元对象。


十一、享元模式应用场景

享元模式应用场景

1、系统中有大量对象 

2、这些对象消耗大量内存 

3、这些对象的状态大部分可以外部化 

4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替 

5、系统不依赖于这些对象身份,这些对象是不可分辨的。

当系统中多处需要同一组信息时,可以把这些信息封装到一个对象中,然后对该对象进行缓存,这样一个对象就可以提供给多处需要使用的地方,避免大量同一对象的多次创建,消耗大量内存空间。享元模式其实就是工厂模式的一个改进机制,享元模式同样要求创建一个或者一组对象,并且就是通过工厂方法生成对象的,只不过享元模式中为工厂方法增加了缓存这一功能。主要总结为一下应用场景:

1、常常应用于系统底层的开发,以便解决系统的性能问题

2、系统有大量相似对象、需要缓冲池的场景

像生活中中介机构的房源共享、全国社保联网等场景


十二、享元模式实现方式

享元模式实现方式

将需要改写为享元的类成员变量拆分为两个部分:

内在状态:包含不变的、可在许多对象中重复使用的数据的成员变量;

外在状态:包含每个对象各自不同的情景数据的成员变量。

保留类中表示内在状态的成员变量,并将其属性设置为不可修改,这些变量仅可在构造函数中获得初始数值。

找到所有使用外在状态成员变量的方法,为在方法中所用的每个成员变量新建一个参数,并使用该参数代替成员变量。

你可以有选择地创建工厂类来管理享元缓存池,它负责在新建享元时检查已有的享元,如果选择使用工厂,客户端就只能通过工厂来请求享元,它们需要将享元的内在状态作为参数传递给工厂。

客户端必须存储和计算外在状态(情景)的数值,因为只有这样才能调用享元对象的方法,为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。


十三、享元模式优缺点

享元模式优缺点

优点

1、减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率。

2、减少内存之外的其他资源占用。

3、大大减少对象的创建,降低系统的内存,使效率提高。

缺点

1、关注内、外部状态、关注线程安全问题。

2、使得系统、程序的逻辑复杂化。

3、提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。