设计模式(13):享元模式FLYWEIGHT

享元模式

意图

运用共享技术有效地支持大量细粒度的对象。


使用场景

Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都成立时使用Flyweight模式:

  • 一个应用程序使用了大量的对象。
  • 完全由于使用大量的对象,造成很大的存储开销。
  • 对象的大多数状态都可变为外部状态。
  • 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
  • 应用程序不依赖于对象标识。由于Flyweight对象可以被共享,对于概念上明显有别的对
    象,标识测试将返回真值。

结构

享元模式结构如下
ShowImage

  • Flyweight

    – 描述一个接口,通过这个接口flyweight可以接受并作用于外部状态。

  • ConcreteFlyweight

    – 实现Flyweight接口,并且为内部状态(如果有)提供储存。一个ConcreteFlyweight对象必须是可以共享的。它所存储的任何状态都应该是内部的,也就是说,它必须独立于ConcreteFlyweight对象的上下文(Context)。

  • UnsharedConcreteFlyweight

    – 并非所有的Flyweight子类都需要被共享。Flyweight接口提供了共享的能力,但是它并不是强制共享。在Flyweight的某些层次,UnsharedConcreteFlyweight对象通常会将ConcreteFlyweight对象当做子节点。

  • FlyweightFactory

    – 创建并且管理Flyweight对象

    – 确保flyweights对象能被合理地共享。当一个client请求一个flyweight时,FlyweightFactory对象提供一个已经存在的实例或者新建一个(如果一个都不存在的话)。

  • Client

    – 维护一个对flyweight的引用

    – 计算或者储存flyweight对象的外部状态

协作

  • flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部状态则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该外部状态传递给它。
  • Client不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。

优点

使用Flyweight模式时,传输、查找和/或计算外部状态都会产生运行时的开销,尤其当flyweight原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的flyweight越多,空间节省也就越大。
存储节约由以下几个因素决定:

  • 因为共享,实例总数减少的数目
  • 对象内部状态的平均数目
  • 外部状态是计算的还是存储的

共享的Flyweight越多,存储节约也就越多。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大。所以,可以用两种方法来节约存储:用共享减少内部状态的消耗,用计算时间换取对外部状态的存储。

享元模式实现(Implement)

案例

DOTA2是风靡全球的电子竞技游戏。在游戏中,地图一共分为上中下三路,每条路会出现大量的小兵,玩家可以通过击杀小兵获取金钱和经验。那么在这么一整张地图之中,如果对每一个小兵都产生一个新的对象,那么系统会消耗大量的资源,享元模式就可以在这里派上用处,通过共享来节约对象的产生。

代码实现

近战小兵抽象类,也就是Flyweight接口,内部状态是公共而且固定的,包括小兵视野,攻击距离以及经验,外部状态是移动的位置

1
2
3
4
5
6
7
8
9
public abstract class MeleeCreep {
/*内部状态,存在Flyweight对象中并且共享*/
protected int sightRange;
protected int attackRange;
protected int experience;
/*外部状态,存在client对象中,并且被传递到Flyweight*/
abstract public void move(double currentX, double currentY, double newX, double newY);
}

天辉方小兵,ConcreteFlyweight类之一

1
2
3
4
5
6
7
public class RadiantMeleeCreep extends MeleeCreep {
@Override
public void move(double currentX, double currentY, double newX, double newY) {
System.out.println("Before move X = " + currentX + ",Y = " + currentY);
System.out.println("After move X = " + newX + ",Y = " + newY);
}
}

夜魇方小兵,ConcreteFlyweight类之一

1
2
3
4
5
6
7
public class DireMeleeCreep extends MeleeCreep {
@Override
public void move(double currentX, double currentY, double newX, double newY) {
System.out.println("Before move X = " + currentX + ",Y = " + currentY);
System.out.println("After move X = " + newX + ",Y = " + newY);
}
}

小兵工厂类,可以设置成单例的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class CreepsFactory {
private static Map<String, MeleeCreep> flyweights = new Hashtable<>();
/*If key exists in Map, return flyweight from Map*/
public MeleeCreep getCreeps(String key) {
if (flyweights.containsKey(key)) return flyweights.get(key);
/*If key does not exist in Map, create flyweight*/
MeleeCreep meleeCreep;
switch (key) {
case "Radiant":
meleeCreep = new RadiantMeleeCreep();
meleeCreep.sightRange = 750;
meleeCreep.attackRange = 100;
meleeCreep.experience = 40;
break;
case "Dire":
meleeCreep = new DireMeleeCreep();
meleeCreep.sightRange = 750;
meleeCreep.attackRange = 100;
meleeCreep.experience = 40;
break;
default:
throw new IllegalArgumentException("Unsupported meleeCreep type");
}
flyweights.put(key, meleeCreep);
return meleeCreep;
}
//Use Singleton in CreepsFactory
private static class CreepsFactoryHolder {
private static final CreepsFactory INSTANCE = new CreepsFactory();
}
private CreepsFactory() {
}
public static CreepsFactory getInstance() {
return CreepsFactoryHolder.INSTANCE;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class FlyweightDemoTest {
@Test
public void creepsTest() {
//小兵工厂
CreepsFactory creepsFactory = CreepsFactory.getInstance();
//通过小兵工厂获取实例
MeleeCreep direMeleeCreepOnTopLane = creepsFactory.getCreeps("Dire");
MeleeCreep direMeleeCreepOnMiddleLane = creepsFactory.getCreeps("Dire");
MeleeCreep direMeleeCreepOnBottomLane = creepsFactory.getCreeps("Dire");
MeleeCreep radiantMeleeCreepOnTopLane = creepsFactory.getCreeps("Radiant");
MeleeCreep radiantMeleeCreepOnMiddleLane = creepsFactory.getCreeps("Radiant");
MeleeCreep radiantMeleeCreepOnBottomLane = creepsFactory.getCreeps("Radiant");
//小兵进行移动
System.out.println("上路夜魇小兵移动");
direMeleeCreepOnTopLane.move(100, 100, 0, 100);
System.out.println("中路夜魇小兵移动");
direMeleeCreepOnMiddleLane.move(100, 100, 50, 50);
System.out.println("下路夜魇小兵移动");
direMeleeCreepOnBottomLane.move(100, 100, 100, 0);
System.out.println("上路天辉小兵移动");
radiantMeleeCreepOnTopLane.move(0, 0, 0, 100);
System.out.println("中路天辉小兵移动");
radiantMeleeCreepOnMiddleLane.move(0, 0, 50, 50);
System.out.println("下路天辉小兵移动");
radiantMeleeCreepOnBottomLane.move(0, 0, 100, 0);
//工厂只维护两个小兵对象
assertThat(direMeleeCreepOnTopLane.hashCode(),
both(is(direMeleeCreepOnMiddleLane.hashCode()))
.and(is(direMeleeCreepOnBottomLane.hashCode())));
assertThat(radiantMeleeCreepOnTopLane.hashCode(),
both(is(radiantMeleeCreepOnMiddleLane.hashCode()))
.and(is(radiantMeleeCreepOnBottomLane.hashCode())));
}
}

外部状态小兵位置通过client来控制,内部状态保存在对象之中共享,这样整张地图只有两个小兵对象,节约了大量对象创建的开销,通过时间来换取空间。

总结

享元模式有优点也有缺点。享元模式可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能;享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。但同时,享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。

参考代码

------ 本文结束 ------

版权声明


BillyYccc's blog by Billy Yuan is licensed under a Creative Commons BY-NC-SA 4.0 International License.
本文原创于BillyYccc's Blog,转载请注明原作者及出处!