GoF设计模式(十一):Flyweight Pattern 享元模式

Flyweight Pattern 享元模式作为结构型的设计模式,其通过共享来解决大量细粒度对象的复用问题。Flyweight一词在拳击比赛中指的是特轻量级,故这里采用意译”享元”来更好的表达该设计模式的作用

abstract.jpeg

现实世界的指引

对一本英文书来说,其所用到的英文字母的种类数肯定不超过26x2个(区分大小写)。现在我们期望这本书变的更美丽一些,让每个字符都使用不同的颜色。例如对于书中的单词”assess”而言,我们期望a字符被印刷为白色,第一个s字符被印刷为红色,第二个s字符被印刷为黑色,……,第四个s字符被印刷为绿色。当我们使用活字印刷术时,实际上也只需要这52个不同英文字符的字块即可。至于同一种字符下不同的颜色的印刷效果,则可以在每次使用该字符的字块前先对其进行染色即可。而不会就同一种字符制造出各种颜色版本的字块,因为这样的话直接导致字块的数量爆炸

模式思想

通过前面例子我们实际上可以发现,在软件开发中我们同样会经常遇到这样的场景,我们需要某个类大量的对象实例,而这些实例很相似、相互之间只是有些细微的差别。这个时候,我们就可以通过分析,将对象的属性分为两种类型:内部状态、外部状态。前者变化有限,而后者变化繁多。将不同内部状态的对象实例通过共享的方式提供给client,来避免频繁创建对象实例。至于外部状态则是在client每次获取到具有某种内部状态的对象实例后,client自行按需设定的。这就是所谓的享元模式——即共享细粒度的对象

  • 抽象享元角色:其定义了具体享元对象需要实现的方法,包括对于外部状态的设置等。即下面例子中的ShapeFlyweight接口
  • 具体享元角色:其是抽象享元角色的具体实现。对于内部状态相同的具体享元角色,其是可以被共享的,即一次构造、重复使用。如下面例子中的Shape类
  • 非共享的具体享元角色:其虽然也是抽象享元角色的具体实现。但是其实例一般是无法共享的。由于该对象内部通常持有了多个具体享元角色,而后者则是可以共享的。故该角色通常又被称为复合享元角色。如下面例子中的Shapes类
  • 享元工厂角色:不论是具体享元角色还是非共享的具体享元角色,都不允许client直接创建相关实例。而是通过该工厂来创建、管理享元角色。具体地,当client需要一个享元对象时,首先会进行检查是否曾经创建过该对象,如果有,则不再重复创建,直接从缓存池中取出给client;如果没有,才会去创建返回给client,并将其放入缓存池中以便于日后的共享复用。如下面例子中的ShapeFlyweightFactory类

实现

可共享的具体享元角色

享元模式下,用的比较多的是可以共享的具体享元角色。这里我们通过享元模式来实现各种颜色的几何形状为例。这里的几何形状有两个属性:颜色、类型。类型的变化相对而言比较有限,比如正方形、圆形等。而颜色则显得变幻无穷。所以这里,我们认为对于几何形状而言,类型属于内部状态,而颜色则属于外部状态

现在,让我们先定义一个抽象享元角色ShapeFlyweight类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 抽象享元角色:形状享元
*/
public interface ShapeFlyweight {
/**
* 设置外部状态:颜色
*/
void setExtrinsicState(String color);

/**
* 获取形状信息
*/
void getInfo();
}

然后再来实现一个具体享元角色Shape类,可以看到其一方面支持通过构造器实现内部状态类型的注入,另一方面亦提供了外部状态颜色的设置方法以供client使用

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
/**
* 具体享元角色:形状
*/
@EqualsAndHashCode // Override hashCode Method By Lombok
public class Shape implements ShapeFlyweight{
/**
* 内部状态:形状类型
*/
private String type;

/**
* 外部状态:颜色
*/
private String color;

public Shape(String type) {
this.type = type;
}

/**
* 设置外部状态:颜色
* @param color
*/
@Override
public void setExtrinsicState(String color) {
this.color = color;
}

@Override
public void getInfo() {
String info = "[形状] Type:" + type + ", Color: " + color;
System.out.println(info);
}
}

现在就通过享元工厂ShapeFlyweightFactory来获取具体的享元角色了,可以看到工厂内部通过Map来缓存各种不同类型的具体享元角色Shape实例,实现对同一种形状类型实例的共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 享元工厂角色: 形状享元对象工厂
*/
public class ShapeFlyweightFactory {
/**
* Shape对象的缓存池
* key: type 形状类型; value: 该类型的形状对象
*/
private static Map<String, Shape> shapeMap = new HashMap<>();

/**
* Shape的工厂, 用于获取Shape
* @param type 形状类型
* @return
*/
public static Shape factory(String type) {
Shape shape = shapeMap.get(type);
if( shape==null ) {
shape = new Shape(type);
shapeMap.put(type, shape);
}
return shape;
}
}

