设计之禅——享元模式
引言
之前已经写了17篇关于设计模式的文章,而这些设计模式大都是为了降低代码之间的耦合,避免违反开闭原则,但它们大都有同样的一个缺点,产生更多的类和对象,如果数量达到一定程度,就会导致系统性能降低,而今天要讲的这个模式就是为了解决这样的一个问题,它就是享元模式。
正文
定义
享元模式是为了尽可能地划分出细粒度对象并复用,减少内存资源的占用,因而它是一种优化系统性能的模式。
为什么是细粒度的对象复用呢?因为对象越大,对象的复用率并不高(在系统运行时你不可能总是会使用一些很大的对象),如果将其缓存下来,对系统而言反而是负担。并且有可能那些特大的对象是我们自己因为方便而创造出来的,承担了很多的责任,而你常用的可能只是其中的一部分功能,所以我们需要尽可能的划分出细粒度对象并缓存起来。因其轻量级特性,享元模式又称为蝇量模式。
好吧,那照你这么说,我只需要缓存对象就行了,难道这就是享元模式么?
当然不是。将对象细粒度划分后,有其固有不变的属性,也有些属性可能会随着环境的改变而改变,如果将这些属性都放到一个对象里,那还怎么复用这个对象呢?享元模式则是将属性进行划分,固有属性放在对象内部,称为内在属性,而会变化的部分则放在对象的外部,称为外在属性,并通过方法参数传递进去,这样我们就能复用这个对象了。
你这么说我好像懂了,但还是很模糊。
是的,概念总是抽象的,我举个例子。比如你有一张图片,需要显示在电脑桌面的不同位置,那么只需要将坐标位置属性抽离出来,根据你的设置动态显示就行了,这里坐标就是外部会变化的属性。
这样就很清楚了,那我在网上看到很多博客都说Java中的字符串就是享元模式的应用,你觉得呢?
我也看到了,但个人不太认同这样的观点,String只是利用了缓存,并不能说明使用了享元模式,并且字符串的固有属性和外部属性是什么呢?字符串本身内容吗?那使用享元模式实现就有些多此一举了。不过不用太纠结这一点,我们的重点是学习享元模式。
那应该如何实现享元模式呢?
我们先来看看其类图:
从类图上看享元模式具有三个角色:
- 抽象享元接口或抽象类
- 具体享元类(需要共享的对象)
- 享元工厂
享元模式常常需要配合工厂模式使用,使用工厂是为了维护一个共享对象池,将对象缓存到该池中,该池一般使用类似Map的键值对来实现,并提供一个方法供外部获取池中对象,如果池中没有该对象,就新建一个并放入池中。
说了这么多,快让我看代码吧!
好的,当然没问题。我想大多数人都知道CS这款游戏,曾经火遍全球。想想看如果每个角色都新建一个对象是不是很浪费内存呢?毕竟它们基本上一样,只是需要经常切换武器装备啊。这就可以使用享元来达到节省内存的目的了啊,就像下面这样:
Coding
public abstract class AbstractPlayer {
private String weapon;
protected String mission;
public void assignWeapon(String weapon) {
System.out.println("使用武器:" + weapon);
this.weapon = weapon;
}
public void execute() {
System.out.println("execute mission: " + mission);
}
}
public class Police extends AbstractPlayer {
public Police() {
this.mission = "kill terrorist!";
}
}
public class Terrorist extends AbstractPlayer {
public Terrorist() {
this.mission = "kill police!";
}
}
首先我创建了一个抽象的玩家角色类,并实现了土匪和警察两个具体的类,对于这两类玩家而言,任务是不会改变的:土匪杀警察,警察杀土匪。而对所有角色而言,武器是随时都会更换的,因此抽离到外部并通过参数传入进来。而对象的创建工厂如下:
public class PlayerFactory {
private static Map<String, AbstractPlayer> pool = new HashMap<>();
public static AbstractPlayer getPlayer(String type) throws Exception {
AbstractPlayer player = pool.get(type);
if (player == null) {
switch (type) {
case "P":
System.out.println("Create police: ");
player = new Police();
break;
case "T":
System.out.println("Create terrorist: ");
player = new Terrorist();
break;
default:
throw new Exception("无此类型的玩家!");
}
}
return player;
}
}
很简单,玩家调用getPlayer方法并传入对应的标识创建警察或是土匪。
public class CS {
// 角色表示
private static String[] players = {"T", "P"};
// 武器类型
private static String[] weapons = {"AK-47", "Knife", "Sniper"};
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
AbstractPlayer player = PlayerFactory.getPlayer(getPlayer(i));
player.assignWeapon(getWeapon(i));
player.execute();
}
}
/**
* 随机创建土匪或是警察
*/
private static String getPlayer(int i) {
return players[i % players.length];
}
/**
* 随机获取武器
*/
private static String getWeapon(int i) {
return weapons[i % weapons.length];
}
}
这里看起来实现非常简单,但在实际开发中,要精确把控区分可共享域和不可共享域是非常难的,并且这里只见到了单纯享元模式,另外还有复合享元模式,笔者不再打算展开讲述,感兴趣的可自行查阅资料。
总结
享元模式在提高对象复用性,节省内存方面非常有用,但在复用度不高时,并不建议使用享元模式,因为享元工厂本身需要维护一个共享池,浪费资源。并且,需要将变化属性外部化,使得程序逻辑变得复杂难以理解,同时,外部属性也会导致运行时间变长。
参考
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 3种方式限制ip访问Oracle数据库
- 服务发现之consul的介绍、部署和使用
- Mybatis处理查询map 为null 导致查询map无该key对象
- 深入对比TOML,JSON和YAML
- consul配置参数大全、详解、总结
- 跳跃表原理和实现
- 你动、蒙娜丽莎跟着一起动,OpenCV这么用,表情口型造假更难防了
- 好用的PHP高性能多并发restful的HTTP Client
- Golang程序调试工具介绍(gdb vs dlv)
- tcp_tw_reuse、tcp_tw_recycle注意事项
- PHP性能规范
- PHP代码规范
- php调试利器之phpdbg
- 【DB笔试面试860】在Oracle中,如何判断Oracle是32位还是64位?
- JsonPath实践(四)