26.項目多也別傻做 - 享元模式 (大話設計模式Kotlin版)

例子與代碼均來自《大話設計模式》程傑,簡單記錄加深印象。
設計模式

項目多也別傻做

問題情景
最近忙得很,在給一些私營業主做網站,做好一個產品展示網站需要一個星期,包括購買服務器和搭建數據庫!但是隨着外快越來越多,他們的需求有的是新聞發佈式的網站、有希望是博客形式的,還有的只是在原來產品展示的圖片上加說明形式的,而且他們都希望費用大大降低。
他們的需求差別不大,難道我必須給n個不同形式的網站copy一套代碼和創建100個數據庫嗎?
如果是那樣的話,如果出現bug你豈不是要修改n遍,那維護量就太可怕了!

用傳統的方式來網站

WebSite 網站類

/**
 * @create on 2020/5/23 22:44
 * @description 網站類
 * @author mrdonkey
 */
class WebSite constructor(private val name: String) {
    /**
     * [name] 網站名
     */
    fun use() {
        println("網站分類:$name")
    }
}

Client 客戶端

/**
 * @create on 2020/5/23 22:46
 * @description 客戶端
 * @author mrdonkey
 */
class Client {
    companion object {
        @JvmStatic
        fun main(vararg args: String) {
            WebSite("產品展示").use()
            WebSite("產品展示").use()
            WebSite("產品展示").use()
            WebSite("博客").use()
            WebSite("博客").use()
            WebSite("博客").use()
        }
    }
}

A: 上面的客戶端要做三個產品展示、三個博客類型的網站,就需要六個網站的實例,其實本質上是一樣的代碼,如果網站增多,實例也就隨之增多!
B: 能不能共用一套代碼呢?
A: 當然可以,比如現在大型的博客網站或電子商務網站,裏面每一個博客或者商家也可以理解爲是一個小的網站,但它們是如何做到區別的?
B: 利用用戶的ID來區分不同的用戶 ,具體的數據和模板可以不同,但是核心代碼和數據庫確是共享的。
A: 是的,如果需要的網站結果相似度很高,而且非高訪問的網站,用傳統的方式相當於一個相同的網站的生成許多份實例,這就是造成大量冗餘,而且後面不好維護。那如果整合到一個網站中,共享其相關的代碼和數據,減少服務器資源,而對一代碼,由於只是一份實例,維護和拓展都更加容易。
B: 那如何做到共享一份實例呢?

享元模式

在弄明白共享代碼之前。我們先談一個設計模式——享元模式

享元模式(Flyweight): 運用共享技術有效地支持大量細粒度的對象。

享元模式的UML圖:

享元模式UML圖
Flyweight類,所有具體享元類的超類

/**
 * @create on 2020/5/23 22:53
 * @description 所有具體享元類的超類,通過這個接口,Flyweight可以接受並作用於外部狀態
 * @param [extrinsicstate] 外部狀態
 * @author mrdonkey
 */
abstract class Flyweight {
    abstract fun operation(extrinsicstate: Int)
}

ConcreteFlyweight 類,具體的Flyweight

/**
 * @create on 2020/5/23 22:57
 * @description 具體的flyweight
 * @author mrdonkey
 */
class ConcreteFlyweight : Flyweight() {
    override fun operation(extrinsicstate: Int) {
        println("具體Flyweight:$extrinsicstate")
    }
}

UnSharedConcreteFlyweight類,指那些不需要共享的Flyweight子類

/**
 * @create on 2020/5/23 22:58
 * @description 指那些不需要共享的Flyweight子類
 * @author mrdonkey
 */
class UnsharedConcreteFlyweight : Flyweight() {
    override fun operation(extrinsicstate: Int) {
        println("不共享的具體Flyweight:$extrinsicstate")
    }
}

FlyweightFactory 類 享元工長,用來創建並管理Flyweight對象

 * @create on 2020/5/23 22:59
 * @description 享元工常,用來創建並管理Flyweight對象
 * @author mrdonkey
 */
class FlyweightFactory {
    private val flyweights = hashMapOf<String, Flyweight>()

    /**
     * 初始化工廠,先生成3個共享實例
     */
    init {
        flyweights["1"] = ConcreteFlyweight()
        flyweights["2"] = ConcreteFlyweight()
        flyweights["3"] = ConcreteFlyweight()
    }

    /**
     * 根據客戶端請求,獲得已生成的實例
     */
    fun getFlyweight(key: String): Flyweight? {
        return flyweights[key]
    }

    /**
     * 獲得網站分類總數
     */
    fun getWebSiteCount() = flyweights.size

}

Client 客戶端類

/**
 * @create on 2020/5/23 23:06
 * @description 客戶端代碼
 * @author mrdonkey
 */
