问题情景
最近忙得很,在给一些私营业主做网站,做好一个产品展示网站需要一个星期,包括购买服务器和搭建数据库!但是随着外快越来越多,他们的需求有的是新闻发布式的网站、有希望是博客形式的,还有的只是在原来产品展示的图片上加说明形式的,而且他们都希望费用大大降低。
他们的需求差别不大,难道我必须给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): 运用共享技术有效地支持大量细粒度的对象。
/**
* @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对象操作时,将该状态传递给它。(概括:内部状态存在共享对象之中,外部状态通过作为共享对象的入参传入,外部状态是作为共享对象的区别)
在第二版网站代码中,只体现了网站的共享部分,也就是内部状态一致的情形,而没有体现同一个共享状态之间的区别,即用外部状态来区分(当某个客户端调用时,调用方法传递外部状态)
客户的账号,就是外部状态!
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中,字符串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个空位可以放棋子,那如果按照常规的面向对象编程,每盘棋都有可能有两三百个棋子对象产生,一台服务器就很难支持更多的玩家玩游戏了,毕竟内存空间是有限的。如果用了享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例。
围棋:
内部状态:棋子的颜色(只有黑白两种不变的状态)
外部状态:各个棋子之间差别就是在棋盘上的位置不同,所以棋子的方位坐标是外部状态。