設計模式【10】-- 順便看看享元模式

秦懷雜貨店發表於2022-01-09

設計模式系列:http://aphysia.cn/categories/designpattern

1

開局還是那種圖,各位客官往下看...

享元模式是什麼?

享元模式(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連線池等等,這些資源本質上都比較寶貴,我們可以共享。

JDKInteger其實也用了快取的技術,因為大家常用的都是較小的數值,所以預設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);
    }

我們可以看到如果在lowhigh範圍內的資料,就會從快取裡面獲取,否則會直接新建一個物件,那麼lowhigh的範圍多大呢?

        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-128127被快取了,也驗證了我們的結果,注意必須使用Integer.valueOf()這個辦法,要是使用構造器new Integer(),建立出來必定是新的物件。

總結

  • 優點:如果有很多相似或者重複的物件,使用享元模式,可以節省空間
  • 缺點:如果重用很多,不同地方還做了特殊化處理,程式碼複雜度增加

設計模式其實是在軟體工程的不斷摸索中,總結出來的常用的一種設計思路,並不是非用不可,不是銀彈,但是總有值得我們學習的地方,瞭解它這般設計的好處,不斷的改進我們寫程式碼,即使每次一點點改進。曾經聽過一句話:看見別人寫得不優雅的程式碼就有想重構它的衝動,可以多讀讀自己寫的程式碼,然後寫得更好(大致是這個意思)。共勉!

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,個人網站:http://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。

劍指Offer全部題解PDF

開源程式設計筆記

相關文章