例子與代碼均來自《大話設計模式》程傑,簡單記錄加深印象。
項目多也別傻做
問題情景
最近忙得很,在給一些私營業主做網站,做好一個產品展示網站需要一個星期,包括購買服務器和搭建數據庫!但是隨着外快越來越多,他們的需求有的是新聞發佈式的網站、有希望是博客形式的,還有的只是在原來產品展示的圖片上加說明形式的,而且他們都希望費用大大降低。
他們的需求差別不大,難道我必須給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圖:
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圖:
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這個外部狀態來區分共享部分的不同,後期可以根據這個外部狀態來做一些差異,比如加載這個用戶的信息等等。。
享元模式的應用
應用場景
- 如果一個程序使用了大量的對象,而大量的這些對象造成了很大的存儲開銷時就應該考慮使用。
- 對象的大多數狀態可以外部狀態化,如果刪除了對象的外部狀態,那麼就可以用相對較少的共享對象來取代很多組對象,此時可以考慮使用享元模式。
- 爲了使對象可以共享,需要將一些狀態外部化,這使得程序的邏輯複雜化。因此,應當在有足夠多的對象實例可供共享時才值得使用共享模式。
使用效果
用了享元模式,有共享對象,實例總數大大減少,如果共享對象增多,存儲節約就更多,節約量隨着共享狀態的增多而增多。
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個空位可以放棋子,那如果按照常規的面向對象編程,每盤棋都有可能有兩三百個棋子對象產生,一臺服務器就很難支持更多的玩家玩遊戲了,畢竟內存空間是有限的。如果用了享元模式來處理棋子,那麼棋子對象就可以減少到只有兩個實例。
圍棋:
內部狀態:棋子的顏色(只有黑白兩種不變的狀態)
外部狀態:各個棋子之間差別就是在棋盤上的位置不同,所以棋子的方位座標是外部狀態。