起步
享元模式属于结构型。“元”有单元,或者内容单元的意思,基本上你可以认为是“同一个对象”。所以享元模式可以理解为共享同一个对象。
听起来是不是很像单例模式?实则不同!
享元模式
现在系统需要增添一个加载配置文件的功能,就是把磁盘里的配置文件内容写入到内存中,供系统使用。功能很简单,代码很容易,话不说多:
class JsonParser(object):
def parse(self, path) -> Dict:
... # 解析配置文件
return { # 返回解析后的配置信息
"DB": ...,
"Base": ...,
}
class ConfigSource(object):
def load(self, file) -> Dict:
parser = JsonParser()
return parser.parse(file)
某个功能需要用到配置信息,就实例化 ConfigSource,然后调用 load 即可。但这里有个问题,如果系统到处都要需要获取配置信息,每需要一次就调用 load,就会发生一次磁盘读事件;其次,如果调用期间配置文件发生了变动,前后调用 load 拿到的配置内容就不一样了,可能会造成系统瞎运行,严重的甚至崩溃;而如果运气好,一切正常运行,但是每个需要读取配置的对象都持有一个单独的配置信息,只是这些配置信息都是相同的,那就造成了内存浪费。
为解决上述问题,我们就可以使用享元模式了。最简单的享元模式仅需对 ConfigSource 做稍稍修改。
class ConfigSource(object):
config = {}
def load(self, file) -> Dict:
# 如果没有配置
if not self.config:
parser = JsonParser()
config = parser.parse(file)
self.__class__.config = config
return self.config
现在就能保证系统各处使用的配置信息为同一份,既节约了内存,又减少了 IO 事件。
与单例的区别
就上述代码来说,乍一看,这不就是单例模式吗?!
设计模式之间的区别不在于代码实现,而在于通过什么方式解决了什么问题。单例模式强调一个类只能有一个实例对象,而享元模式则没有此限制;前者是为了限制实例对象的数量,后者是为了节省内存。
譬如现在已知系统每个小时配置信息就会更变一次,不同日期的相同时间配置信息相同。则代码可以如下设计:
class ConfigSource(object):
config = {}
def load(self, file) -> Dict:
# 获取当前小时
cur_hour = datetime.now().hour
# 如果没有相关磁盘信息,从磁盘获取
if cur_hour not in self.config:
parser = JsonParser()
self.config[cur_hour] = parser.parse(file)
return self.config[cur_hour]
如果 24 小时过去,config 里边就存放了 24 个配置信息,这跟单例模式有明显的不同。
与“池化”的区别
我们常见的“池化”有线程池,连接池等等,也是先实例化一定数量的对象,等需要的时候取出来用。好像也是有点享元的意思。
然而,享元模式是允许同一时间点上,被多个客户端持有。拿上述的配置文件实例来说,假设当前配置信息为 config\_a,对于客户端 clientA, clientB,clientC 等等,都可以同时拥有 config\_a,这没毛病。但对于“池化”来说,在一个时间点上,一个对象只能被一个客户端持有。譬如连接池中的连接对象,只有用完以后在放回连接池,才能被其他客户端使用。
总结
享元模式是为了复用对象,节省内存而存在的。是否使用享元模式还有一个限制条件:享元对象是不可变对象。如果配置信息允许被客户端改变,则很可能造成其他客户端持有的配置信息也被改变,在某些时候我们不需要这种改变,就不应该选择享元模式。
参考
- 感谢 极客时间王争老师的《设计模式之美:享元模式》
还不快抢沙发