class Client {
    companion object {
        @JvmStatic
        fun main(vararg args: String) {
            //外部狀態
            var extrinsicstate = 22
            FlyweightFactory().apply {
                this.getFlyweight("1")?.operation(--extrinsicstate)
                this.getFlyweight("2")?.operation(--extrinsicstate)
                this.getFlyweight("3")?.operation(--extrinsicstate)
            }
            UnsharedConcreteFlyweight().operation(--extrinsicstate)
        }
    }
}

測試結果:

具體Flyweight:21
具體Flyweight:20
具體Flyweight:19
不共享的具體Flyweight:18

A: FlyweightFactory 根據客戶端返回早已生成好的對象,但一定要事先生成對象的實例嗎?
B: 不一定需要,初始化完全可以什麼都不做,到需要時,判斷對象是否爲null再生成即可
A: 爲什麼要有UnSharedConcreteFlyweight的存在呢?
B: 儘管大部分時間都需要共享對象來降低內存的損耗,但是個別時候也有可能不需要共享的,那麼此時的UnSharedConcreteFlyweight子類就有存在的必要了,它可以解決那些不需要共享對象的問題。

網站共享代碼

參照上面的基本共享模式樣例改寫傳統做網站的代碼。
首先網站得有一個抽象類和n個具體的網站類,然後通過網站工廠來產生對象。

第二版網站代碼

WebSite 網站抽象類

/**
 * @create on 2020/5/23 23:14
 * @description 網站抽象類
 * @author mrdonkey
 */
abstract class WebSite {
    /**
     * 使用
     */
    abstract fun use()
}

ConcreteWebSite 具體網站類

/**
 * @create on 2020/5/23 23:15
 * @description 具體網站類
 * @author mrdonkey
 */
class ConcreteWebSite(val name: String) : WebSite() {
    override fun use() {
        println("網站分類:$name")
    }
}

WebSiteFactory 網站工廠

/**
 * @create on 2020/5/23 23:16
 * @description 網站工廠類
 * @author mrdonkey
 */
class WebSiteFactory {
    //網站實例管理
    private val flyweights = hashMapOf<String, WebSite>()

    /**
     * 獲得網站分類
     * 如果實例不存在,則創建
     */
    fun getWebSiteCategory(key: String): WebSite {
        return flyweights[key] ?: ConcreteWebSite(key).apply { flyweights[key] = this }

    }

    /**
     * 獲得網站分類總數
     */
    fun getWebSiteCount() = flyweights.size

}

Client 客戶端

/**
 * @create on 2020/5/23 23:21
 * @description 客戶端代碼
 * @author mrdonkey
 */
class Client {
    companion object {
        @JvmStatic
        fun main(vararg args: String) {
            WebSiteFactory().apply {
                this.getWebSiteCategory("產品展示").use()
                this.getWebSiteCategory("產品展示").use()
                this.getWebSiteCategory("產品展示").use()
                this.getWebSiteCategory("博客").use()
                this.getWebSiteCategory("博客").use()
                this.getWebSiteCategory("博客").use()
                println("網站分類總數爲:${getWebSiteCount()}")
            }
        }
    }
}

測試結果:

網站分類:產品展示
網站分類:產品展示
網站分類:產品展示
網站分類:博客
網站分類:博客
網站分類:博客
網站分類總數爲:2

A:這樣寫基本實現了享元模式的共享對象的目的,也就是說不管創建了幾個網站,只要是‘產品展示’都是一樣的,只要是‘博客’也是完全相同的實例,但是有個問題,你給別人做網站,他們的不是同一家公司,數據不會完全相同,所以他們都應該有不同的賬號,你怎麼辦?
B:啊?對的,實際上上面寫的代碼沒有體現對象間的不同,只體現了他們共享的部分(相同的部分)

內部狀態與外部狀態

內部狀態: 在享元對象內部並且不會隨着環境改變而改變的共享部分。
外部狀態: 在享元對象內部隨環境改變而改變的、不可以共享的狀態。

享元模式可以避免大量非常相似的類的開銷。在程序設計中,有時需要生成大量細粒度的類實例來表示數據。如果能發現這些實例除了幾個參數外基本相同的,有時就能夠受大幅度地減少需要實例化的類的數量。如果能把哪些參數移到類實例的外面,在方法調用時傳遞進來,就可以通過共享大幅度減少單個實例的數量。

