Java设计模式-享元模式
享元(Flyweight)模式,有时也被称为蝇量模式或者轻量级模式,在23种设计模式中属于对象结构型模式。
享元模式是运用共享技术有效地支持大量细粒度对象的复用。
模式介绍
在平常面向对象的软件开发中,偶尔会遇到这种场景,业务比较复杂,需要构建大量的对象,但是这些对象又很相似,状态变化比较小。比如设计一款围棋游戏、丛林探险类游戏或者常见的文本处理软件,在这些场景中棋子、树木或者字符都被设计为单个对象,如果每一个对象都包含了该种类型的所有属性(例如围棋的颜色、位置属性),那么不可避免的系统会产生大量的相似或者相同对象。造成的后果就是系统运行代价过高,带来系统性能下降,可能引发OOM等一系列问题。享元模式就是为了解决这类问题:需要创建大量相似对象,而又不影响开发者使用面向对象的技术操作这些对象。
在开发中抽象的粒度一直都是一个相当难以把握的尺度,而享元模式重点就在于对象粒度的抽象。熟悉和应用该设计模式在某些程度上一定可以加深你对面向对象开发的理解。
适用性
一般在介绍到设计模式的适用性时,只要满足适用性罗列的条件之一即可,但是享元模式比较例外,它建议当以下条件都满足时再使用。
- 一个应用使用了大量相同或者相似的对象,造成很大的内存开销;
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中;
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象;
示例介绍
这里就以中国围棋为示例介绍一下共享模式,在本示例中,我们将黑色棋子和白色棋子作为共享对象,将棋子的位置属性作为非共享对象。
// 围棋类-享元类 public abstract class AlphaGo { public abstract String getColor(); public void display(Coordinates coord) { System.out.println("棋子颜色:" + getColor() + ",棋子位置:" + coord.getX() + " " + coord.getY()); } } // 黑色棋子-具体共享类 public class BlackGo extends AlphaGo { @Override public String getColor() { return "黑色"; } } // 围棋棋子工程类-享元工厂类 public class GoFactory { private Mapmap = new HashMap<>(); private GoFactory() { map.put("b", new BlackGo()); map.put("w", new WhiteGo()); } public static GoFactory getInstance() { return InstanceHolder.INSTANCE; } public AlphaGo getGo(String color) { return map.get(color); } private static class InstanceHolder { private static GoFactory INSTANCE = new GoFactory(); } } // 围棋位置坐标类-外部状态类 public class Coordinates { private int x; private int y; public Coordinates(int x, int y) { this.x = x; this.y = y; } // 省略了setter和getter方法 } public class Client { public static void main(String[] args) { // 获取享元工厂 GoFactory factory = GoFactory.getInstance(); // 获取三颗黑色棋子 AlphaGo black01 = factory.getGo("b"); AlphaGo black02 = factory.getGo("b"); AlphaGo black03 = factory.getGo("b"); // 获取两颗白色棋子 AlphaGo white01 = factory.getGo("w"); AlphaGo white02 = factory.getGo("w"); System.out.println("黑棋是否 相同:" + (black01 == black02)); System.out.println("白棋是否 相同:" + (white01 == white02)); // 显示棋子位置 black01.display(new Coordinates(2, 3)); black02.display(new Coordinates(4, 5)); black03.display(new Coordinates(3, 7)); white01.display(new Coordinates(2, 4)); white02.display(new Coordinates(3, 5)); } }
程序输出结果如下:
黑棋是否 相同:true 白棋是否 相同:true 棋子颜色:黑色,棋子位置:2 3 棋子颜色:黑色,棋子位置:4 5 棋子颜色:黑色,棋子位置:3 7 棋子颜色:白色,棋子位置:2 4 棋子颜色:白色,棋子位置:3 5
模式分析
共享模式类图如下:
- Flyweight抽象享元类:有时也定义为一个接口的形式,在该类中声明了具体享元类公共方法,这些方法向外部提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- ConcreteFlyweight具体享元类:它实现了抽象享元类,其实例被称为享元对象;在具体享元类中会为内部状态提供存储空间。一般会将具体享元类设计为单例模式或者借助于工厂模式创建,为每一个具体享元类提供唯一的享元对象。
- UnsharedConcreteFlyweight非共享具体享元类:并不是所有抽象享元类的子类都是共享的,这些不需要共享的子类就被称为非共享具体享元类。当需要一个非共享的享元类对象时,可以直接通过实例化创建,一般以方法的入参传入。
- FlyweightFactory享元工厂类:该类负责创建并管理享元对象,一般结合工厂模式设计该类。
如果具体享元类与非具体享元类差异性比较大,那么这两种类在抽象时大可不必继承自同一个抽象享元类。可以将抽象享元类仅设计成具体享元类的公共方法即可,而非具体享元类可以直接定为一个JavaBean即可。
外部在创建ConcreteFlyweight对象,不可以通过该类直接创建,一定要通过FlyweightFactory获取到ConcreteFlyweight对象。
一般在介绍享元类,一定会牵扯到两个状态:内部状态(Intrinsic State)和外部状态(Extrinsic State),内部状态可以理解为就是享元类的成员变量或者方法,而外部状态可以理解为享元类中方法的入参。
内部状态(Intrinsic State)就是存储在享元对象内部,不会随着环境改变而改变的状态,因此内部状态是可以共享的。比如围棋棋子白色,不管在哪个位置,白色棋子的颜色属性都不会改变。
外部状态(Extrinsic State)会随着环境改变而改变,这些状态是不可以共享的。享元对象的外部状态不会虽然享元对象的创建而创建,它只有在使用的时候才会传入享元对象内部,外部状态之间是相互独立的。比如围棋的位置信息,不同的围棋对应的位置一定是不同的。
模式优点
可以有效极少系统内存占用,使得相同或者相似的对象在内存中只保存一份,节省系统资源,提高系统性能。这里需要注意一点,有许多文章描述说可以有效减少内存中对象个数,其实这个说法相当不严谨,细粒度化对象,反而可能增加对象数量,但是相对的,对象总的内存占用会降低。
享元对象的外部状态相对独立,且不会影响内存状态。
模式缺点
享元模式可能使得系统变复杂,因为需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
享元模式将享元对象的状态外部化(指外部状态),而读取外部状态使得程序运行时间很长(比如选中一个棋子对象,就再也不能通过该对象知道它所在的位置了)。
与其它模式关系
享元模式中享元工厂是一个特殊的工厂方法模式,之所以说它特殊,就是因为这里维护的是一个创建过的产品对象的记录,并根据这个记录和内部状态循环使用这些产品对象。
享元工厂一般设计为单例模式,由于只有一个系列的享元,因此只需要一个实例的享元工厂即可。
与对象池关系
有些开发者可能会将享元模式与对象池混淆在一起,虽然在享元模式中也有池的概念,但是即使享元池跟对象池也是由本质区别的。这里我们拿线程池做对比了解一下,享元池其实就是FlyweightFactory,这个类是为了创建和管理不同享元对象的,不同享元对象在享元池也仅存在一个,但是线程池则不一样,线程池为了避免频繁创建和销毁线程对象浪费系统资源的,在线程池中同种类型的对象存在多个,所以线程池才是标准意义上的对象池,当外部需要使用线程对象时,直接从线程池中取出一个即可。
享元模式是有效减小内存使用量的一种模式,间接上可能会提供系统性能,而对象池设计本身就是为了提升系统性能而设计的。
在享元模式和对象池的根本出发点也不一样,享元模式的根本出发点是为了共享细化对象,以减少对象在内存中的占用。对象池是为了复用单个对象,在线程池中,当取出的某个线程对象用完之后,可以再放入线程池,待后续可以继续使用。享元模式是为了共享,而对象池是为了复用。
小结
当在开发中需要创建大量相似对象,享元模式可以有效降低系统内存占用,但是它的使用频率却不怎么高。另外想要真正理解享元模式,还需要对Java对象的创建和内存分配有一定了解,否则很难理解享元模式是如何降低内存使用的。
理解享元模式需要知道分离与共享的概念,分离是为了将变化的状态分离出来,作为单独的对象使用;共享是为了将共性的状态独立出来,以便大家都可以使用到。
示例源代码下载,提取码:ehl3