定義
通過共享對象的內(nèi)部狀態(tài),減少相似對象的創(chuàng)建稽荧。
背景
假設(shè)橘茉,12306的火車票查詢系統(tǒng)是我們開發(fā)的,它的主要功能是向乘客顯示車票(Ticket)信息姨丈,車票中包含車次畅卓、出發(fā)地、目的地蟋恬、票價翁潘、姓名等信息。
下面的偽代碼表示查詢不同車次下每一位乘客的車票信息歼争。
其中外層for循環(huán)表示不同的車次拜马,內(nèi)層for循環(huán)表示該車次的乘客渗勘,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();
}
}
}
}
上線后一切正常俩莽,但是在臨近春節(jié)時旺坠,一件出乎我們意料的事發(fā)生了:"海"量用戶通過APP、取票機(jī)豹绪、檢票機(jī)都無法查詢車票信息价淌。(拉去祭天吧I暄邸)
問題
首先瞒津,我們通過監(jiān)控系統(tǒng)發(fā)現(xiàn)內(nèi)存使用量在蹭?蹭?蹭?地往上漲。(心跳加速@ㄊ)
于是巷蚪,我們初步斷定這不是代碼的問題而是內(nèi)存不足的問題。(想甩鍋濒翻?)
接著屁柏,通過日志文件我們又發(fā)現(xiàn)大量Ticket數(shù)據(jù)高度重復(fù):相同車次的Ticket對象的起點、終點有送、票價都是一樣的淌喻,不一樣的僅僅是姓名。(離死不遠(yuǎn)了H刚)
最終裸删,我們定位到了原因:海量相似的Ticket對象耗盡了內(nèi)存空間。(死得其所U笤)
即使上天不給我再來一次的機(jī)會涯塔,我也會選擇享原模式——通過共享減少對象的創(chuàng)建數(shù)量。
方案
假如清蚀,我們已經(jīng)創(chuàng)建了一個Ticket對象匕荸,現(xiàn)在要創(chuàng)建與之相同車次的另一個Ticket對象;
那么枷邪,享原模式的方式是復(fù)用前一個Ticket對象中相同的數(shù)據(jù)(車次榛搔,出發(fā)地、目的地)东揣,更改不同的數(shù)據(jù)(姓名践惑、座次)來實現(xiàn)另一個Ticket對象的創(chuàng)建。
從程序的角度看運行時創(chuàng)建了兩個不一樣的對象救斑,但從內(nèi)存角度看只創(chuàng)建了一個對象童本,另一個對象是通過共享前一對象的部分?jǐn)?shù)據(jù)來實現(xiàn)的。
在享原模式中脸候,對象中可共享的數(shù)據(jù)被稱為內(nèi)部狀態(tài)穷娱,它通常是不可變的且需要被緩存的部分绑蔫;與之相反,對象中不可共享的數(shù)據(jù)被稱為外部狀態(tài)泵额,它通常是可變的且由客戶端傳入對象配深。
結(jié)構(gòu)
抽象享原角色(Flyweight): 是一個接口或抽象類,它負(fù)責(zé)定義外部狀態(tài)并將它作為方法的入?yún)ⅰ?/p>
具體享原角色(ConcreteFlyweight):抽象享原角色的實現(xiàn)類嫁盲,它負(fù)責(zé)定義內(nèi)部狀態(tài)并為其提供存儲空間以及保證其不可變篓叶。
享原工廠角色(FlyweightFactory):通常是一個簡單工廠類,它負(fù)責(zé)創(chuàng)建具體享原角色并將其緩存在HashMap中羞秤。
客戶端(Client):它通過享原工廠創(chuàng)建對象缸托,但并不知道創(chuàng)建的對象是否是一個共享對象。
//抽象享原角色
public interface Flyweight {
//參數(shù)化外部狀態(tài)
public void operation(String extrinsic);
}
//具體享原角色
public class ConcreteFlyweight implements Flyweight{
//存儲內(nèi)部狀態(tài)
protected String intrinsic;
public ConcreteFlyweight(String intrinsic){
this.intrinsic=intrinsic;
}
@Override
public void operation(String extrinsic) {
System.out.println("不變的內(nèi)部狀態(tài):"+intrinsic+"瘾蛋,可變的外部狀態(tài):"+extrinsic);
}
}
//簡單工廠類
public class FlyweightFactory {
protected static HashMap<String,Flyweight> pool = new HashMap<>();
public static Flyweight getInstance(String key){
//復(fù)用已經(jīng)存在的對象
Flyweight flyweight = pool.get(key);
if(flyweight==null){
flyweight = new ConcreteFlyweight("intrinsic");
//緩存新創(chuàng)建對象
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);
}
}
}
應(yīng)用
接下來俐镐,我們使用享原模式重構(gòu)一下車票查詢系統(tǒng),讓它減少相似對象的創(chuàng)建提高內(nèi)存的利用率哺哼。
首先佩抹,在ITicket接口中聲明依賴外部狀態(tài)的方法。
public interface ITicket {
//每個車次的姓名和座位都是不一樣的
public void print(String userName,String seatOrder);
}
然后取董,創(chuàng)建Ticket并實現(xiàn)ITicket接口棍苹,它只負(fù)責(zé)存儲內(nèi)部狀態(tài)。
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);
}
}
現(xiàn)在茵汰,創(chuàng)建TicketFactory枢里,根據(jù)車次緩存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;
}
}
最后经窖,我們在看看客戶端如何使用簡單工廠復(fù)用共享的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();
//現(xiàn)在的處理方式將共同的屬性和特殊的屬性分離
ITicket ticket = TicketFactory.getTicket(trainNumber);
ticket.print(userName,seatOrder);
}
}
}
}
總結(jié)
我在很多享原模式的文章中,都看到過這樣一種觀點:String常量池画侣、數(shù)據(jù)庫連接池冰悠、緩沖池等池化技術(shù)都使用了享原模式。
其實配乱,這種認(rèn)識是有誤的溉卓,因為他們混淆了完全復(fù)用對象和部分復(fù)用對象的差異。如:String常量搬泥,它復(fù)用的條件是字符和常量池中的字符完全一致桑寨;而享原模式只是復(fù)用對象的部分屬性,而且還要參數(shù)化外部狀態(tài)忿檩。
所以尉尾,不能將兩者等同看待,應(yīng)該區(qū)分他們之間的差異燥透。