现在client就可以通过工厂来获取各种类型的几何形状了,而对于外部状态则需要client在每次使用前按需设定

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
/**
* Flyweight Pattern 享元模式 Demo
*/
public class FlyweightPatternDemo {
public static void main(String[] args) {
System.out.println("--------------- Test 1: 可共享的享元对象Shape测试 ---------------");

// client需要一个红色的圆形
ShapeFlyweight redCircle = ShapeFlyweightFactory.factory("圆形");
redCircle.setExtrinsicState( "红色" ); // 设置外部状态(颜色)为红色
redCircle.getInfo();

// client需要一个蓝色的圆形
ShapeFlyweight blueCircle = ShapeFlyweightFactory.factory("圆形");
blueCircle.setExtrinsicState( "蓝色" ); // 设置外部状态(颜色)为蓝色
blueCircle.getInfo();

// client需要一个绿色的矩形
ShapeFlyweight greenRectangle = ShapeFlyweightFactory.factory("矩形");
greenRectangle.setExtrinsicState( "绿色" ); // 设置外部状态(颜色)为蓝色
greenRectangle.getInfo();

// client需要一个黑色的矩形
ShapeFlyweight blackRectangle = ShapeFlyweightFactory.factory("矩形");
blackRectangle.setExtrinsicState( "黑色" ); // 设置外部状态(颜色)为黑色
blackRectangle.getInfo();

System.out.println( "红色圆形 与 蓝色圆形 是否为同一个对象: " + (redCircle==blueCircle) );
System.out.println( "绿色矩形 与 黑色矩形 是否为同一个对象: " + (greenRectangle==blackRectangle) );
}
}

从测试结果我们也可以看到,从工厂中多次获得的某类型的几何图形实际上是同一个对象。即具体享元角色Shape的实例是可以被共享的

figure 1.png

不可共享的具体享元角色

如果client需要同一种颜色下的多种类型的形状,如果直接使用上面Shape的类虽然也可以,但是需要client多次调用比较繁琐。为此就可以引入一个复合享元角色Shapes类,其是不可共享的。可以看到该类中内部会持有一个容器shapeSet用于保存不同类型的Shape实例,并提供了add方法用于向容器进行添加操作。而在该类中设置外部状态的setExtrinsicState方法,显然需要对容器shapeSet各Shape实例均进行设置。值得一提的是,不可共享的含义指的是Shapes实例不可共享、复用,但Shapes的容器shapeSet中存放的Shape实例是可共享、复用的

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
/**
* 非共享的具体享元角色(复合享元角色): 多个形状的集合
*/
public class Shapes implements ShapeFlyweight {
/**
* 容器中存放各种不同的Shape形状
*/
private Set<Shape> shapeSet = new HashSet<>();

/**
* 添加形状Shape对象到容器shapeSet中
*/
public void add(Shape shape) {
shapeSet.add(shape);
}

/**
* 对容器shapeSet中各Shape形状均设置同一种颜色
* @param color
*/
@Override
public void setExtrinsicState(String color) {
for(Shape shape : shapeSet) {
shape.setExtrinsicState(color);
}
}

@Override
public void getInfo() {
for(Shape shape : shapeSet ) {
shape.getInfo();
}
}
}

同样Shapes实例也是需要通过工厂来获取的,而不允许client直接构造。故我们继续在ShapeFlyweightFactory类添加相应的工厂方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 享元工厂角色: 形状享元对象工厂
*/
public class ShapeFlyweightFactory {
...
/**
* Shapes的工厂,用于获取Shapes
* @param typeSet 类型集合
* @return
*/
public static Shapes factory(Set<String> typeSet) {
Shapes shapes = new Shapes(); // 此行即可说明Shapes对象是不共享的
for(String type : typeSet) {
// 但Shapes中的各Shape则是共享的
Shape shape = factory(type);
shapes.add(shape);
}
return shapes;
}

}

至此,client如果期望一次性拿到多种类型的几何形状,就可以通过Shapes实例很方便的实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Flyweight Pattern 享元模式 Demo
*/
public class FlyweightPatternDemo {
public static void main(String[] args) {
System.out.println("--------------- Test 2: 不可共享的享元对象Shapes测试 ---------------");

// client需要一个黄色的多边形集合
Set<String> typeSet1 = new HashSet<>();
typeSet1.addAll( Arrays.asList("三角形", "正方形", "长方形") );
ShapeFlyweight shapes1 = ShapeFlyweightFactory.factory( typeSet1 );
shapes1.setExtrinsicState("黄色");
shapes1.getInfo();

// client需要一个白色的形状集合
Set<String> typeSet2 = new HashSet<>();
typeSet2.addAll( Arrays.asList("圆形", "椭圆形","三角形") );
ShapeFlyweight shapes2 = ShapeFlyweightFactory.factory( typeSet2 );
shapes2.setExtrinsicState("白色");
shapes2.getInfo();
}
}

测试结果如下,符合预期

figure 2.png

最后补充说明下,该设计模型在实际开发中,一般应用的并不多。其使得系统变得较为复杂。在区分对象属性是内部状态还是外部状态的过程中,有时候并不容易。该设计模式比较典型的实践有Java的String类

参考文献

  1. Head First 设计模式 弗里曼著
0%