也就是說,享元模式的Flyweight執行時所需要的狀態有內部也有外部的,內部狀態存儲與ConcreteFlyweight中,而外部狀態則應該考慮由客戶端對象存儲或計算,當調用Flyweight對象操作時,將該狀態傳遞給它。(概括:內部狀態存在共享對象之中,外部狀態通過作爲共享對象的入參傳入,外部狀態是作爲共享對象的區別

在第二版網站代碼中,只體現了網站的共享部分,也就是內部狀態一致的情形,而沒有體現同一個共享狀態之間的區別,即用外部狀態來區分(當某個客戶端調用時,調用方法傳遞外部狀態)

客戶的賬號,就是外部狀態!

第三版網站代碼

第三版的UML圖:
第三版的UML圖

User 用戶類,是“網站”類的外部狀態

/**
 * @create on 2020/5/23 23:27
 * @description 用戶類,用於網站的客戶賬號,是"網站"類的外部狀態 [name] 用戶名
 * @author mrdonkey
 */
class User(val name: String)

WebSite 網站抽象類

/**
 * @create on 2020/5/23 23:26
 * @description 網站抽象類
 * @author mrdonkey
 */
abstract class WebSite {
    abstract fun use(user: User)
}

ConcreteWebSite具體網站類

/**
 * @create on 2020/5/23 23:15
 * @description 具體網站類
 * @author mrdonkey
 */
class ConcreteWebSite(val name: String) : WebSite() {
    override fun use(user: User) {
        println("網站分類:$name 用戶:${user.name}")
    }
}

WebSiteFactory 網站工廠類

/**
 * @create on 2020/5/23 23:16
 * @description 網站工廠類
 * @author mrdonkey
 */
class WebSiteFactory {
    private val flyweights = hashMapOf<String, WebSite>()

    /**
     * 獲得網站分類
     * 如果實例不存在,則創建
     */
    fun getWebSiteCategory(key: String): WebSite {
        return flyweights[key] ?: ConcreteWebSite(key).apply { flyweights[key] = this }

    }

    /**
     * 獲得網站分類總數
     */
    fun getWebSiteCount() = flyweights.size

}

Client 客戶端

/**
 * @create on 2020/5/23 23:21
 * @description 客戶端代碼
 * @author mrdonkey
 */
class Client {
    companion object {
        @JvmStatic
        fun main(vararg args: String) {
            WebSiteFactory().apply {
                this.getWebSiteCategory("產品展示").use(User("孫悟空"))
                this.getWebSiteCategory("產品展示").use(User("豬八戒"))
                this.getWebSiteCategory("產品展示").use(User("沙悟淨"))
                this.getWebSiteCategory("博客").use(User("白龍馬"))
                this.getWebSiteCategory("博客").use(User("白骨精"))
                this.getWebSiteCategory("博客").use(User("唐僧"))
                println("網站分類總數爲:${getWebSiteCount()}")
            }
        }
    }
}

測試結果:

網站分類:產品展示 用戶:孫悟空
網站分類:產品展示 用戶:豬八戒
網站分類:產品展示 用戶:沙悟淨
網站分類:博客 用戶:白龍馬
網站分類:博客 用戶:白骨精
網站分類:博客 用戶:唐僧
網站分類總數爲:2

結果顯示,儘管給了六個不同用戶使用網站,但實際只有兩個網站的實例。
通過user這個外部狀態來區分共享部分的不同,後期可以根據這個外部狀態來做一些差異,比如加載這個用戶的信息等等。。

享元模式的應用

應用場景

  1. 如果一個程序使用了大量的對象,而大量的這些對象造成了很大的存儲開銷時就應該考慮使用。
  2. 對象的大多數狀態可以外部狀態化,如果刪除了對象的外部狀態,那麼就可以用相對較少的共享對象來取代很多組對象,此時可以考慮使用享元模式。
  3. 爲了使對象可以共享,需要將一些狀態外部化,這使得程序的邏輯複雜化。因此,應當在有足夠多的對象實例可供共享時才值得使用共享模式。

使用效果

用了享元模式,有共享對象,實例總數大大減少,如果共享對象增多,存儲節約就更多,節約量隨着共享狀態的增多而增多。

Kotlin中的共享模式

在kotlin中,字符串String中也運用到了享元模式。舉個例子,使用 == 來比較 String 引用類型的引用是否相等。

     val a = "你好"
     val b = "你好"
     val c = String(StringBuffer("你好"))
     println("a==b ${a == b}")
     println("c==b ${c == b}")

結果:

a==b true
c==b true

A: 爲什麼兩個字符串的引用是一樣的?
B: 如果每次創建字符串對象時,都需要創建一個新的字符串對象的話,內存開銷會很大,所以如果第一次創建了字符串對象a,下次再創建b時只是把引用指向常量池中的‘你好’,這就實現了‘你好’在內存中的共享。

java 解釋推薦文章:String s = new String(" a ") 到底產生幾個對象?

五子棋、圍棋的共享模式

一盤棋理論上有361個空位可以放棋子,那如果按照常規的面向對象編程,每盤棋都有可能有兩三百個棋子對象產生,一臺服務器就很難支持更多的玩家玩遊戲了,畢竟內存空間是有限的。如果用了享元模式來處理棋子,那麼棋子對象就可以減少到只有兩個實例。

圍棋:
內部狀態:棋子的顏色(只有黑白兩種不變的狀態)
外部狀態:各個棋子之間差別就是在棋盤上的位置不同,所以棋子的方位座標是外部狀態。

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