1.概述
享元模式(Flyweight Pattern)也叫蝇量模式,它是运用共享技术有效地支持大量细粒度的对象,解决重复对象浪费内存的问题。它常用于系统底层的开发,解决系统的性能问题。比如数据库连接池,这里面都是已经创建好的连接对象,需要时直接拿来使用,避免重复创建。享元模式最经典的使用场景就是池技术了,String常量池、数据库连接池等、缓冲池等都是享元模式的使用。本文将介绍享元模式的原理及使用方式。
2.原理及使用
2.1 原理
享元模式的类图如下所示:
类图中主要包含四个核心角色:
抽象的享元角色(FlyWeight): 它是产品的抽象类,同时定义出对象的内部状态(不会随着环境的改变而改变的可共享部分)和外部状态(随环境改变而改变的不可以共享的部分);
具体的享元角色(ConcreteFlyWeight):是具体的产品类,实现抽象角色定义的相关业务;
不可共享角色(UnsharedConcreteFlyWeight): 不能被共享的子类可设计成不可共享角色;
享元工厂类(FlyweightFactory):负责创建和管理享元角色,当使用者创建享元对象时,会由享元工厂先从工厂中获取,若存在则对外提供,不存在则由工厂创建。
2.2 案例
如果要开发一个在线斗地主游戏,已知一副牌有54张,如果每张牌都是一个单独的对象,也就是一桌需要存储54个对象,如果有1000、10000、1000000桌的话,需要存储的对象分别是100054,1000054,1000000*54,很明显这不合理。
通过享元模式来设计上述场景,如下:
抽象的享元角色就是Poker,它的内部分别包含了两个抽象方法,发牌方法push()和获取当前牌的状态;
享元角色就是ConcretePoker,它的内部有两个属性:牌的数字(number)和花色(color),分别有两个方法getNumber()、getColor()来获取当前牌的数字和花色;
不可共享角色就是UnsharedConcretePoker,内部有一个属性status(当前牌的状态:已出、未出),可能还存储张牌的用户信息userId等这些不可共享角色;
享元工厂就是PokerFactory,内部有一个pokers数组,用来存储享元角色ConcretePoker。
这样划分的好处是:整个系统内存中只存储了54张牌的信息,即使参与的人数再多,变更的也只是不可共享角色部分,极大地节约了内存资源。
编码如下:
public abstract class Poker {
abstract void push();
abstract boolean getStatus();
}
public class ConcretePoker extends Poker {
public ConcretePoker(String number, String color) {
this.number = number;
this.color = color;
}
private String number;
private String color;
public String getNumber() {
return number;
}
public String getColor() {
return color;
}
@Override
public String toString() {
return "ConcretePoker{" +
"number='" + number + '\'' +
", color='" + color + '\'' +
'}';
}
@Override
void push() {
throw new UnsupportedOperationException();
}
@Override
boolean getStatus() {
throw new UnsupportedOperationException();
}
}
public class UnsharedConcretePoker extends Poker {
public UnsharedConcretePoker(Long userId) {
this.userId = userId;
}
private Boolean status;
private Long userId;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public void setStatus(boolean status) {
this.status = status;
}
@Override
void push() {
System.out.println("牌已经被打出");
setStatus(true);
}
@Override
boolean getStatus() {
System.out.println("牌的状态是:" + status);
return status;
}
}
public class PokerFactory {
private HashMap<String, ConcretePoker> pokers = new HashMap<>();
private void addPoker(ConcretePoker poker) {
//以牌的number+":"+color来做key
String key = poker.getNumber() + ":" + poker.getColor();
pokers.put(key, poker);
System.out.println("添加牌:" + poker.toString() + "成功");
}
public ConcretePoker getPoker(String key) {
ConcretePoker concretePoker = pokers.get(key);
//双重验证,多线程安全
if (concretePoker == null) {
synchronized (PokerFactory.class) {
ConcretePoker concretePoker1 = pokers.get(key);
if (concretePoker1 == null) {
//创建ConcretePoker对象放入列表
String[] split = key.split(":");
ConcretePoker concretePoker2 = new ConcretePoker(split[0], split[1]);
pokers.put(key, concretePoker2);
System.out.println("工厂添加牌:" + concretePoker2.toString() + "成功");
return concretePoker2;
}
}
} else {
System.out.println("工厂已存在牌:" + concretePoker.toString());
}
return concretePoker;
}
}
//测试类
public class Test {
public static void main(String[] args) {
String[] strings = new String[]{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "Small", "Big"};
PokerFactory pokerFactory = new PokerFactory();
//Hearts:红桃,Spades:黑桃,Diamond:方片,Clubs:梅花
String[] colors = new String[]{"Hearts", "Spades", "Diamond", "Clubs"};
Random random = new Random();
for (int i = 0; i < 20; i++) {
int index = random.nextInt(strings.length);
String number = strings[index];
if (!number.equals("Small") && !number.equals("Big")) {
int colorIndex = random.nextInt(colors.length);
String color = colors[colorIndex];
String key = number + ":" + color;
ConcretePoker poker = pokerFactory.getPoker(key);
} else {
if (number.equals("Small")) {
String key = "Small:King";
ConcretePoker poker = pokerFactory.getPoker(key);
} else {
String key = "Big:King";
ConcretePoker poker = pokerFactory.getPoker(key);
}
}
}
}
}
运行结果如下:
2.3 享元模式的应用
享元模式在各种池的场景中使用较多,这里以Integer常量池为例,当值处于-128到127时,会从缓存池中获取,否则会生成新的对象指向所创建对象的地址。
3.小结
1.享元模式大大地减少了对象地创建,降低了程序内存的应用,提高了效率;
2.使用享元模式时,需要注意划分内部状态和外部状态,并且需要一个工厂类加以控制;
3.享元模式的使用场景是系统中存在大量对象,且这些对象消耗大量内存,这些对象的状态大部分可以外部化;
4.享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变。
4.参考文献
1.《设计模式-可复用面向对象软件的基础》-Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides
2.《可复用物联网Web3D框架的设计与实现》-程亮(知网)
3.https://www.bilibili.com/video/BV1G4411c7N4-尚硅谷设计模式
4.https://zhuanlan.zhihu.com/p/74872012