引言
之前已经写了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];
}
}
这里看起来实现非常简单,但在实际开发中,要精确把控区分可共享域和不可共享域是非常难的,并且这里只见到了单纯享元模式,另外还有复合享元模式,笔者不再打算展开讲述,感兴趣的可自行查阅资料。
总结
享元模式在提高对象复用性,节省内存方面非常有用,但在复用度不高时,并不建议使用享元模式,因为享元工厂本身需要维护一个共享池,浪费资源。并且,需要将变化属性外部化,使得程序逻辑变得复杂难以理解,同时,外部属性也会导致运行时间变长。
参考
- GeeksForGeeks
- 享元模式