12-五子棋游戏:享元模式
12.1 五子棋游戏
本章以五子棋游戏为例,来学习享元模式。
12.2 模式定义
享元模式(Flyweight Pattern),以共享的方式高效地支持大量的细粒度对象。通过复用内存中已经存在的对象,降低系统创建对象实例的性能消耗。享元的英文是Flyweight,它是一个来自于体育方面的专业术语,在拳击、摔跤和举重比赛中特指最轻量的级别。把这个单词移植到软件工程里,也是用来表示告别小的对象,即细粒度对象。至于为什么把Flyweight翻译为“享元”,可以理解为共享元对象,也就是共享细粒度对象。
在面向对象的眼中,万事万物一切皆对象。但是不可避免的是,采用面向对象的编程方式,可能会增加一些资源和性能上的开销。不过,在大多数情况下,这种影响还不是太大,所以,它带来的空间和性能上的损耗相对于它的优点而言,基本上不用考虑。但是,在某些特殊情况下,大量细粒度对象的创建、销毁及存储所造成的资源和性能上的损耗,可能会在系统运行时形成瓶颈。那么该如何避免产生大量的细粒度对象,同时又不影响系统使用面向对象的方式进行操作呢?享元模式提供了一个比较好的解决方案。
12.3 模式分析
下面,我们来分析上面的五子棋游戏。
1)需要一个抽象棋子类,黑子和白子继承该类;
2)需要一个获得棋子的工厂,用来获得棋子对象,并且缓存棋子对象实例,不至于每次都是new一个棋子出来。
享元模式的要义就在于“避免产生大量的细粒度对象”。
在棋子工厂的缓存中,如果存在棋子对象内容,则使用缓存当中的对象实例;如果不存在,则创建一个新的棋子对象,缓存之后返回。一般情况下,我们把工厂设置为单例模式,使用HashTable作为工厂的缓存结构,缓存中以“B”和“W”字母作为key,value中则存储“黑子”和“白子”对象实例。
12.4 模式实现
12.4.1 创建抽象棋子
package com.demo.flyweight.factory; /** * Created by lsq on 2018/3/20. * 抽象棋子类 */ public abstract class AbstractChessman { //棋子类别(黑|白) protected String chess; public AbstractChessman(String chess){ this.chess = chess; } //显示棋子信息 public void show(){ System.out.println(this.chess); } }
12.4.2 棋子实现
1. 黑子实现——BlackChessman
package com.demo.flyweight.factory; /** * Created by lsq on 2018/3/20. * 黑子实现类 */ public class BlackChessman extends AbstractChessman{ /** * 构造方法:初始化黑棋子 */ public BlackChessman() { super("○"); System.out.println("---BlackChessman Constructor Execute!"); } }
2. 白子实现——WhiteChessman
package com.demo.flyweight.factory; /** * Created by lsq on 2018/3/20. * 白子实现类 */ public class WhiteChessman extends AbstractChessman{ /** * 构造方法,初始化白棋子 */ public WhiteChessman() { super("●"); System.out.println("---WhiteChessman Constructor Execute!"); } }
12.4.3 创建棋子工厂
package com.demo.flyweight.factory; import java.util.Hashtable; /** * Created by lsq on 2018/3/20. * 棋子工厂 */ public class FiveChessmanFactory { //单例模式工厂 private static FiveChessmanFactory fiveChessmanFactory = new FiveChessmanFactory(); //缓存存放共享对象 private final Hashtable<Character, AbstractChessman> cache = new Hashtable<>(); //私有化构造方法 private FiveChessmanFactory(){} //获得单例工厂 public static FiveChessmanFactory getInstance(){ return fiveChessmanFactory; } /** * 根据字符获取棋子 * @param c (B:围棋,W:白棋) * @return */ public AbstractChessman getChessmanObject(char c){ //从缓存中获得棋子对象实例 AbstractChessman abstractChessman = this.cache.get(c); if (abstractChessman == null){ //缓存中没有棋子对象实例信息,则创建棋子对象实例,并放入缓存 switch (c){ case 'B': abstractChessman = new BlackChessman(); break; case 'W': abstractChessman = new WhiteChessman(); break; default: break; } //为防止非法字符的进入,返回null if (abstractChessman != null){ //放入缓存 this.cache.put(c, abstractChessman); } } return abstractChessman; } }
12.4.4 客户端测试
package com.demo.flyweight; import com.demo.flyweight.factory.AbstractChessman; import com.demo.flyweight.factory.FiveChessmanFactory; import java.util.Random; /** * Created by lsq on 2018/3/21. * 应用程序 */ public class Client { public static void main(String[] args) { //创建五子棋工厂 FiveChessmanFactory fiveChessmanFactory = FiveChessmanFactory.getInstance(); //随机数,用来随机生成棋子对象 Random random = new Random(); int radom = 0; AbstractChessman abstractChessman = null; //随机获得棋子 for (int i=0;i<10;i++){ radom = random.nextInt(2); switch (radom){ //获得黑棋 case 0: abstractChessman = fiveChessmanFactory.getChessmanObject('B'); break; //获得白棋 case 1: abstractChessman = fiveChessmanFactory.getChessmanObject('W'); break; } if (abstractChessman != null){ abstractChessman.show(); } } } }
运行结果:
从上面的运行结果可以看到,“黑子”和“白子”的构造方法分别只被执行了一次,其余的都是从缓存中获得的对象实例。
12.4.5 如何实现棋子的位置
我们的享元对象已经生效了,共享了元对象,节省了内存空间。然而,我们现在只做到了共享棋子对象实例,还有一点不要忘记,那就是棋子的位置。虽然棋子对象是可以共享的,但是,每一个棋子的位置都是不一样的,是不能共享的。这也就是享元模式的两种状态:内蕴状态(Internal State)和外蕴状态(External State)。
1)内蕴状态
享元对象的内蕴状态是不会随着环境的改变而改变的,在存储在享元对象内部的状态信息,因此内蕴状态是可以共享的,对于任何一个享元对象来讲,它的值是完全相同的。就像上面例子中的“黑子”和“白子”,它代表的状态就是内蕴状态。
2)外蕴状态
外蕴状态会随环境的改变而改变,因此是不可以共享的状态,对于不同的享元对象来讲,它的值可能是不同的。享元对象的外蕴状态必须由客户端保存,在享元对象被创建之后,需要使用的时候再传入到享元对象内部。就像五子棋的位置信息,代表的状态就是享元对象的外蕴状态。
享元模式的两种状态是相互独立的,彼此没有关联。
3)实现棋子的外蕴状态——位置
首先,我们需要声明的一点是,虽然外蕴状态的内容是会随着环境改变而改变的,但是外蕴状态变量还是需要的,我们需要在抽象棋子类中增加棋子位置信息,以及设置位置的方法内容。
下面,我们就在抽象棋子类中增加棋子位置信息x,y,同时增加设置棋子位置的方法point,在显示棋子信息的方法中增添棋子的位置信息。修改后的AbstractChessman抽象棋子类内容如下所示:
package com.demo.flyweight.object; /** * Created by lsq on 2018/3/20. * 抽象棋子类 */ public abstract class AbstractChessman { //棋子坐标 protected int x,y; //棋子类别(黑|白) protected String chess; public AbstractChessman(String chess){ this.chess = chess; } //点坐标设置 public abstract void point(int x, int y); //显示棋子信息 public void show(){ System.out.println(this.chess+"("+this.x+","+this.y+")"); } }
完美黑子类“BlackChessman”,实现父类抽象方法point,内容如下所示:
package com.demo.flyweight.object; /** * Created by lsq on 2018/3/20. * 黑子实现类 */ public class BlackChessman extends AbstractChessman { /** * 构造方法:初始化黑棋子 */ public BlackChessman() { super("○"); System.out.println("---BlackChessman Constructor Execute!"); } /** * 实现父类方法,设置位置信息 */ @Override public void point(int x, int y) { this.x = x; this.y = y; this.show(); } }
完美白子类“WhiteChessman”,实现父类抽象方法point。
package com.demo.flyweight.object; /** * Created by lsq on 2018/3/20. * 白子实现类 */ public class WhiteChessman extends AbstractChessman { /** * 构造方法,初始化白棋子 */ public WhiteChessman() { super("●"); System.out.println("---WhiteChessman Constructor Execute!"); } /** * 实现父类方法,设置位置信息 */ @Override public void point(int x, int y) { this.x = x; this.y = y; this.show(); } }
12.4.6 测试棋子的外蕴状态
package com.demo.flyweight; import com.demo.flyweight.object.AbstractChessman; import com.demo.flyweight.object.FiveChessmanFactory; import java.util.Random; /** * Created by lsq on 2018/3/21. * 应用程序 */ public class Client2 { public static void main(String[] args) { //创建五子棋工厂 FiveChessmanFactory fiveChessmanFactory = FiveChessmanFactory.getInstance(); //随机数,用来随机生成棋子对象 Random random = new Random(); int radom = 0; AbstractChessman abstractChessman = null; //随机获得棋子 for (int i=0;i<10;i++){ radom = random.nextInt(2); switch (radom){ //获得黑棋 case 0: abstractChessman = fiveChessmanFactory.getChessmanObject('B'); break; //获得白棋 case 1: abstractChessman = fiveChessmanFactory.getChessmanObject('W'); break; } if (abstractChessman != null){ //设置棋子位置信息(x:0~9,y:0~15的随机数产生) abstractChessman.point(i, random.nextInt(15)); } } } }
运行结果如下:
享元模式的重点在于共享元对象,降低内存的使用空间,提高系统性能。享元对象的外蕴状态是通过客户端来保存传入的,它是可能发生变化的。因此,在我们进行软件系统设计的时候,一定要区分享元对象的内蕴状态和外蕴状态,不能混淆,更不能互相关联,二者应是彼此分开的。
12.5 使用场合
1)当系统中某个对象类型的实例较多的时候;
2)在系统设计中,对象实例进行分类后,发现真正有区别的分类很少的时候。
扩展1:Java SDK中的享元模式
享元模式是系统中经常用到的,特别是对于细粒度对象比较多的软件系统,使用起来非常有效,不但可以提高系统内在空间,还可以提高系统效率。在JDK中也存在着享元模式的身影,如java.lang.Integer。java.lang.Integer类中的valueOf方法就是享元模式的具体应用。在该方法中,首先判断参数的范围,如果在缓存范围内,则返回缓存中的内容,否则创建一个新对象返回。而缓存类IntegerCache作为java.lang.Integer类的内部类,已经在类初始化的时候,设置了Integer数组缓存cache[]内容。