設計模式【10】– 順便看看享元模式
設計模式系列://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()
,創建出來必定是新的對象。
總結
- 優點:如果有很多相似或者重複的對象,使用享元模式,可以節省空間
- 缺點:如果重用很多,不同地方還做了特殊化處理,程式碼複雜度增加
設計模式其實是在軟體工程的不斷摸索中,總結出來的常用的一種設計思路,並不是非用不可,不是銀彈,但是總有值得我們學習的地方,了解它這般設計的好處,不斷的改進我們寫程式碼,即使每次一點點改進。曾經聽過一句話:看見別人寫得不優雅的程式碼就有想重構它的衝動,可以多讀讀自己寫的程式碼,然後寫得更好(大致是這個意思)。共勉!
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,個人網站://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。