從實際場景來看設計模式2:由自定義類加載器到模板方法模式及橋接模式

問題摘要

問題一自定義類加載器怎樣做到只需要覆寫findClass方法即可將自身與JVM整體類加載邏輯結合在一起?
問題二:當一個類中組件有多種實現方式又可以通過怎樣的方式梳理子類實現從而避免子類出現”爆炸“呢?

ClassLoader類加載器與自定義類加載器

由Java源文件編譯出來的字節碼class文件是怎樣加載到JVM內存中參與運行的呢?答案是文件由ClassLoader類加載器load到內存中經過加載->鏈接->初始化等步驟轉化爲Java內存中的類對象從而參與執行的。

我們知道Java的類加載器是有一套等級機制的,通過朔源委託加載機制(也叫雙親委派機制)最終決定由哪個層級的類加載器來進行加載。層級由等級從高到低依次爲:BootStrap(引導類加載器)->Extension(拓展類加載器)->System(系統類加載器)->Custom(用戶自定義類加載器)

當運行中需要用到一個類而該類未經過加載時,系統將通過該類的類加載器(getClassLoader方法)去加載該類,類加載器不會立即去加載該類,如果它不是最頂層的BootStrap類加載器則需要委託它的上一級類加載器去加載,依次向上委託,直到加載請求到達最頂層的BootStrap類加載器,然後BootStrap嘗試進行加載該類,如果加載不到,則交給下一級Extension進行加載,依此類推,如果整條鏈上的類加載器都未能成功加載,則拋出ClassNotFoudException。

也就是說:如果一個類被加載到JVM,那麼加載它的類加載器一定是能加載它的類加載器中等級最高的那個,有什麼好處:第一,避免重複加載,父類能加載的,子類就不用再加載一次了;第二,安全因素,如果java.lang.Object可以隨意被用戶自定義加載器加載的話,那麼系統就可以被隨意改造了。

什麼時候會用到自定義類加載器呢?比如說Tomcat是擁有自己的類加載器的,這樣當一個Tomcat中部署了多個項目,而項目中對於同一個類庫引用的版本又有所不同,那麼不將各自的依賴隔離開是會出現運行異常的,還有一定就是可以很方便的進行熱部署:當源碼改動時,使用自定義類加載器進行類替換,實現了不重新部署即可對源碼熱更新。

如何自定義類加載器呢? Java中實現自定義類加載器是很方便的,具體的實現方式爲:繼承ClassLoader類,覆寫findClass方法寫好具體的類加載邏輯即可。那麼這裏引申出摘要中的問題:爲什麼只需要關注自身的類加載邏輯即可融入到整個類加載體系當中的呢?

答案是:整個ClassLoader體系應用了模板方法模式:

定義一個操作中的算法的骨架,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。

設計模式1:模板方法 Template Method

模板方法設計模式可以理解爲:由父類定義組件的流程骨架,將流程中某些具體的實現部分交給子類來實現,與策略模式有所不同,父類中的主幹流程是final方法,並不是所有方法都交給子類去覆寫實現的。
我們來看一個具體的場景:
設計一個配置加載類,它可以從數據源中加載某個配置的屬性,數據源可以是mysql、可以是一個properties文件等等

public abstract class AbsSetting {
   // final方法 不允許覆寫
    public final String getSetting(String key) {
        String value = readFromDatabase(key);
        return value;
    }

	// readFromDatabase 因爲不確定數據源所以具體實現交給繼承Setting的子類
	protected abstract String readFromDatabase(String key);
}

因爲從數據庫或者通過IO來加載數據是很耗時的操作,所以我們引入下緩存,當然,緩存也不是確定的,可以是Redis、可以是Memcache、或者是HashMap等

public abstract class AbsSetting {
   // final方法 不允許覆寫
    public final String getSetting(String key) {
       // 先從緩存讀取:
        String value = lookupCache(key);
        if (value == null) {
            // 在緩存中未找到,從數據庫讀取:
            value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            // 放入緩存:
            putIntoCache(key, value);
        } else {
            System.out.println("[DEBUG] load from cache: " + key + " = " + value);
        }
        return value;
    }

	// readFromDatabase 因爲不確定數據源所以具體實現交給繼承Setting的子類
	protected abstract String readFromDatabase(String key);
}

我們發現裏邊的lookupCache(key)和putIntoCache(key, value)兩個方法還沒聲明的,爲了編譯通過,聲明成抽象的吧,交給子類實現。

