【设计模式与体系结构】结构型模式-享元模式
简介
享元模式(Flyweight Pattern)是一种用于优化创建和使用对象的结构型设计模式。享元模式以共享的方式高效地支持大量细粒度的对象的重用,它的主要目的是通过共享对象来减少内存的使用和提高性能。在很多系统软件中,会创建大量相似的对象,这些对象可能只有部分属性不同,享元模式就是为了处理这种情况而出现的。
享元模式将对象的结构分为了内部状态和外部状态,内部状态是指对象可共享的部分,它存储在享元对象内部并且不会随着环境变化而变化,内部状态是享元对象能被共享的关键,外部状态是随环境而变化的部分,不能够被不同对象共享,它通常在对象的方法调用时作为参数传入。
享元模式的角色
- 抽象享元(Flyweight)类:通常是一个接口或抽象类,在抽象享元类声明了具体享元类的公共方法,这些公共方法向外界提供了享元对象的内部状态的数据,以及供外界处理外部状态的数据。内部状态相关数据通常设计为成员变量,外部状态相关数据通常通过依赖注入的方式添加到享元类中。
- 具体享元(ConcreteFlyweight)类:实现抽象享元类,对应实例称为享元对象。具体享元类为内部状态提供了存储空间,通常可以使用单例模式来设计享元模式的内部状态,为每一个具体享元类提供一个唯一的享元对象。
- 非共享具体享元类(UnsharedConcreteFlyweight)类:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类设计为非共享具体享元类。当需要一个非具体享元类对象的时候,则直接创建对象即可。
- 享元工厂(FlyweightFactory)类:享元工厂类用于创建和管理享元对象,它面向抽象享元类编程,将各种具体享元类的对象存储在享元池中,享元池一般设计为一个键值对的集合(也可以设计为其他数据结构,根据具体需求制宜),一般结合工厂方法模式进行设计。当用户请求创建一个共享的具体享元对象时,首先会从享元池中获取,若存在则直接获取,若不存在则创建一个新的具体享元对象,并存放在享元池中。
享元模式的类型
- 单纯享元模式:所有具体享元类都是可以共享的,不存在非共享具体享元类。
- 复合享元模式:使用组合模式将单纯享元模式进行组合,形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成多个单纯享元对象,而单纯享元对象可以共享。
享元模式的优点
- 极大地减少了内存中对象的数量,降低内存消耗。这对有着大量相似对象的系统有着极其重要的意义
- 由于对象的创建和销毁减少了,因此一定程度上也能提高系统的性能,特别是创建和销毁开销较大的情况
享元模式的缺点
- 增加系统复杂性,享元模式的实现相对复杂,需要正确地划分内部状态和外部状态,并且需要一个享元工厂来管理对象(享元模式一般要配合工厂方法模式进行使用),增加了代码的复杂性和维护成本
- 外部状态的管理可能会较为复杂,因为外部状态不能共享,需要在对象使用过程中不断传递和处理,会导致代码的可维护性和可读性降低
享元模式的应用场景
- 游戏开发:游戏中有大量相似的游戏角色、道具、场景等等
- 图形系统:在很多图形绘制软件中,有很多相似的图形,如各种颜色的线条、各种形状的图形
- 文件处理系统:在文字处理器中,字符的字体、字号等属性可以作为内部状态,字符的位置可以作为外部状态。这样可以减少字符对象的创建数量,提高系统性能。
正文
许多人都玩过游戏,并且为了满足玩家的个性化需求,常常会出一些装备系统等。例如,王者荣耀里面有皮肤机制。王者荣耀里面的英雄,对于每个玩家来说,有一些基本信息都是相同的,那么就可以视为内部状态信息。但是皮肤是需要购买或者活动获取,每个玩家的拥有状态不一样,且每个人喜好的皮肤不一样,装扮状态也不一样,是会随玩家个性化需求变化而变化的,因此皮肤属于外部状态信息。下面就以王者荣耀的英雄信息作为案例,进行代码讲解。
定义一个抽象英雄类 AbstractHero.java。其中英雄的基本信息是共享的,可以定义一个英雄信息类 HeroInfo.java,但是皮肤信息是不共享的,可以简单定义一个字符串类型的 skin 记录不同玩家的英雄皮肤信息。
public abstract class AbstractHero {
private HeroInfo info;
private String skin = "原皮肤";
public AbstractHero(String name) {
HeroFlyweightFactory heroFlyweightFactory = HeroFlyweightFactory.getInstance();
this.info = heroFlyweightFactory.getHeroInfo(name);
}
public HeroInfo getInfo() {
return info;
}
public String getSkin() {
return skin;
}
public void setSkin(String skin) {
this.skin = skin;
}
}
class HeroInfo {
private String name;//名字
public HeroInfo(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
定义一个具体英雄类 Hero.java
public class Hero extends AbstractHero {
public Hero(String name) {
super(name);
}
public void updateSkin(String newSkin) {
System.out.println(getInfo().getName()+ "更换皮肤:" + getSkin() + " -> " + newSkin);
setSkin(newSkin);
}
public void printCurrentSkin() {
System.out.println(getInfo().getName() + "当前皮肤为 " + getSkin());
}
}
定义一个英雄享元工厂类 HeroFlyweightFactory.java,这是享元模式的核心。
public class HeroFlyweightFactory {
private static HeroFlyweightFactory instance;
private Map<String, HeroInfo> heroes;
private HeroFlyweightFactory() {
heroes = new HashMap<String, HeroInfo>();
}
//使用单例模式获取享元工厂的单例
public static HeroFlyweightFactory getInstance() {
if (instance == null) {
synchronized (HeroFlyweightFactory.class) {
if (instance == null) {
instance = new HeroFlyweightFactory();
}
}
}
return instance;
}
//获取英雄:创建英雄可以采取更复杂的逻辑,配合工厂方法模式进行创建,为了编码简单,这边演示的是简单工厂方法
public HeroInfo getHeroInfo(String name) {
if (heroes.containsKey(name)) {//若角色已经创建过
return heroes.get(name);
}
HeroInfo info = new HeroInfo(name);
heroes.put(name, info);
return info;
}
}
随后写一个客户端案例 Client.java
public class Client {
public static void main(String[] args) {
HeroFlyweightFactory heroFlyweightFactory = HeroFlyweightFactory.getInstance();
AbstractHero nvwa1 = new Hero("nvwa");
System.out.println(nvwa1.hashCode() + " " + nvwa1.getInfo().hashCode());
AbstractHero nvwa2 = new Hero("nvwa");
System.out.println(nvwa2.hashCode() + " " + nvwa2.getInfo().hashCode());
AbstractHero pangu = new Hero("pangu");
System.out.println(pangu.hashCode() + " " + pangu.getInfo().hashCode());
nvwa1.setSkin("尼罗河女神");
nvwa2.setSkin("遇见飞天");
System.out.println("nvwa1的皮肤是" + nvwa1.getSkin() + " nvwa2的皮肤是" + nvwa2.getSkin() + " pangu的皮肤是" + pangu.getSkin());
}
}
运行效果截图如下:

浙公网安备 33010602011771号