设计模式学习 - 享元模式

馋奶兔 提交于 2019-12-09 14:03:04

如果一个系统在运行时所创建的相同或相似对象数量太多,将导致运行代价过高,带来的系统资源浪费、性能下降等问题。享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态和外部状态。

内部状态

内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部可以是共享。例如字符的内容不会随外部环境变化而变化,无论在任何环境下字符‘a’始终是'a',不会变成'b'。

外部状态

外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在襄垣对象被创建之后需要使用的时候再传入到享元对象内部。一个外部状态和另一个外部状态之间是独立的。例如字符'a'的颜色在不同地方可以有不同颜色,也可以有不同的大小。字符的颜色和大小是两个独立的外部状态,可以独立变化,互不影响。

 

定义

运用享元技术有效地支持大量细粒度对象的复用。

结构

Flyweight(抽象享元类)

通常是一个接口或抽象类,声明了具体享元公共的方法,这些方法可以向外界提供享元对象内部数据(内部状态),同时也可以通过这些方法设置外部数据(外部状态)。

ConcreteFlyweight(具体享元类)

实现了抽象享元类,其实例称为享元对象。在具体享元类中为内部状态提供了存储空间。通常可以结合单例模式来设计具体享元类,为每个具体享元类提供唯一的享元对象。

UnsharedConcreteFlyweight(非共享具体享元类)

并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可以被设计为非共享具体享元类,当需要一个非共享具体享元类的对象时可以直接通过实例化创建。

FlyweightFactory(享元工厂类)

用于创建并管理享元对象,针对享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设置为键值对的集合,可以结合工厂模式进行设计。当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例,返回新的实例并将这个实例存储在享元池中。

 

举例说明

围棋,只有黑色和白色的棋子,但每一个棋子的位置不同。黑色和白色的棋子如果作为独立对象存储在内存中,那么运行内存消耗较大。通过享元模式,解决这个问题。经分析发现,颜色是内部状态,坐标则是外部状态,

1.辅助类 - 即外部状态(坐标),非共享

// 辅助类 - 坐标类
public class Coordinates {
	private int x;
	private int y;
	
	public Coordinates(int x, int y) {
		this.x = x;
		this.y = y;
	}
	
	public int getX() {
		return x;
	}
	
	public void setX(int x) {
		this.x = x;
	}
	
	public int getY() {
		return y;
	}
	
	public void setY(int y) {
		this.y = y;
	}
	
}

2.抽象享元类

// 抽象享元类 - 围棋棋子类
public abstract class IgoChessman {
	// 获取内部状态 - 围棋颜色
	public abstract String getColor();
	
	// 业务方法 - 显示棋子颜色
	public void display(Coordinates coordinates) {
		System.out.println("棋子颜色:" + this.getColor() + ",坐标:(" + coordinates.getX() + "," + coordinates.getY() + ")");
	}
}

3.具体享元类

// 具体享元类 - 黑色棋子
public class BlackIgoChessman extends IgoChessman {

	@Override
	public String getColor() {
		return "黑色";
	}
	
}
//具体享元类 - 白色棋子
public class WhiteIgoChessman extends IgoChessman {

	@Override
	public String getColor() {
		return "白色";
	}

}

4.享元工厂类

// 享元工厂类 - 单例
public class IgoChessmanFactory {
	private static IgoChessmanFactory instance = new IgoChessmanFactory();
	private static HashMap<String, IgoChessman> hm;
	
	private IgoChessmanFactory() {
		hm = new HashMap<>();
		IgoChessman black,white;
		black = new BlackIgoChessman();
		white = new WhiteIgoChessman();
		hm.put("black", black);
		hm.put("white", white);
	}
	
	public static IgoChessmanFactory getInstance() {
		return instance;
	}
	
	public static IgoChessman getIgoChessman(String color) {
		return (IgoChessman) hm.get(color);
	}
	
}

5.测试

public class Client {
	public static void main(String[] args) {
		IgoChessman black1,black2,black3,white1,white2,white3;
		IgoChessmanFactory factory;
		
		factory = IgoChessmanFactory.getInstance();
		
		black1 = factory.getIgoChessman("black");
		black2 = factory.getIgoChessman("black");
		black3 = factory.getIgoChessman("black");
		System.out.println(black1 == black2);
		System.out.println(black2 == black3);
		
		white1 = factory.getIgoChessman("white");
		white2 = factory.getIgoChessman("white");
		white3 = factory.getIgoChessman("white");
		System.out.println(white1 == white2);
		System.out.println(white2 == white3);
		
		black1.display(new Coordinates(1, 2));
		black2.display(new Coordinates(3, 4));
		black3.display(new Coordinates(5, 6));
		
		white1.display(new Coordinates(2, 1));
		white2.display(new Coordinates(4, 3));
		white3.display(new Coordinates(6, 5));
	}
}

6.运行截图

 

单纯享元模式与复合享元模式

在单纯享元模式中所有的具体享元类都是可以共享的,不存在非共享具体享元类。

将一些单纯享元对象使用组合模式加以组合还也可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者可以共享。

 

享元模式与String类

String使用了享元模式。

public class Test {
	public static void main(String[] args) {
		String str1 = "abcd";
		String str2 = "abcd";
		String str3 = "ab" + "cd";
		String str4 = "ab";
		str4 += "cd";
		
		System.out.println(str1 == str2);
		System.out.println(str1 == str3);
		System.out.println("-----");
		System.out.println(str1 == str4);
		System.out.println("-----");
		str2 += "e";
		System.out.println(str1 == str2);
	}
}

可以看出,前两个输出结果都为true,说明str1,str2,str3引用的是相同对象。str4因为初始值是ab,所以重新分配了内存,再对它进行str4 += "cd",又将"ab"这个对象复制一份,在新对象上进行修改,所以第3个结果输出为false。实际,str4在修改之前和修改之后也是两个不同的内存空间(hashCode不同)。

         

最后str2 += "e",说明对str2进行修改时创建了一个新的对象,修改工作在新的对象上完成,而原来的引用对象并没发生改变,str1仍应用原对象,str2引用新对象。

String类这种在修改享元对象时先将原有对象复制一份,然后在新对象上实施修改的操作的机制称为"Copy on Write"。

 

优点

1.减少内存中对象的数量,节约系统资源,提高系统性能。

2.外部状态相对独立,不会影响内部状态,使得享元对象可以在不同环境下被共享。

缺点

1.使系统变复杂,需要分离出内部状态和外部状态,使得程序逻辑复杂化。

2.为了使得对象可以共享,需要将享元对象的部分状态外部化,而读取外部状态会使运行时间变长。

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!