享元模式
定义
运用共享技术有效地支持大量细粒度的对象。其目的是通过共享对象来减少内存的使用,尤其是当系统中有大量相似对象时,享元模式可以帮助减少对象的创建,从而降低系统的内存消耗。
具体来说,享元模式将对象的共享部分和不共享部分分开,尽可能地将重复的对象共享,而将每个对象的独立部分(外部部分)保留在每个实例中。
关键要素分析
内部状态与外部状态:
内部状态(Intrinsic State):
可以共享的、不变的部分。例如,字体、颜色、形状等不随对象变化而变化的属性。
外部状态(Extrinsic State):
每个对象独有的、可能会变化的部分,例如位置、大小等。
共享对象:
享元模式通过共享内部状态的对象来减少内存消耗。客户端只需要维护外部状态,内部状态由共享的享元对象提供。
享元工厂(FlyweightFactory):
负责管理享元对象的创建和共享。它维护一个缓存池,避免重复创建相同的享元对象,只有在享元对象不存在时才会创建一个新的对象。
目的分析
还是得结合现实来进行一些分析,在生活中,存在一些场景需要我们去使用一些重复的对象或者具有高相似度的对象,在这种需求下,我们如果去给这些对象都进行一个完整的构造,(换句话说,我们如果想要对这些个对象进行各自数据的一套构建) 将会是很费时费力的。
这里我们引用C++中类的一个特性来说明:类的静态(static)成员
我们知道,在类的设计中,我们如果将一个成员的属性声明为了static属性,那么这个成员将会是在这个类实现出的所有类中共享的。通过这个属性的声明,我们能够实现类对于这个成员属性的共享。同时,这个属性也不会在派生出的类中进行创建。
题外话,你如果读过CSAPP你就应该知道这个静态成员会被储存在哪里。其实就是我们可执行文件中的.bss段。所有的实例类中应该都保留着这块内存区域的一个引用以提供对于这个静态成员变量的访问。
从上面我们初步了解了享元模式下的一个基本性质,就是实现对于一些类属性的共享。当然,在我们的享元模式中,一般不指这些通过静态成员的实现。在具体实现中,我们是将这种属性应用到了更复杂的场景之中。这时如果使用一个静态成员来设计的话会导致设计上面的麻烦,所以我们考虑将这些应该被共享的属性放于一个类中并进行管理。
无端猜测
虽然我还没有深入去了解这个的应用场景。不过我想进行一些合理的猜测,在实际应用这种,我们这些共享类的创建如果比较严格,要求不能够被随便复制出去的话,可能考虑使用一个单例模式来创建这些个共享类。如果这些属性需要被频繁的修改以及被一些类给单独拿出进行使用,那么可能使用原型模式来设计共享类。
话说回来,我们还是来系统的说明下襄垣模式的目的吧。我的问题就是在学一个东西时总是会发散出去,想当初学一节CSAPP的课就为了了解这了解那一个半小时的课硬是给我用了一个晚上和一个早上。
目的
- 减少内存消耗:通过共享相同的对象,避免了每次创建相同对象的内存开销。
- 提高性能:减少了重复对象的创建,降低了对象的实例化时间。
- 控制细粒度对象的数量:享元模式适用于需要大量细粒度对象并且这些对象大部分具有相同状态的场景。
美图
UML类图
第一类图分析
让我们来分析下这个类图中的层次
这个类图其实只是实际享元模式中的一个层次,即享元类的创建,不过这个图能够帮助我们很好的一步步理解具体的享元模式下的架构,我们先来分析下这里的功能。这个类图中的功能其实就只是在具体的享元模式中去负责享元类的创建,至于享元类在其他类的运用,并没有在这个图中显示出来。
在这个类中,BlackPiece类和WhitePiece类就是我们的俩种享元类。其实这个类图还可以进行优化,我们看到这俩个类其实具有相同的成员,也就是说,这俩个其实都可以归档到一个父类进行管理以及接口规范的,不过这里还行了,你可以自己去画一个类图。
我们来看,在这种类图中,我们其实不是直接通过享元类去进行创建的。相反,我们是通过一个工厂类来进行这些享元类的生产的。其实给出这个类图后你就应该了解这个工厂存在的意义了。毕竟,如果你都需要使用享元类了,就意味你不免需要创建大量的相似对象,你只是使用了结构性模式去进行这些对象的建构,但是这些对象的生产呢?这里是设计模式的建造者模式中需要去管理的。我们需要注意的是,对于系列的设计模式之间,其并不是相互割裂的,相反,它们的关系极其紧密。
这里我对工厂的猜想是一般不会用到抽象工厂,一般的话应该使用工厂模式即可,不过还是那句话,依照现实需求去进行设计。这里直接使用简单工厂来画也无所谓,只是为了说明这种建造模式是可行且可取的。
简单分析
在这里,我们存在着多种享元类,这些享元类具有着相同的外部属性,但是这些属性内部的实现可能有所不同,就比如在游戏中的子弹,可能伤害高点,可能伤害低点,依照这种差异化进行从抽象父类的派生。
接着,我们需要对这些个享元基类进行一些实例化。虽然说享元基类是为了减少实例化而产生的。但是,这里的工厂创建其实是为了适配现实生活中的场景的。毕竟,即使你再很多地方不用去进行享元基类的再创建,但是你在一些应用场景下你还是需要去对其进行批量再创建的,这里就需要我们的建造型设计模式了,上面说了一些,不再扩展。当然,你也可以不使用工厂,依照现实需求,你甚至可以对这些个享元基类进行单例模式的使用。
总之,这上面这个类图,其实是为了进行享元模式中的享元类的构建,接下来,我们再看一个类图。
第二类图分析

