Design Pattern (17) - Proxy Pattern (代理模式)

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

需求

我們的任務是建立一個影片播放系統,需求如下:

  • 應用能播放多個影片,但避免每次都重複下載相同的影片。
  • 影片需要在用戶第一次訪問時下載,之後從快取中取得以節省資源。
  • 提供一個透明的介面,無需讓客戶端知道影片是透過代理取得的。

物件導向分析 (OOA)

理解需求後,讓我們來快速實作物件導向分析吧!

察覺 Forces

在未使用設計模式的情況下,我們可能面臨以下挑戰:

  1. 高頻寬消耗 (High Bandwidth Usage)

    • 如果每次播放影片都重新下載,將導致不必要的頻寬浪費。
  2. 延遲時間 (High Latency)

    • 每次下載影片會增加播放前的等待時間,影響用戶體驗。
  3. 客戶端耦合 (Client Coupling)

    • 如果客戶端需要處理影片的下載邏輯,會增加不必要的複雜性。

套用 Proxy Pattern (Solution) 得到新的 Context (Resulting Context)

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Proxy Pattern 解決這個問題。

Proxy Pattern 提供了解決方案,通過引入 Proxy 物件來控制對核心物件的訪問,實現快取功能並提升效能。

先來看一下 Proxy Pattern 的 UML:

以下是 Proxy Pattern 的主要角色:

  • Subject (主題介面):定義核心物件與代理物件的共同介面。
  • RealSubject (具體主題):核心物件,負責實際下載與播放影片。
  • Proxy (代理):代理物件,控制對核心物件的訪問,實現快取功能。

將 Proxy Pattern 套用到我們的應用吧

物件導向程式設計 (OOP)

[Subject: VideoPlayer]

interface VideoPlayer {
    fun download(name: String): String
    fun play(data: String)
}

[RealSubject: YoutubeVideoPlayer]

class YoutubeVideoPlayer : VideoPlayer {
    override fun download(name: String): String {
        println("Downloading video from YouTube: $name")
        // 模擬下載結果返回的影片資料
        return "VideoData($name)"
    }

    override fun play(data: String) {
        println("Playing video: $data")
    }
}

[Proxy: ProxyVideoPlayer]

class ProxyVideoPlayer(
    private val player: YoutubeVideoPlayer
) : VideoPlayer {

    private val cacheVideoList = mutableMapOf<String, String>()

    override fun download(name: String): String {
        return if (cacheVideoList.containsKey(name)) {
            println("Fetching video from cache: $name")
            cacheVideoList[name]!!
        } else {
            println("First time download for: $name")
            val videoData = player.download(name)
            cacheVideoList[name] = videoData
            videoData
        }
    }

    override fun play(data: String) {
        player.play(data)
    }
}

[Client: VideoPlayerManager]

class VideoPlayerManager(private val player: VideoPlayer) {
    fun playVideo(name: String) {
        println("Request to play video: $name")
        val videoData = player.download(name)
        player.play(videoData)
    }
}

fun main() {
    // Using ProxyVideoPlayer
    val youtubePlayer = YoutubeVideoPlayer()
    val proxyPlayer = ProxyVideoPlayer(youtubePlayer)
    val manager = VideoPlayerManager(proxyPlayer)

    // Play video
    manager.playVideo("funny_cats.mp4")
    manager.playVideo("funny_cats.mp4") // using cache
    manager.playVideo("epic_fail.mp4")
    manager.playVideo("funny_cats.mp4") // using cache
}

[Output]

Request to play video: funny_cats.mp4
First time download for: funny_cats.mp4
Downloading video from YouTube: funny_cats.mp4
Playing video: VideoData(funny_cats.mp4)

Request to play video: funny_cats.mp4
Fetching video from cache: funny_cats.mp4
Playing video: VideoData(funny_cats.mp4)

Request to play video: epic_fail.mp4
First time download for: epic_fail.mp4
Downloading video from YouTube: epic_fail.mp4
Playing video: VideoData(epic_fail.mp4)

Request to play video: funny_cats.mp4
Fetching video from cache: funny_cats.mp4
Playing video: VideoData(funny_cats.mp4)

結論

透過 Proxy Pattern,我們成功實現了影片快取的功能,解決了頻寬消耗與延遲時間過長的問題。此外,代理物件與核心物件共享相同的介面,對客戶端保持透明性,進一步降低耦合性。此模式特別適用於需要控制對資源訪問的場景,例如遠端代理、安全代理與智慧代理,為系統提供了靈活性與可擴展性。




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • 💡 一台電腦操作多個 GitHub 帳號:最簡單快速的 SSH 設定方法
  • 🚀 如何使用 Excalidraw AI 快速生成專業級圖表,提升工作效率!
  • Setup Development Environment on a New macOS
  • Design Pattern (28) - Interpreter Pattern (解譯器模式)
  • Design Pattern (27) - Visitor Pattern (訪問者模式)