設計模式---代理模式

2022-11-06 21:03:01

簡述

對使用者端隱藏目標類,建立代理類拓展目標類,並且對於使用者端隱藏功能拓展的細節,使得使用者端可以像使用目標類一樣使用代理類,面向代理(使用者端只與代理類互動)。

話不多說,看一個優化案例。

優化案例

最初版v0

目前的功能是下載可以下載檔案。

public class BiliBiliDownloader {
    public byte[] download(String filePath) throws InterruptedException {
        System.out.printf("正在下載BiliBili檔案:%s%n", filePath);
        // 模擬檔案下載,睡個10秒
        Thread.sleep(10000);
        return new byte[1024]; // 假裝是下載檔案的位元組陣列
    }
}

使用者端呼叫程式碼,如下。

public class Client {
    public static void main(String[] args) throws InterruptedException {
        BiliBiliDownloader bilidownloader = new BiliBiliDownloader();
        bilidownloader.download("/root/buzuweiqi/java_manual.txt");
    }
}

下載工具類對使用者端完全暴露,使用者端可以直接使用下載類實現下載,這實際上是無可厚非的。
經過研究發現,這個下載類有一個問題:每次呼叫都肯定會下載新的檔案,即便檔案已經被下載過。

為了解決這個問題,開發團隊經過商討已經有了一個初步的方案。看一下程式碼樣例。

修改版v1

團隊決定使用傳統的修改方式(直接修改BiliBiliDownloader),認為這樣最為的直觀。確實,程式碼量少且未來可以預期修改不頻繁時,傳統的修改方案也未嘗不是一個好的選擇。

public class BiliBiliDownloader {
    // 定義用來快取資料的map物件
    private static Map<String, byte[]> map = new HashMap<>();
    public byte[] download(String filePath) throws InterruptedException {
        System.out.printf("正在下載BiliBili檔案:%s%n", filePath);
        if (map.containsKey(filePath)) {
            return map.get(filePath);
        }
        // 模擬檔案下載,睡個10秒
        Thread.sleep(10000);
        byte[] res = new byte[1024]; // 假裝這是下載後的位元組陣列
        map.put(filePath, res); // 加入快取
        return res;
    }
}

使用者端呼叫程式碼,還是和原來一樣。

public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
        BiliBiliDownloader downloader = new BiliBiliDownloader();
        downloader.download("/root/home/buzuweiqi/java_manual.txt");
        // 由於檔案已經快取,所以這次下載非常快
        downloader.download("/root/home/buzuweiqi/java_manual.txt");
        // 由於檔案還未快取,所以這次下載比較緩慢
        downloader.download("/root/home/buzuweiqi/linux_manual.txt");
    }
}

到目前為止好像都沒有啥不妥的地方。直到有一天,客戶提出了新的需求:雖然現在只可以下載bilibili的檔案(視訊,音訊,文章等),以後還想要下載youtube的檔案。

為了實現這個需求,以及方便以後同類的需求變更,是時候用上代理模式。

修改版v2

代理模式在使用的時候需要頂一個一個頂層介面,並且使得代理類和被代理類都實現這個介面。
代理類中需要持有非代理類的一個物件。並且在呼叫代理類的功能前後可以根據業務需要拓展新的功能。

public interface Downloader {
    byte[] download(String filePath) throws InterruptedException;
}

public class BiliBiliDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
        System.out.printf("正在下載BiliBili檔案:%s%n", filePath);
        // 模擬檔案下載,睡個10秒
        Thread.sleep(10000);
        return new byte[1024]; // 假裝是下載檔案的位元組陣列
    }
}

public class ProxyBiliBiliDownloader implements Downloader {
    private static Map<String, byte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
        if (map.containsKey(filePath)) {
            System.out.printf("正在下載BiliBili檔案:%s%n", filePath);
            return map.get(filePath);
        }
        byte[] res = downloader.download(filePath);
        map.put(filePath, res);
        return res;
    }
}