这里我们就不对那些之前分析过的进行分析,我们只来看这里的享元基类与实际对象之间的关系。
享元类与构件类
可以看到,它们之间是通过一个聚合关系来进行连接的,这其实也符合享元模式的设计思路。享元模式希望这些享元类是一些相对静态的数据。那么,我们不应该希望这些个享元被挂载到特定的构件类中导致这俩的声明周期被绑定。简单说吧,通过聚合关系而不是组合关系能够更符合我们生产环境中对于享元模式中的管理。
额外注意到一点,在这个类图中,其实享元类的生命周期是被工厂类所管理的。由工厂类去管理是否要生产享元类,同时,由工厂类来进行这些享元类的实际分配。通过这种职责的分离,各个类功能进行进一步的解耦,更加符合单一职责原则。
好了,其他的其实就没有什么好说的,你应该也能够看懂,该上代码了。
代码示例
在设计模式的搓代码过程中,慢一点,跟着搓,去逐层建构其整个设计模式下的层次逻辑,能给让你对整个的理解更加深入。
享元基类
1 | //享元模式的基类 |
这个类定义了我们整个享元类的基本属性,定义了接口规范等,老生常谈的东西,这里就不再赘述,自己看吧。
享元子类
1 | //享元类的子类-共享的 |
这个其实页没有什么好说的,就是一个具体的享元类,定义了这类享元所应该有的属性。
需要注意的是,我们派生出来的这些享元类之间的属性应该是存在差异的,就比如,一个可能是共享的,一个可能是不共享的。可能还是抽象了点。换句话说吧,在**<<葬送的芙莉莲>>**(我缺的第二季这块快点给我端上来啊!!!!!)中的宝箱。这其中一些可能包含了一些魔导书,可能包含了一些装备,当然,也有可能是宝箱怪。但是,这些都是宝箱,而且,这些都是会在异世界世界观下普遍出现的事物。这里的就跟享元模式的应用环境很像了。
享元挂载类
1 | //发射炮弹,享元类挂载的大类,复杂的实际对象 |
这里就是对应的享元类挂载的地方,在这其中其实包含的是一些相对统一的方法。而且事先声明一点,在稍微复杂一点的设计中,这种类可能是不止一个的。这样类比吧,我这里其实是吧那些个宝箱怪,具有各种奖励的宝箱都归纳到了这个类中,并没有赋予具体的属性,在实际的设计中,对于这些宝箱怪,武器宝箱等,一般是存在各自独立的类来进行挂载的,这里只是为了简单而编写的,别被我限定了思路。
回来看到这个类中,我们可以看到在这个类中存在着一些自己的方法,其实这些方法你看起来有没有点眼熟。是不是有点像之前的外观模式中的封装。事实上也确实,你想啊,当你打开一个宝箱时,你知道里面发生了什么吗,不知道吧,你只是看到了最后的一个结果。当然,打住,这方面不再深入。
总之,对于这个类,我们需要注意的是,这一系列类其实就是对应的享元类的具体使用实现,这些类是使用享元类的外层,是我们用户层所直接接触到的东西
享元工厂类
1 | //享元简单工厂类 |
这个类其实是我认为这些类之中最有理解难度的一个类了。
梳理
看到这了,我们先停下来,你还记得住前面我们讲的层次吗,我们再来梳理一遍。
首先,这些类最底层的,应该是我们的享元抽象类,这个抽象类管理者享元类的基本属性以及接口规范
然后,是我们的具体享元类,这些类定义了我们具体的享元属性。例如,宝箱怪属性,武器箱属性
再然后,是我们的享元挂载类,这是一系列享元实例化的地方。为什么要说实例化,因为一般来说,单独的享元类是没有使用的 价 值的,只有将这些属性添加到实际的事物上它才有使用的价值。
最后就是我们现在要讲的享元工厂类了。
当然,我们这里讲的不是整个的层次,你看层次还是得看UML类图,这里只是说的我的顺序。叠个小甲
来进入这个工厂,这个工厂的外部功能其实很清晰,而且由于我们已经学完了工厂模式,所以我们看过去就知道这个工厂打的怎么工作的,所以我们看到这个工厂中最有意思的一个地方。就是这里的成员变量 map<string,FlyweightBody*> m_map;
这里最有意思的地方就是这里的键值对,这里的第二个成员是我们的抽象享元基类指针,这意味着什么,意味着我们可以对其进行内存的管理。而我们回想一下工厂的功能是什么,就是生产我们的实例对象吧。你到这里应该有自己的想法了吧。是的,这里工厂最特殊的地方就是在于其可以管理所有在这个工厂中生产的享元对象的整个生命周期,包括从创建一直到死亡。
你到现在可能还有点疑惑,没事,等我给出下面的测试代码你就明白了。
测试程序
1 | //创建三种型号炮弹,每种有若干个对象对该类对象进行复用 |
这个测试程序我就不进行分析了,自己分析去吧。我们主要来看到这个测试程序中的内存管理这一块。
在这个程序中,我们大部分的享元类都是由工厂创建的,那么也就意味着,这些个享元类在工厂类中都保有一份指针。或者,我们其实可以这么理解,这些享元类是一系列的单一产品,由工厂生产,且只会在工厂内部保留一份。当你想要使用产品时,你去申请对应的使用权,你可以利用这个产品进行一些操作,,但是你无权对于这个产品进行销毁。
现实生活中其实存在这种例子,但是我不是很清楚,我能够想到的就是一些公司提供的一些服务。有懂的可以在下面评论区分享。
总之,在这种工厂架构下,你无权去对这个产品的声明周期进行管理,这里是可以的,所以这里其实存在一些设计上的巧妙之处,当然巧在哪我也不知道()。也就是说,这个工厂其实还起到了一个屏蔽上下层使用的作用,用户只需要去使用,至于内存管理这方面的细节,其并不需要去关心,自然有人去进行管理。在这里,就是由工厂区进行管理的。
当我理解这一点之后,我其实是感觉到很兴奋的,因为这种设计实在是太妙了。用户不需要去进行繁琐的可能错漏的一个个享元对象的内存管理(如果存在多个享元类实例的话)。享元类本身其实也不需要去注意它什么时候去死。享元类的内存生命周期被巧妙的绑定到了工厂之上,由工厂直接管理。我们可以在工厂中去定义内存管理上面的细节,就比如工厂(死亡)倒闭时,其中的产品也大多不存在一般,可能是在工厂死亡是被一起析构掉的,也可能是其他。
总的来说,这个架构对于单一职责的遵守,使得整个程序的架构变得很美。这种美是你不去自己搓一遍码,去理解对应的UML图所接触不到的,所以说,看UML类图是设计模式的不可或缺的一环。
总结
在享元模式中,存在一些令人兴奋的设计方法,这些方法需要你去进行一定程度的理解。
享元模式的重点,如其他模式一般,落在了它的名字之上。
享元二字,重点在享。通过对于一系列的共有属性的剥离,实现了属性的复用,通过将这种属性的创建和管理对于工厂的委托,实现了资源管理上的优化。不仅使得属性得以很大程度上的复用,还是得用户不用去担心内存管理上的细节。