// 從緩存讀取
protected abstract String lookupCache(String key);
// 添加key-value到緩存
protected abstract void putIntoCache(String key, String value);

好了,骨架我們已經寫好了,現在我們寫子類,數據源用properties文件吧,緩存用hashmap,這樣實現是最簡單的,java原生類庫就支持
只需要繼承Setting類,覆寫三個讀寫相關的方法即可

public class LocalSetting extends AbsSetting {
    private Map<String, String> cache = new HashMap<>();
	@Override
    protected String lookupCache(String key) {
        return cache.get(key);
    }
	@Override
    protected void putIntoCache(String key, String value) {
        cache.put(key, value);
    }
    @Override
    protected String readFromDatabase(String key) throws IOException {
        String f = "setting.properties";
        Properties props = new Properties();
        props.load(new java.io.FileInputStream(f));
        return props.getProperty(key);
    }
}

測試代碼

class Test {
    public static void main(String[] args) throws IOException {
        AbsSetting setting = new LocalSetting ();
        setting.getResource("testKey");
    }
}       

還有改進空間嗎

從模板方法的實現中,我們看到了頂層設計中如何避免核心邏輯被修改(final修飾)以及如何讓子類更簡單的去實現需要實現的邏輯。

小結:模板方法是一種高層定義骨架,底層實現細節的設計模式,適用於流程固定,但某些步驟不確定或可替換的情況。

此時我們應該發現上述實現中一個新的問題:如果數據源有10多種,緩存有10多種,那麼學過數學排列組合的你一定想到,如果僅使用繼承去實現所有數據源與緩存的自由組合,需要定義10 * 10種子類,那再繼續擴充呢,子類就輕易的“爆炸”了

這是一個典型的問題,爲了優雅的適應這種變化,可以考慮一下橋接設計模式

設計模式2:橋接模式 Bridge

將抽象部分與它的實現部分相分離,使它們都可以獨立的變化

咋一看挺玄乎,不過我們先看下面這段話:

持有高層接口不但代碼更靈活,而且把各種接口組合起來也更容易。一旦持有某個具體的子類類型,要想做一些改動就非常困難。

好像還是玄乎,,,其實簡單來說,上一小結留給我們的問題是:一個子類只能選數據源中的一種與緩存中的一種來實現,這樣持有具體實現的強耦合結構,不利於拓展,想要其他組合只能另起爐竈–重新再定義一個。

