設計模式系列:http://aphysia.cn/categories/designpattern
開局還是那種圖,各位客官往下看...
享元模式是什麼?
享元模式(FlyWeight
),是結構型模式的一種,主要是為了減少建立物件的數量,減少記憶體佔用以及提高效能。說到這裡,不知道你是否會想到池技術,比如String
常量池,資料庫連線池,緩衝池等等,是的,這些都應用了享元模式。
比如,有一些物件,建立時候需要資源比較多,建立成本比較高,記憶體開銷比較大,如果我們一直建立,機器吃不消,那麼我們就想到了池化技術,把建立好的物件放在裡面,需要時,去池子裡面取就可以了,也就是大家共享了池子裡面的物件,這就是共享。
聽名字,就很共享單車:
享元模式的特點
一般而言,享元物件需要在不同的場景下使用,那狀態如果可隨意修改,就容易造成混亂,出錯的概率大大增加。但是如果所有的內部屬性都是不可修改的,貌似也不是十分靈活,因此為了在穩定和靈活性之間找到平衡點,一般的享元物件,都會將內部屬性劃分為兩大類:
- 內部狀態:不可變,且在多個地方中共享,重複使用的部分,只能通過建構函式設值
- 外部狀態:每個物件,在不同場景下,可能存在不一樣的狀態,可以修改
- 單純享元模式:在單純享元模式中,所有的具體享元類都是可以共享的,不存在非共享具體享元類。
- 複合享元模式:將一些單純享元物件使用組合模式加以組合,還可以形成複合享元物件,這樣的複合享元物件本身不能共享,但是它們可以分解成單純享元物件,而後者則可以共享
這裡我們說的是單純享元模式,享元模式一般會有幾種物件:
- 享元介面或則抽象類(
Flyweight
):在介面或者抽象類中宣告定義了公共的方法,可以對外提供部分能力,或者按需提供資料。 - 具體的享元實現類(
ConcreteFlyweight
):實現了抽象享元類,在內部有一部分資料是不可變的,實現介面的時候,會對外提供一部分能力或者資料。 - 享元工廠(
FlyweightFactory
): 享元工廠主要是用來建立和管理享元物件的,將各種型別的享元物件放到一個池子裡,一般是鍵值對的形式存在,當然也可以是其他的型別,如果初次獲取一個物件,需要先建立,如果池子裡已經有該物件,那麼就可以直接返回了。
實現
舉個小栗子,比如我們出去玩耍需要購買飛機票,假設一架航班的唯一性是與航班號,出發時間,到達時間相關,使用者喜歡通過航班號,來查詢航班的相關資訊,首先我們需要建立航班一個介面:
public interface IFlight {
void info();
}
具體的航班類Flight
:
public class Flight implements IFlight {
private String flightNo;
private String start;
private String end;
private boolean isDelay;
public Flight(String flightNo, String start, String end) {
this.flightNo = flightNo;
this.start = start;
this.end = end;
isDelay = Math.random() > 0.5;
}
@Override
public void info() {
System.out.println(String.format("從[%s]到[%s]的航班[%s]: %s ",
start, end, flightNo, isDelay ? "延誤起飛" : "正常起飛"));
}
}
航班搜尋工廠類FlightSearchFactory
:
public class FlightSearchFactory {
public static IFlight searchFlight(String flightNo,String start,String end){
return new Flight(flightNo,start,end);
}
}
模擬客戶端請求:
public class ClientTest {
public static void main(String[] args) {
IFlight flight = FlightSearchFactory.searchFlight("C9876","北京","上海");
flight.info();
}
}
我們可以看到列印出了以下資訊:
從[北京]到[上海]的航班[C9876]: 延誤起飛
但是,上面的有一個問題,每次來訪問,都會建立一個物件,坐同一個航班的人,理論上查詢的是相同的資料才對,這部分其實可以共享的,複用來提高效率,何樂而不為呢?
怎麼快取呢?
我們一般用HashMap
來快取,只需要將唯一識別的key
定義好即可:
import java.util.HashMap;
import java.util.Map;
public class FlightSearchFactory {
private static Map<String, IFlight> maps = new HashMap<>();
public static IFlight searchFlight(String flightNo, String start, String end) {
String key = getKey(flightNo, start, end);
IFlight flight = maps.get(key);
if (flight == null) {
System.out.print("快取中沒有,需要重新構建:");
flight = new Flight(flightNo, start, end);
maps.put(key, flight);
}else{
System.out.print("從快取中讀取資料:");
}
return flight;
}
private static String getKey(String flightNo, String start, String end) {
return String.format("%s_%s_%s", flightNo, start, end);
}
}
測試程式碼:
public class ClientTest {
public static void main(String[] args) {
IFlight flight = FlightSearchFactory.searchFlight("C9876","北京","上海");
flight.info();
IFlight flight1 = FlightSearchFactory.searchFlight("C9876","北京","上海");
flight1.info();
IFlight flight2 = FlightSearchFactory.searchFlight("H1213","北京","廣州");
flight2.info();
}
}
測試結果:
快取中沒有,需要重新構建:從[北京]到[上海]的航班[C9876]: 正常起飛
從快取中讀取資料:從[北京]到[上海]的航班[C9876]: 正常起飛
快取中沒有,需要重新構建:從[北京]到[廣州]的航班[H1213]: 正常起飛
可以看到如果快取裡面有,那麼就不會重新構建物件,可以達到共享物件的目的,我們平時在專案裡面使用的各種連線池,比如Redis
連線池,Mysql
連線池等等,這些資源本質上都比較寶貴,我們可以共享。
JDK
中Integer
其實也用了快取的技術,因為大家常用的都是較小的數值,所以預設Integer
如果使用valuesOf(int i)
方法獲取,就會優先讀取快取內容:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
我們可以看到如果在low
和high
範圍內的資料,就會從快取裡面獲取,否則會直接新建一個物件,那麼low
和high
的範圍多大呢?
static final int low = -128;
static final int high;
high
是動態變化的,但是high
是有斷言的,必須大於等於127
:assert IntegerCache.high >= 127;
,而範圍可以從java.lang.Integer.IntegerCache.high
這個配置項讀取出來:
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
測試一下:
public class IntegerTest {
public static void main(String[] args) {
// 不相等
Integer integer = Integer.valueOf(128);
Integer integer1 = Integer.valueOf(128);
System.out.println(integer == integer1);
// 相等
Integer integer2 = Integer.valueOf(127);
Integer integer3 = Integer.valueOf(127);
System.out.println(integer2 == integer3);
// 相等
Integer integer4 = Integer.valueOf(0);
Integer integer5 = Integer.valueOf(0);
System.out.println(integer4 == integer5);
// 相等
Integer integer6 = Integer.valueOf(-128);
Integer integer7 = Integer.valueOf(-128);
System.out.println(integer6 == integer7);
// 不相等
Integer integer8 = Integer.valueOf(-129);
Integer integer9 = Integer.valueOf(-129);
System.out.println(integer8 == integer9);
}
}
從上面的結果可以看出實際上Integer
從-128
到127
被快取了,也驗證了我們的結果,注意必須使用Integer.valueOf()
這個辦法,要是使用構造器new Integer()
,建立出來必定是新的物件。
總結
- 優點:如果有很多相似或者重複的物件,使用享元模式,可以節省空間
- 缺點:如果重用很多,不同地方還做了特殊化處理,程式碼複雜度增加
設計模式其實是在軟體工程的不斷摸索中,總結出來的常用的一種設計思路,並不是非用不可,不是銀彈,但是總有值得我們學習的地方,瞭解它這般設計的好處,不斷的改進我們寫程式碼,即使每次一點點改進。曾經聽過一句話:看見別人寫得不優雅的程式碼就有想重構它的衝動,可以多讀讀自己寫的程式碼,然後寫得更好(大致是這個意思)。共勉!
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,個人網站:http://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。