定义
通过共享对象的内部状态,减少相似对象的创建。
背景
假设,12306的火车票查询系统是我们开发的,它的主要功能是向乘客显示车票(Ticket)信息,车票中包含车次、出发地、目的地、票价、姓名等信息。
下面的伪代码表示查询不同车次下每一位乘客的车票信息。
其中外层for循环表示不同的车次,内层for循环表示该车次的乘客,ticket.print()表示向乘客显示车票信息。
public class Client {
public static void main(String[] args) {
for (int trainNumber = 0; trainNumber < 10; trainNumber++) {
for (int i = 0; i < 100; i++) {
Ticket ticket = new Ticket(trainNumber,to,price,userName,seatOrder);
ticket.print();
}
}
}
}
上线后一切正常,但是在临近春节时,一件出乎我们意料的事发生了:"海"量用户通过APP、取票机、检票机都无法查询车票信息。(拉去祭天吧!)
问题
首先,我们通过监控系统发现内存使用量在蹭❗蹭❗蹭❗地往上涨。(心跳加速!)
于是,我们初步断定这不是代码的问题而是内存不足的问题。(想甩锅?)
接着,通过日志文件我们又发现大量Ticket数据高度重复:相同车次的Ticket对象的起点、终点、票价都是一样的,不一样的仅仅是姓名。(离死不远了!)
最终,我们定位到了原因:海量相似的Ticket对象耗尽了内存空间。(死得其所!)
即使上天不给我再来一次的机会,我也会选择享原模式——通过共享减少对象的创建数量。
方案
假如,我们已经创建了一个Ticket对象,现在要创建与之相同车次的另一个Ticket对象;
那么,享原模式的方式是复用前一个Ticket对象中相同的数据(车次,出发地、目的地),更改不同的数据(姓名、座次)来实现另一个Ticket对象的创建。
从程序的角度看运行时创建了两个不一样的对象,但从内存角度看只创建了一个对象,另一个对象是通过共享前一对象的部分数据来实现的。
在享原模式中,对象中可共享的数据被称为内部状态,它通常是不可变的且需要被缓存的部分;与之相反,对象中不可共享的数据被称为外部状态,它通常是可变的且由客户端传入对象。
结构
抽象享原角色(Flyweight): 是一个接口或抽象类,它负责定义外部状态并将它作为方法的入参。
具体享原角色(ConcreteFlyweight):抽象享原角色的实现类,它负责定义内部状态并为其提供存储空间以及保证其不可变。
享原工厂角色(FlyweightFactory):通常是一个简单工厂类,它负责创建具体享原角色并将其缓存在HashMap中。
客户端(Client):它通过享原工厂创建对象,但并不知道创建的对象是否是一个共享对象。
//抽象享原角色
public interface Flyweight {
//参数化外部状态
public void operation(String extrinsic);
}
//具体享原角色
public class ConcreteFlyweight implements Flyweight{
//存储内部状态
protected String intrinsic;
public ConcreteFlyweight(String intrinsic){
this.intrinsic=intrinsic;
}
@Override
public void operation(String extrinsic) {
System.out.println("不变的内部状态:"+intrinsic+",可变的外部状态:"+extrinsic);
}
}
//简单工厂类
public class FlyweightFactory {
protected static HashMap<String,Flyweight> pool = new HashMap<>();
public static Flyweight getInstance(String key){
//复用已经存在的对象
Flyweight flyweight = pool.get(key);
if(flyweight==null){
flyweight = new ConcreteFlyweight("intrinsic");
//缓存新创建对象
pool.put(key,flyweight);
}
return flyweight;
}
}
public class Client {
public static void main(String[] args) {
List<String> types = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
Flyweight flyweightType1 = FlyweightFactory.getInstance(types.get(random()));
flyweightType1.operation("extrinsic"+i);
}
}
}
应用
接下来,我们使用享原模式重构一下车票查询系统,让它减少相似对象的创建提高内存的利用率。
首先,在ITicket接口中声明依赖外部状态的方法。
public interface ITicket {
//每个车次的姓名和座位都是不一样的
public void print(String userName,String seatOrder);
}
然后,创建Ticket并实现ITicket接口,它只负责存储内部状态。
public class Ticket implements ITicket{
//起点
protected String from;
//终点
protected String to;
//座次
protected String seatOrder;
//车次
protected String trainNumber;
public Ticket(String trainNumber,String from, String to){
this.trainNumber=trainNumber;
this.from=from;
this.to=to;
}
@Override
public void print(String userName,String seatOrder) {
System.out.println("from:"+from+",to:"+to+",seatOrder:"+seatOrder+",userName:"+userName);
}
}
现在,创建TicketFactory,根据车次缓存Ticket对象。
public class TicketFactory {
protected static HashMap<String,ITicket> sharedPart= new HashMap<>();
public static ITicket getTicket(String trainNumber){
ITicket ticket = sharedPart.get(trainNumber);
if(ticket==null){
ticket = new Ticket("trainNumber","from","to");
sharedPart.put(trainNumber,ticket);
}
return ticket;
}
}
最后,我们在看看客户端如何使用简单工厂复用共享的Ticket对象。
public class Client {
public static void main(String[] args) {
for (int trainNumber = 0; trainNumber < 10; trainNumber++) {
for (int i = 0; i < 100; i++) {
//原来的处理方式
//Ticket ticket = new Ticket(trainNumber,from,to,seatOrder,userName);
//ticket.print();
//现在的处理方式将共同的属性和特殊的属性分离
ITicket ticket = TicketFactory.getTicket(trainNumber);
ticket.print(userName,seatOrder);
}
}
}
}
总结
我在很多享原模式的文章中,都看到过这样一种观点:String常量池、数据库连接池、缓冲池等池化技术都使用了享原模式。
其实,这种认识是有误的,因为他们混淆了完全复用对象和部分复用对象的差异。如:String常量,它复用的条件是字符和常量池中的字符完全一致;而享原模式只是复用对象的部分属性,而且还要参数化外部状态。
所以,不能将两者等同看待,应该区分他们之间的差异。