享元模式(Flyweight)
定义
1. 使用共享技术有效的支持大量细粒度的对象。
2. 享元对象能做到共享的关键是区分了内部状态和外部状态:
结构图
- FlyweightFactory(享元工厂类):创建并管理享元对象,享元池一般设计为键值对。主要用来确保合理的共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory提供一个Flyweight(不存在时创建一个)。
- Flyweight(抽象享元类):通常是一个接口或者抽象类,是所有享元类的超类或接口。申明公共方法,这些方法可以向外界提供对象的内部状态,设置外部状态。
- ConcreteFlyweight(具体享元类):实现或者继承Flyweight,为内部状态提供成员变量进行存储。
- UnsharedConcreteFlyweight(非共享享元类):不能被共享的子类可设置成非共享的享元类。Flyweight接口共享成为可能,但是并不强制共享。
- 内部状态:可以共享,不会随着环境变化而改变。
- 外部状态:不可以共享,会随着环境变化而改变。
享元模式可以避免大量相似类的开销。在程序设计时,有时候需要生成大量细粒度的类实例来表示数据,如果能发现这些实例除了几个参数外其他基本都是相同的,有时就能够大幅度的减少需要实例化的类的数量。如果将那些不同的参数移到类实例的外面,在方法调用的时候将他们传递进来,就可以通过共享大幅度的减少单个实例的数量。也就是说,享元模式Flyweight执行时所需要的状态是有内部的,也有可能是外部的,内部状态存储于ConcreteFlyweight中,而外部对象则应该考虑由客户端存储或计算,当调用Flyweight时,将该状态传递给它。
围棋示例
理论上,一盘围棋可以有361个空位可以放置棋子,如果用常规的面向对象编程,每盘棋都有可能有两三百个对象产生,一台服务器就很难支撑多个玩家同时玩游戏了,毕竟内存空间还是有限的。那么用享元模式来处理呢,每个围棋都是一个对象,并且有如下属性:颜色、形状、大小、位置,然而颜色、形状、大小等属性可能是一样的,可以将它们归属于内部状态,可以共享;而位置是实时变化的,将它归属于外部状态,不可共享。这样,就可以将围棋对象减少到两个对象实例了:即黑色、白色,位置用一个外部状态来控制。
- 先创建外部可变状态UnsharedConcreteFlyweight,位置坐标类
/**
* User:tumbler
* Desc:享元模式--非共享状态UnsharedConcreteFlyweight
* 棋子位置坐标类
*/
public class Coordinate {
int x, y;
public Coordinate(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;
}
}
- 创建享元接口Flyweight
/**
* User:tumbler
* Desc:享元模式--享元接口Flyweight
* 棋子享元,以颜色为共享内部状态为例
*/
public interface ChessFlyweight {
/**
* 获取颜色
* @return
*/
String getColor();
/**
* 显示位置,将外部状态作为参数进行传递
* @param c
*/
void display(Coordinate c);
}
- 具体享元类ConcreteFlyweight
/**
* User:tumbler
* Desc:享元模式--具体享元类
*/
public class ConcreteFlyweight implements ChessFlyweight {
//内部状态:颜色
private String color;
public ConcreteFlyweight(String color) {
this.color = color;
}
@Override
public String getColor() {
return this.color;
}
@Override
public void display(Coordinate c) {
System.out.println("棋子颜色:" + this.color + "----位置:<" + c.getX() + "," + c.getY() + ">");
}
}
- 享元工厂FlyweightFactory,通过颜色来获取具体享元类对象
/**
* User:tumbler
* Desc:享元模式--享元工厂
* 提供享元池存储享元对象,并提供方法返回对象
*/
public class FlyweightFactory {
//享元池
private static Map<String, ChessFlyweight> map = new HashMap<>();
/**
* 判断:如果已经存在需要的对象,则返回;不存在则创建对象,并将其添加到享元池中,以便下次直接获取,并返回
* @param color
* @return 享元对象
*/
public static ChessFlyweight getFlyweight(String color) {
if (map.get(color) != null) {
return map.get(color);
}else {
ChessFlyweight flyweight = new ConcreteFlyweight(color);
map.put(color, flyweight);
return flyweight;
}
}
}
- 客户端测试Client
/**
* User:tumbler
* Desc:享元模式--客户端测试
*/
public class Client {
public static void main(String[] ars) {
//首先获得两枚棋子:此时两个对象地址应该是同一个
ChessFlyweight chess1 = FlyweightFactory.getFlyweight("黑色");
ChessFlyweight chess2 = FlyweightFactory.getFlyweight("黑色");
System.out.println(chess1);
System.out.println(chess2);
//增加外部状态位置并显示
chess1.display(new Coordinate(10,10));
chess2.display(new Coordinate(15,20));
}
}
运行结果:同一个对象,位置不同。
src.com.tumbler.flyweight.ConcreteFlyweight@6d6f6e28
src.com.tumbler.flyweight.ConcreteFlyweight@6d6f6e28
棋子颜色:黑色----位置:<10,10>
棋子颜色:黑色----位置:<15,20>
- UML图:
开发应用场景
- 享元模式由于其共享的特性,可以在任何“池”中进行操作。如:线程池、数据库连接池。
- String类的设计使用了享元模式。
如果一个应用程序中使用了大量的对象,而这些大量的对象造成了很大的存储开销时就应该考虑使用。
对象的大多数状态是是外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。
优缺点
- 优点:
- 极大的减少了内存中对象的数量
- 形同或相似对象内存中只存一份,极大的节约资源,提高系统性能
- 外部状态相对独立,不影响内部状态
- 缺点:
- 模式较复杂,是程序逻辑复杂化
- 为了节省内存,共享了内部状态,分理处外部状态,而读取外部状态使运行时间变长。用时间换取了空间。