享元模式(Flyweight Pattern)
基本介绍
核心
以共享的方式高效地支持大量细粒度对象的重用。
介绍
- 享元模式(Flyweight Pattern) 也叫 蝇量模式。 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个。
- 享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率。
- “享”就表示共享,“元”表示对象。
关键
享元对象能做到共享的关键是区分了内部状态和外部状态。
- 内部状态:存储在享元对象内部,可以共享,不会随环境变化而改变。
- 外部状态:对象得以依赖的一个标记,不可以共享,会随环境变化而改变。
举例:
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一点,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态。
享元模式的UML类图
角色分析
FlyweightFactory享元工厂类
创建池容器并管理享元对象, 同时提供从池中获取对象方法,享元池一般设计成键值对
FlyWeight抽象享元类
抽象的享元角色, 他是产品的抽象类(或接口), 同时定义出对象的外部状态和内部状态的接口或实现。
ConcreteFlyWeight具体享元类
具体的享元角色,为内部状态提供成员变量进行存储,是具体的产品类,实现抽象角色定义相关业务。
UnsharedConcreteFlyWeight非共享享元类
不可共享的角色,一般不会出现在享元工厂。
享元模式开发中应用的场景
- 享元模式由于其共享的特性,可以在任何“池”中操作,比如:线程池、数据库连接池。
- String类的设计也是享元模式
实例分析
围棋软件设计
围棋软件设计,主要考虑棋子的颜色和下棋的位置
传统方式实现
如果采用传统方式来实现,对每一步下的棋子,传入颜色和坐标集合。
代码实现
围棋
public class Chess {
private String color;
private int x,y;
public Chess ( String color, int x, int y ) {
this.color = color;
this.x = x;
this.y = y;
}
public String getColor () {
return color;
}
public void setColor ( String color ) {
this.color = color;
}
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;
}
}
客户端调用
public class Client {
public static void main ( String[] args ) {
//每一步下棋实例化一个对象
Chess chess1=new Chess("白色",52,32);
Chess chess2=new Chess("黑色",15,64);
}
}
分析
问题
棋字的结构相似,拥有颜色、横坐标、纵坐标三个属性。但每一步棋都会实例化一个对象,棋字多了后,实力对象很多,造成服务器的资源浪费。
改进
- 整合棋字类,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源。
- 对于代码来说,由于是一份实例,维护和扩展都更加容易
- 可以使用享元模式 来解决
享元模式实现
思路
围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好的解决了对象的开销问题。
内部状态和外部状态
内部状态:颜色,只有两种状态(白色和黑色),不随环境改变
外部状态:位置,棋字的位置随下棋者的思维动态发生改变
UML类图
代码实现
FlyWeight抽象享元类
public interface ChessFlyWeight {
void setColor(String c);
String getColor();
/** 下棋 */
void display(Coordinate c);
}
ConcreteFlyWeight具体享元类
public class ConcreteChess implements ChessFlyWeight {
private String color;
public ConcreteChess(String color) {
super();
this.color = color;
}
@Override
public void display(Coordinate c) {
System.out.println("棋子颜色:"+color);
System.out.println("棋子位置:"+c.getX()+"----"+c.getY());
}
@Override
public String getColor() {
return color;
}
@Override
public void setColor(String c) {
this.color = c;
}
}
UnsharedConcreteFlyWeight非共享享元类
public class Coordinate {
private int x,y;
public Coordinate(int x, int y) {
super();
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;
}
}
FlyweightFactory享元工厂类
public class ChessFlyWeightFactory {
//享元池
private static Map<String,ChessFlyWeight> map = new HashMap<>();
public static ChessFlyWeight getChess(String color){
if(map.get(color)!=null){
return map.get(color);
}else{
ChessFlyWeight cfw = new ConcreteChess(color);
map.put(color, cfw);
return cfw;
}
}
}
客户端调用
public class Client {
public static void main(String[] args) {
ChessFlyWeight chess1 = ChessFlyWeightFactory.getChess("黑色");
ChessFlyWeight chess2 = ChessFlyWeightFactory.getChess("黑色");
ChessFlyWeight chess3 = ChessFlyWeightFactory.getChess("白色");
System.out.println(chess1);
System.out.println(chess2);
System.out.println("增加外部状态的处理===========");
chess1.display(new Coordinate(10, 10));
chess2.display(new Coordinate(20, 20));
}
}
结果
com.atguigu.flyweight.test.improve.ConcreteChess@2503dbd3
com.atguigu.flyweight.test.improve.ConcreteChess@2503dbd3
com.atguigu.flyweight.test.improve.ConcreteChess@4b67cf4d
增加外部状态的处理===========
棋子颜色:黑色
棋子位置:10----10
棋子颜色:黑色
?棋子位置:20----20
由此得知,当创建同一个颜色的棋字为一个对象,所以整个系统,棋字对象只有两个(黑色棋字、白色棋字)。
享元模式的注意事项和细节
- 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
- 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable 存储
- 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
- 享元模式经典的应用场景是需要缓冲池的场景,比如 String 常量池、数据库连接池
优点
- 享元模式大大减少了对象的创建。
- 相同或相似对象内存中只存一份,极大的节约资源,提高系统性能。
- 外部状态相对独立,不影响内部状态。
缺点
- 需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方。
- 模式较复杂,使程序逻辑复杂化。
- 为了节省内存,共享了内部状态,分离出外部状态,而读取外部状态。
使运行时间变长。用时间换取了空间。