public class YoutubeDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
        System.out.printf("正在下載Youtube檔案:%s%n", filePath);
        // 模擬檔案下載,睡個10秒
        Thread.sleep(10000);
        return new byte[1024]; // 假裝是下載檔案的位元組陣列
    }
}

public class ProxyYoutubeDownloader implements Downloader {
    private static Map<String, byte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
        if (map.containsKey(filePath)) {
            System.out.printf("正在下載Youtube檔案:%s%n", filePath);
            return map.get(filePath);
        }
        byte[] res = downloader.download(filePath);
        map.put(filePath, res);
        return res;
    }
}

使用者端的使用案例如下。

public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
        Downloader downloader = new ProxyBiliBiliDownloader();
        downloader.download("/root/home/buzuweiqi/java_manual.txt");
        downloader = new ProxyYoutubeDownloader();
        downloader.download("/root/home/buzuweiqi/linux_manual.txt");
    }
}

使用者端不再依賴目標類,而是轉而依賴代理類。
代理模式使得增加相似需求時可以只增加一對實現類(目標類,代理類),而不用修改原本的類,符合開閉原則。

實際上通常我們會使用一個更為簡單的方式控制代理物件的建立:反射。

修改版v3

高層介面,實現的目標類、代理類依舊不變。

public interface Downloader {
    byte[] download(String filePath) throws InterruptedException;
}

public class BiliBiliDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
        System.out.printf("正在下載BiliBili檔案:%s%n", filePath);
        // 模擬檔案下載,睡個10秒
        Thread.sleep(10000);
        return new byte[1024]; // 假裝是下載檔案的位元組陣列
    }
}

public class ProxyBiliBiliDownloader implements Downloader {
    private static Map<String, byte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
        if (map.containsKey(filePath)) {
            System.out.printf("正在下載BiliBili檔案:%s%n", filePath);
            return map.get(filePath);
        }
        byte[] res = downloader.download(filePath);
        map.put(filePath, res);
        return res;
    }
}

public class YoutubeDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
        System.out.printf("正在下載Youtube檔案:%s%n", filePath);
        // 模擬檔案下載,睡個10秒
        Thread.sleep(10000);
        return new byte[1024]; // 假裝是下載檔案的位元組陣列
    }
}

public class ProxyYoutubeDownloader implements Downloader {
    private static Map<String, byte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
        if (map.containsKey(filePath)) {
            System.out.printf("正在下載Youtube檔案:%s%n", filePath);
            return map.get(filePath);
        }
        byte[] res = downloader.download(filePath);
        map.put(filePath, res);
        return res;
    }
}

在使用者端呼叫時,引入Java反射,通過反射建立具體的代理物件。
config.prop檔案中定義PROXY_NAME變數並指定需要反射建立的類的完整路徑

public class Client {
    public static void main(String[] args) throws Exception {
        Properties prop = new Properties();
        prop.load(new FileReader("src/resource/props/config.prop"));
        Downloader downloader = (Downloader) Class.forName(prop.getProperty("PROXY_NAME"))
                .getDeclaredConstructor().newInstance();
        downloader.download("/root/home/buzuweiqi/java_manual.txt");
        downloader = new ProxyYoutubeDownloader();
        downloader.download("/root/home/buzuweiqi/linux_manual.txt");
    }
}

通過Java反射機制,應對每次的需求變更,甚至都不需要修改使用者端程式碼,只需要修改案例中的config.prop即可。減少了不必要的程式碼修改,提高了系統的可維護性。

總結

優點

  • 代理類與目標類的使用方式一致,這極大的降低了使用者端呼叫的學習成本,易用性高。

  • 面向介面,無需在意實現的細節。

缺點

  • 類的數量倍增,系統複雜度增加。

適用場景

  • 當需要對於模組拓展,但又不方便打破使用者端原有的呼叫規則時。使用者端中物件的建立依舊需要修改,這沒有什麼好的辦法。
  • 常用的代理模式使用方案
    • 緩衝代理(案例)
    • 遠端代理
    • 虛擬代理

除此之外還有很多應用場景,代理模式是設計模式中使用非常廣泛的一種。