好的,持有抽象是吧,那緩存用抽象的,因爲緩存主要設計兩個主要功能:緩存鍵值的存和取,這樣我們可以定義一個接口(抽象類和接口如何選擇?形容詞用接口,名詞用抽象類。接口代表實現類公有的能力,抽象類是子類的模板,與普通類的區別僅僅在於有沒有抽象方法。

/**
 * 定義緩存的抽象接口 爲了將實現延遲到客戶端
 */
interface CacheSource {
    /**
     * 設置緩存值
     *
     * @param key
     * @param value
     * @return
     */
    String setKeyValue(String key, String value);

    /**
     * 獲取緩存值
     *
     * @param key
     * @return
     */
    String getValue(String key);
}

好了,緩存抽象出來了,我們再AbsSetting中持有它的類型

public abstract class AbsSetting {

	// 這裏就是持有抽象接口了  可以靈活切換實現類
    private CacheSource cacheSource;

    public Setting2(CacheSource cacheSource) {
        this.cacheSource = cacheSource;
    }
   // 下邊的代碼先省略 與模板方法中的AbsSetting相同,後文會給出完整實例代碼
}

其實現在我們已經寫好了橋接模式的一部分,現在我們給出一個具體的緩存實現類型吧,簡單起見就用HashMap。

/**
 * 緩存實現1-使用HashMap作爲緩存池
 */
class LocalCache implements CacheSource {
    private Map<String, String> cache = new HashMap<>(1024);

    public LocalCache() {
        super();
    }

    @Override
    public String setKeyValue(String key, String value) {
        return cache.put(key, value);
    }

    @Override
    public String getValue(String key) {
        return cache.get(key);
    }
}

然後我們給出一個AbsSetting的子類,Mysql作爲數據源,這樣一個數據源中的緩存可以通過實現類的切換來避免重複定義子類,我們的10 * 10自由組合已經變成 10(10種數據源) + 10(10種緩存實現)了,不過不着急寫代碼

爲了再一次擁抱變化,通常我們將數據源再次進行抽象,即AbsSetting與 XXXSetting之間再加一層AbsDataBase ,在這個中間層的抽象類中,我們可以對之後所有數據源的共有改變定義到次層,再一次降低了耦合的概率

當然,XXXSetting也不再直接繼承AbsSetting了,而是繼承抽象公共層AbsDataBase

/**
 * 數據源之上的抽象類 靈活拓展其他共用方法  比如獲取下數據源類型、強制關閉數據源連接、實現一個連接池等操作
 * 所有具體數據源實例繼承此類
 */
abstract class AbsDataBase extends Setting2 {
    private CacheSource cacheSource;

    public AbsDataBase(CacheSource cacheSource) {
        super(cacheSource);
        this.cacheSource = cacheSource;
    }

    @Override
    protected String putIntoCache(String key, String value) {
        return cacheSource.setKeyValue(key, value);
    }

    @Override
    protected String lookupCache(String key) {
        return cacheSource.getValue(key);
    }
}

這樣,我們的數據源子類就可以這樣寫了

/**
 * 具體數據源實現類-mysql數據源
 * 之後其他任意數據源只需要繼承AbsDataBase,覆寫readFromDatabase即可
 */
class MysqlSetting extends AbsDataBase {

    public MysqlSetting(CacheSource cacheSource) {
        super(cacheSource);
    }

    @Override
    protected String readFromDatabase(String key) throws IOException {
        // 這裏寫從數據庫獲取的邏輯即可
        return "find in db";
    }
}

基本上,橋接模式已經寫進去了,之後數據源的增加,只需要繼承AbsDataSource即可;緩存類型的增加,只需要實現CacheSource接口即可。數據源與緩存實現在兩個維度上獨立的變化,相互之間無任何影響,不會對原有代碼進行改動

重溫里氏替換原則

良好的代碼設計應該具備里氏替換原則:對拓展開放,對修改關閉。

總結

  • 模板方法可以讓父類與子類各司其職:父類負責骨架,子類填充實現。
  • 模板方法在java中有廣泛應用:ClassLoader;在集合類中,AbstractList和AbstractQueuedSynchronizer都定義了很多通用操作,子類只需要實現某些必要方法。
  • 橋接模式通過持有組件的高層抽象接口,使組件間獨立的進行變化,很好的解決了子類爆炸問題。
  • 通過學習設計模式,可以很好的瞭解面向對象的封裝、繼承與多態,通過合理的運用,將臃腫的代碼結構變成可維護、具有良好拓展性的優雅的代碼。

附模板方法模式與橋接模式實例的完整代碼

因爲是先寫好的代碼,而後作總結的,所以代碼中類和方法的命名會有出入

模板方法源碼

package com.designparttern.template;

/**
 * 設計模式-行爲型模式-模板方法
 * 模板方法的核心思想是:父類定義骨架,子類實現某些細節。
 * java中有許多應用  例如類加載器:當用戶需要自定義加載器時,只需要繼承ClassLoader類重寫findClass方法 實現自定義的類加載策略即可
 */

import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 獲取配置抽象類
 */
public abstract class Setting {

    /**
     * 主要對外提供的方法  通過key來獲取配置value
     * 代碼邏輯爲先從緩存中獲取  如果獲取不到則從database中獲取 至於使用什麼緩存  使用什麼database 暫時未指定  提供需子類實現的抽象方法
     * 子類覆寫時 需要考慮緩存操作get/set 及 數據庫操作 get 的具體實現
     * getResource定義爲final方法  不允許子類覆寫修改核心業務邏輯
     *
     * @param key
     * @return
     */
    public final String getResource(String key) throws IOException {
        String val = lookupCache(key);
        if (val == null) {
            String value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            putIntoCache(key, value);
            return value;
        }
        System.out.println("[DEBUG] load from cache: " + key + " = " + val);
        return val;
    }

    protected abstract String readFromDatabase(String key) throws IOException;

    protected abstract String putIntoCache(String key, String value);

    protected abstract String lookupCache(String key);
}

/**
 * 僞代碼  僅作爲示例
 * 子類1 使用Map作爲緩存  使用mysql作爲database
 */
class LocalCacheMysqlSetting extends Setting {

    private Map<String, String> cache = new HashMap<>(1024);

    @Override
    protected String readFromDatabase(String key) {
        // 假裝我在mysql中通過sql獲取了數據
        return "read for Mysql or null";
    }

    @Override
    protected String putIntoCache(String key, String value) {
        Object put = cache.put(key, value);
        // key or null
        return put.toString();
    }

    @Override
    protected String lookupCache(String key) {
        return cache.get(key);
    }
}

/**
 * 僞代碼 僅作爲示例
 * 子類2  使用redis做緩存  使用文件作爲數據源 通過io流讀取
 */
/*class RedisCacheFileSetting extends Setting {

    private RedisClient client = RedisClient.create("redis://localhost:6379");

    @Override
    protected String readFromDatabase(String key) throws IOException {
        String f = "setting.properties";
        Properties props = new Properties();
        props.load(new java.io.FileInputStream(f));
        return props.getProperty(key);
    }

    @Override
    protected String putIntoCache(String key, String value) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.get(key);
        }
    }

    @Override
    protected String lookupCache(String key) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.set(key, value);
        }
    }
}*/

class Test {
    public static void main(String[] args) throws IOException {
        Setting setting = new LocalCacheMysqlSetting();
        setting.getResource("testKey");

        /**
         * 思考一個問題  假設緩存有map redis memcache mongodb 等類型  數據源有mysql oracle 等類型  當緩存與數據源隨意組合時  可產生m * n  8種不同組合得Setting實現
         * 如果有更多的緩存類型及數據源  那麼就會帶來子類爆炸問題
         * 想要在緩存和數據源兩種維度各自擁有不同的拓展  那麼可以使用另一種設計模式來實現--橋接設計模式
         */
    }
}

加入橋接模式的源碼

package com.designparttern.template;

/**
 * 因爲Setting的實現中 緩存及數據源都具有不同的類型 當我們想要得到任意組合的配置源時  需要爲自由組合的緩存及數據源都提供一個實現類
 * 爲了避免子類爆炸,本例Setting2嘗試使用橋接模式來解耦配置源的實現
 */

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 獲取配置抽象類
 * 將核心邏輯getResource實現 具體與數據庫和緩存的交互交由子類實現
 * 持有緩存實例的接口 對於任意緩存的添加和實現都只需要實現CacheSource接口即可 不再需要繼續組合數據源創建子類
 * 數據源通過再次定義一個AbsDataBase抽象類  可以在其中定義其他公用的功能
 * 每一個數據源實例都只需要繼承AbsDataBase,然後覆寫readFromDatabase 從數據源獲取配置即可,很好的實現了對拓展開放
 * 客戶端選擇具體數據源在其中傳入相應的緩存實現,調用getResource即可
 */
public abstract class Setting2 {

    private CacheSource cacheSource;

    public Setting2(CacheSource cacheSource) {
        this.cacheSource = cacheSource;
    }

    /**
     * 主要對外提供的方法  通過key來獲取配置value
     * 代碼邏輯爲先從緩存中獲取  如果獲取不到則從database中獲取 至於使用什麼緩存  使用什麼database 暫時未指定  提供需子類實現的抽象方法
     * 子類覆寫時 需要考慮緩存操作get/set 及 數據庫操作 get 的具體實現
     * getResource定義爲final方法  不允許子類覆寫修改核心業務邏輯
     * @param key
     * @return
     */
    public final String getResource(String key) throws IOException {
        String val = lookupCache(key);
        if (val == null) {
            String value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            putIntoCache(key, value);
            return value;
        }
        System.out.println("[DEBUG] load from cache: " + key + " = " + val);
        return val;
    }

    protected abstract String readFromDatabase(String key) throws IOException;

    protected abstract String putIntoCache(String key, String value);

    protected abstract String lookupCache(String key);
}

/**
 * 數據源之上的抽象類 靈活拓展其他共用方法  比如獲取下數據源類型、強制關閉數據源連接、實現一個連接池等操作
 * 所有具體數據源實例繼承此類
 */
abstract class AbsDataBase extends Setting2 {
    private CacheSource cacheSource;

    public AbsDataBase(CacheSource cacheSource) {
        super(cacheSource);
        this.cacheSource = cacheSource;
    }

    @Override
    protected String putIntoCache(String key, String value) {
        return cacheSource.setKeyValue(key, value);
    }

    @Override
    protected String lookupCache(String key) {
        return cacheSource.getValue(key);
    }
}

/**
 * 具體數據源實現類-mysql數據源
 * 之後其他任意數據源只需要繼承AbsDataBase,覆寫readFromDatabase即可
 */
class MysqlSetting extends AbsDataBase {

    public MysqlSetting(CacheSource cacheSource) {
        super(cacheSource);
    }

    @Override
    protected String readFromDatabase(String key) throws IOException {
        // 這裏寫從數據庫獲取的邏輯即可
        return "find in db";
    }
}

/**
 * 定義緩存的抽象接口 爲了將實現延遲到客戶端
 */
interface CacheSource {
    /**
     * 設置緩存值
     *
     * @param key
     * @param value
     * @return
     */
    String setKeyValue(String key, String value);

    /**
     * 獲取緩存值
     *
     * @param key
     * @return
     */
    String getValue(String key);
}

/**
 * 緩存實現1-使用HashMap作爲緩存池
 */
class LocalCache implements CacheSource {
    private Map<String, String> cache = new HashMap<>(1024);

    public LocalCache() {
        super();
    }

    @Override
    public String setKeyValue(String key, String value) {
        return cache.put(key, value);
    }

    @Override
    public String getValue(String key) {
        return cache.get(key);
    }
}


class Test2 {
    public static void main(String[] args) throws IOException {
        /**
         * 思考一個問題  假設緩存有map redis memcache mongodb 等類型  數據源有mysql oracle 等類型  當緩存與數據源隨意組合時  可產生m * n  8種不同組合得Setting實現
         * 如果有更多的緩存類型及數據源  那麼就會帶來子類爆炸問題
         * 想要在緩存和數據源兩種維度各自擁有不同的拓展  那麼可以使用另一種設計模式來實現--橋接設計模式
         *
         * 解答:通過以上實現將緩存抽離出接口 具體的數據源實現 具體的緩存實現將在不同方向上靈活拓展
         */
        Setting2 setting = new MysqlSetting(new LocalCache());
        setting.getResource("testKey");
    }
}

橋接模式源碼

package com.designparttern.bridge;

/**
 * 橋接模式
 * 通過抽象將一個類中可能出現自由組合的組件進行管理,避免子類爆炸
 * 加入汽車品牌有很多種 奔馳 寶馬 奧迪  引擎也有多種 電動 燃油  混合  如果將上述品牌及引擎自由組合在一起 通過繼承方式實現 需要定義三個抽象類 至少九個不同子類
 * 如果再增加一個品牌和一個引擎 則整個汽車結構將變得異常臃腫難以維護
 * 橋接模式通過持有引擎抽象類  並將汽車抽象化  在客戶端實現時再實現具體的汽車品牌及傳入具體的引擎,將整個實現方式變得靈活
 */

/**
 * 頂層抽象類 汽車類  持有引擎接口  並且子類通過實現該接口靈活實現不同品牌
 */
public abstract class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public abstract void drive();
}

