26.项目多也别傻做 - 享元模式 (大话设计模式Kotlin版)

例子与代码均来自《大话设计模式》程杰,简单记录加深印象。
26.项目多也别傻做 - 享元模式 (大话设计模式Kotlin版)_第1张图片

项目多也别傻做

问题情景
最近忙得很,在给一些私营业主做网站,做好一个产品展示网站需要一个星期,包括购买服务器和搭建数据库!但是随着外快越来越多,他们的需求有的是新闻发布式的网站、有希望是博客形式的,还有的只是在原来产品展示的图片上加说明形式的,而且他们都希望费用大大降低。
他们的需求差别不大,难道我必须给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图:

26.项目多也别傻做 - 享元模式 (大话设计模式Kotlin版)_第2张图片
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图:
26.项目多也别傻做 - 享元模式 (大话设计模式Kotlin版)_第3张图片

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个空位可以放棋子,那如果按照常规的面向对象编程,每盘棋都有可能有两三百个棋子对象产生,一台服务器就很难支持更多的玩家玩游戏了,毕竟内存空间是有限的。如果用了享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例。

围棋:
内部状态:棋子的颜色(只有黑白两种不变的状态)
外部状态:各个棋子之间差别就是在棋盘上的位置不同,所以棋子的方位坐标是外部状态。

你可能感兴趣的:(设计模式)