一、享元模式介绍
享元模式介绍
迷雾散尽,露出了古朴庄严的森林,古老的铁杉,在头顶编成绿色穹顶。 阳光在树叶间破碎成金色顶棚,从树干间远眺,远处的森林渐渐隐去。
这是我们游戏开发者梦想的超凡场景,这样的场景通常由一个模式支撑着,它的名字低调至极:享元模式。
享元是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。
计算机世界中无穷无尽的可能,其本质都是由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实在是太过了,幸运的是,有一种老办法来处理它。
关键点在于,哪怕森林里有千千万万的树,它们大多数长得一模一样,它们使用了相同的网格和纹理,这意味着这些树的实例的大部分字段是一样的。
你要么是疯了,要么是亿万富翁,才能让美术给森林里每棵树建立独立模型。
一行树,每棵都有自己的网格、纹理、树叶,调节参数和位置朝向。
注意每一棵树的小盒子中的东西都是一样的。
我们可以通过显式地将对象切为两部分来更加明确地模拟。 第一,将树共有的数据拿出来分离到另一个类中:
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_;
};
你可以将其想象成这样:
一行树,每个都有自己的参数和位置朝向,指向另一个有网格、纹理、树叶的树模型。
这有点像类型对象模式,两者都涉及将一个类中的状态委托给另外的类,来达到在不同实例间分享状态的目的,但是,这两种模式背后的意图不同。
使用类型对象,目标是通过将类型引入对象模型,减少需要定义的类,伴随而来的内容分享是额外的好处,享元模式则是纯粹的为了效率。
把所有的东西都存在主存里没什么问题,但是这对渲染也毫无帮助,在森林到屏幕上之前,它得先到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];
// 其他代码……
};
每个相同地形的区块会指向相同的地形实例。
一行区块,每个区块指向共享的草、河、山丘对象。
由于地形实例在很多地方使用,如果你想要动态分配,它们的生命周期会有点复杂,因此,我们直接在游戏世界中存储它们。
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,几乎没有额外开销——指针通常不比枚举大。
五、性能如何?
性能如何?
我在这里说几乎,是因为性能偏执狂肯定会想要知道它和枚举比起来如何,通过解引用指针获取地形需要一次间接跳转。 为了获得移动开销这样的地形数据,你首先需要跟着网格中的指针找到地形对象, 然后再找到移动开销,跟踪这样的指针会导致缓存不命中,降低运行速度。
需要更多指针追逐和缓存不命中的相关信息,看看数据局部性这章。
就像往常一样,优化的金科玉律是需求优先,现代计算机硬件过于复杂,性能只是游戏的一个考虑方面。 在我这章做的测试中,享元较枚举没有什么性能上的损失,享元实际上明显更快。但是这完全取决于内存中的事物是如何排列的。
我可以自信地说使用享元对象不会搞到不可收拾。 它给了你面向对象的优势,而且没有产生一堆对象,如果你创建了一个枚举,又在它上面做了很多分支跳转,考虑一下这个模式吧。 如果你担心性能,那么至少在把代码编程为难以维护的风格之前先做些性能分析。
六、享元模式游戏运用
享元模式游戏运用
我们先一起来理解一下这个词的意思,然后再说这个模式解决的问题,希望我的解释能帮你快速的了解这个设计模式的意图。
我是这样理解的,享元,共享单元。什么意思呢,将一些资源共享,以减少一些不必要的资源消耗。我接着举几个例子说明一下,为了代入感更强,我就拿游戏举例了。
声明:以下内容只为学习类比使用,并不代表游戏设计方案,游戏如何设计实现,我未参与,也未研究,感兴趣的可自行了解。
游戏地图
我们都知道我的世界是一个自由度超高的沙盒游戏,进到游戏之后我们应该会看到一个画面,就是地图在不断的渲染。这里可能以前玩的时候大家都没有注意过,只是觉得游戏好大,但是不怎么卡。不卡的原因有很多。我们今天要说的就是如何通过享元模式来减少资源负担。
假如我的世界地图中每个单位格子的内容大小为1kb,粗略估计一个画面内格子的数量为1,000,000,此时加载地图需要 1GB 的内存,如果每个格子2kb则 2GB。如果一个单元格内容所用的贴画是 10kb 呢。目前来看 10G 内存也都能接受,可这款游戏放在当年的话,估计不会有人玩了。
如何解决
其实这个方案非常的正常,也非常的简单。
首先我们可以这样做,事先将需要用到的格子贴画统计好,然后一次加载到内存中,记录一下内存的地址,需要用的时候,直接取出来渲染就好了。他们的样子都差不多,只是摆放的位置不同。
还有一种方式呢,就是我用一个先去我的 资源库 找,找不到就创建一个放到资源库中,如果能够找到,就直接返回。
这两种方式都可以。第一种方式将压力放在了启动过程,第二种的方式将压力放在第一次渲染的过程。而一般情况下,游戏的开发都是用第一种方式,也就是我们所说的“过图”,”地图加载“。这个时候去做的,因为一次卡顿加载完和你走着走着卡一下当然第一种更容易接受。
英雄联盟
英雄联盟这款游戏大家应该并不陌生,S10 刚刚结束(10月31日全球总决赛),SN来年再战,加油。
“兵线”
游戏中一共有 3 路兵线,每次出现几只我不清楚,8只好了,3路乘以2(双方)然后在乘以8,这应该是48个对象。而且他们还包含各自的动作,比如魔法兵吐得“口水“,炮车喝奶茶吐的“珍珠”等等,如果是你在开发兵线系统的时候,内存爆炸了,比如有的玩家搞怪,不杀小兵,积攒了很多小兵,然后他卡了,说你游戏垃圾。你该如何去做呢。
其实我们分析下来的话,这里只会出现三种不同的兵种,步兵、魔法兵、炮车。然后再分为红蓝两方。在加上两个子弹。是不是就只有这8个对象呢,至于他们的轨迹,那些是每个对象的“外部状态”
如何构成
知道了这种设计模式思路,就要继续了解一下享元模式具体的构成角色都有哪些了,比如以英雄联盟的兵线为例吧。
客户端
首先有一个客户端,负责获取对象,然后渲染,这里我们通过#get、#draw(x,y)来表示获取和画来代替这步动作,(x,y)表示渲染出来的对象坐标。
享元工厂
然后还有一个为我们提供小兵的统一接口,这里使用的就是我们前面学习的工厂方法,小兵工厂。这里顺便复习一下之前的工厂和抽象工厂两个设计模式。如果我通过一个工厂来实现小兵对象的创建,那么就是一个工厂模式,但是我现在想在应用的时候,在灵活一些,我们可以从小兵身上抽取特征,比如步兵、魔法兵、炮车、这是小兵类别,但我们有两个不同的作战方,红方和蓝方,所以此时可以使用抽象工厂模式来生产小兵,红方小兵工厂生产出来的都是红方的步兵、魔法兵、炮车。蓝方生产出来的就是蓝方的步兵、魔法兵、炮车。
享元对象
有了工厂之后,我们就要有具体的共享对象了,共享对象就是我们上面所说的那 8 个。
下面这个类图顺便复习了一下 抽象工厂模式。
享元模式类图
七、再来一个实例
再来一个实例
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类 Circle,下一步是定义工厂类 ShapeFactory。
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、提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。