/**
 * 爲了使引擎可以獨立拓展  通過抽象接口  定義共有行爲
 * 汽車類持有頂層接口  可以靈活切換具體實現
 */
interface Engine {
    void start();
}

/**
 * 定義抽象修正類用來添加一些公共的額外的操作
 */
abstract class RefindCar extends Car {
    private Engine engine;

    public RefindCar(Engine engine) {
        super(engine);
        this.engine = engine;
    }

    @Override
    public void drive() {
        engine.start();
        System.out.println("Drive " + getBrand() + " car");
    }

    /**
     * 獲取汽車品牌
     *
     * @return
     */
    public abstract String getBrand();
}

/**
 * 針對每一種汽車 繼承自抽象修正類
 * 汽車品牌可以任意獨立拓展  bossCar tinyCar bigCar 。。。
 */
class BossCar extends RefindCar {
    public BossCar(Engine engine) {
        super(engine);
    }

    @Override
    public String getBrand() {
        return "BossCar";
    }
}

/**
 * 針對每一種引擎都可以通過繼承Engine來拓展
 * 引擎可以有任意多種  HyBridEngine  FuelOilEngine  ElectricEngine 。。。
 */
class HyBridEngine implements Engine {

    @Override
    public void start() {
        System.out.println("HyBridEngine start...");
    }
}

class TestDemo {
    public static void main(String[] args) {
        Car car = new BossCar(new HyBridEngine());
        car.drive();
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章