设计模式之享元模式

1 引子

黑白五子棋或者围棋只有两种颜色-黑白,如果我们把棋子作为一个抽象类Chess,黑棋BlackChess和白棋WhiteChess分别作为继承抽象类的具体类,那么每下一步都需要new一个新的棋子对象,如此下来会产生大量的黑白棋对象。仔细观察黑白棋,不难发现黑白棋对象其实都一样,唯一不同的是其位置的变化。那么是否有一种方法可以实现这样的效果:不用创建大量的黑白棋对象,但是也能准确的实现其位置的变化?答案是:有的。
抽象棋子类Chess:

public abstract class Chess {
	private String type;  // 棋子种类,分黑两种
	public Chess(String type){
		this.type = type;
	}
	public String getType(){
		return type;
	}
	// 对棋子的操作
	public abstract void operation(Local local);
}

黑白棋子类BlackWhiteChess:

public class BlackWhiteChess extends Chess {
	public BlackWhiteChess(String type) {
		super(type);
	}
	@Override
	public void operation(Local local) {  // Local表示位置类
		System.out.println(getType() + " " + local);
	}
}

位置类Local:

public class Local {
	private int x;
	private int y;
	public Local(int x, int y){
		this.x = x;
		this.y = y;
	}
	// 省略get和set方法
	
	@Override
	public String toString() {
		return "横:" + x + ", 纵:" + y ;
	}
}

棋子工厂类:

public class ChessFactory {
	private static ChessFactory chessFactory;
	public ConcurrentMap<String, Chess> flyWeightMap; // 共享池
	private ChessFactory(){
		flyWeightMap = new ConcurrentHashMap<String, Chess>();
	}
	// 双重锁单例模式获取棋子工厂类
	public static ChessFactory getChessFactory() {
		synchronized (ChessFactory.class) {
			if (chessFactory == null){
				synchronized (ChessFactory.class) {
					chessFactory = new ChessFactory();
				}
			}
		}
		return chessFactory;
	}
	// 棋子工厂根据color构造不同的对象,并放入共享池
	public Chess getChess(String color){
		if (!flyWeightMap.containsKey(color)) {
			flyWeightMap.put(color, new BlackWhiteChess(color));
		}
		return flyWeightMap.get(color);
	}
}

客户端类Client:

public class Client {
	public static void main(String[] args) {
		String black = "black";
		String white = "white";
		ChessFactory chessFactory = ChessFactory.getChessFactory(); // 共享池
		Chess chess, _chess, chess2, _chess2;  // 棋子
		
		chess = chessFactory.getChess(black); // 黑棋
		_chess = chessFactory.getChess(black); // 黑棋
		chess.operation(new Local(5, 5));
		_chess.operation(new Local(5, 6));
		System.out.println(chessFactory.flyWeightMap.size());
		
		chess2 = chessFactory.getChess(white); // 白棋
		_chess2 = chessFactory.getChess(white); // 白棋
		chess2.operation(new Local(6, 6));
		_chess2.operation(new Local(7, 7));
		System.out.println(chessFactory.flyWeightMap.size());
	}
}

运行结果:

black 横:5, 纵:5
black 横:5, 纵:6
1
white 横:6, 纵:6
white 横:7, 纵:7
2

结果显示黑白棋子虽然有4颗,但共享池中的实际对象只有2个,即一个黑棋对象和一个白棋对象。实例代码的UML类图为:
设计模式之享元模式
这里的关键在于ChessFactory中的getChess(String)方法。根据方法参数color不同构造不同对象,并把对象放入线程安全的共享池,客户端需要的时候直接从共享池中获取即可,达到对象共享的目的;另外,把位置信息作为方法参数传入对象,达到不同状态的目的。这种共享元对象的代码结构称为享元模式

2 享元模式的原理

《大话设计模式》中这样定义享元模式:运用共享技术有效的支持大量细粒度的对象
问题1:共享技术是指什么?细粒度的对象是什么?
共享技术是一种通过共享池避免重复生成大量的相似对象的技术。
细粒度的对象是指实现一个接口的类的对象,使用过程中会大量产生的对象,对象之间有大量重复的不可变的信息(类型),只有少量可变的信息(位置)。
问题2:如何支持(实现)细粒度的对象共享?
在享元模式中,把对象内部不可变的信息(如类型)称为内部状态,把少量可变的信息(如位置)提取到对象外部,如客户端来决定可变信息,这种可变信息称为外部状态。所以,共享是根据内部状态把对象放入共享池内,客户端传入外部状态和内部状态来获取共享池内的对象。
享元模式的UML类图:
设计模式之享元模式
享元模式各角色介绍:
Flyweight:所有具体享元类的抽象类或者接口,通过这个接口,Flyweight可以接受并作用于外部状态。
ConcreteFlyweight:继承Flyweight抽象类或者接口,并为内部状态增加存储空间;
UnsharedConcreteFlyweight:继承Flyweight抽象类或者接口,指那些不需要共享的Flyweight子类。因为Flyweight接口共享成为可能,但他并不强制共享。
FlyweightFactory:一个享元工厂,用来创建并管理Flyweight对象,它主要是用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。

PS:实例中并没有UnsharedConcreteFlyweight类,非共享的类根据实际情况来定。

3 享元模式的特点

避免重复创建对象,节省内存空间。根据内部状态把对象存储在共享池,需要时去共享池取就行。

4 享元模式使用场景

借用设计模式书中的话就是:如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。

5 参考资料

《大话设计模式》
https://www.cnblogs.com/lfxiao/p/6817141.html
https://blog.csdn.net/nobody_1/article/details/85415892