Flyweight Pattern 享元模式作为结构型的设计模式,其通过共享来解决大量细粒度对象的复用问题。Flyweight一词在拳击比赛中指的是特轻量级,故这里采用意译”享元”来更好的表达该设计模式的作用
现实世界的指引
对一本英文书来说,其所用到的英文字母的种类数肯定不超过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 public class Shape implements ShapeFlyweight{
private String type;
private String color;
public Shape(String type) { this.type = type; }
@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 {
private static Map<String, Shape> shapeMap = new HashMap<>();
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
|
public class FlyweightPatternDemo { public static void main(String[] args) { System.out.println("--------------- Test 1: 可共享的享元对象Shape测试 ---------------");
ShapeFlyweight redCircle = ShapeFlyweightFactory.factory("圆形"); redCircle.setExtrinsicState( "红色" ); redCircle.getInfo();
ShapeFlyweight blueCircle = ShapeFlyweightFactory.factory("圆形"); blueCircle.setExtrinsicState( "蓝色" ); blueCircle.getInfo();
ShapeFlyweight greenRectangle = ShapeFlyweightFactory.factory("矩形"); greenRectangle.setExtrinsicState( "绿色" ); greenRectangle.getInfo();
ShapeFlyweight blackRectangle = ShapeFlyweightFactory.factory("矩形"); blackRectangle.setExtrinsicState( "黑色" ); blackRectangle.getInfo();
System.out.println( "红色圆形 与 蓝色圆形 是否为同一个对象: " + (redCircle==blueCircle) ); System.out.println( "绿色矩形 与 黑色矩形 是否为同一个对象: " + (greenRectangle==blackRectangle) ); } }
|
从测试结果我们也可以看到,从工厂中多次获得的某类型的几何图形实际上是同一个对象。即具体享元角色Shape的实例是可以被共享的
不可共享的具体享元角色
如果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 {
private Set<Shape> shapeSet = new HashSet<>();
public void add(Shape shape) { shapeSet.add(shape); }
@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 { ...
public static Shapes factory(Set<String> typeSet) { Shapes shapes = new Shapes(); for(String type : typeSet) { 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
|
public class FlyweightPatternDemo { public static void main(String[] args) { System.out.println("--------------- Test 2: 不可共享的享元对象Shapes测试 ---------------");
Set<String> typeSet1 = new HashSet<>(); typeSet1.addAll( Arrays.asList("三角形", "正方形", "长方形") ); ShapeFlyweight shapes1 = ShapeFlyweightFactory.factory( typeSet1 ); shapes1.setExtrinsicState("黄色"); shapes1.getInfo();
Set<String> typeSet2 = new HashSet<>(); typeSet2.addAll( Arrays.asList("圆形", "椭圆形","三角形") ); ShapeFlyweight shapes2 = ShapeFlyweightFactory.factory( typeSet2 ); shapes2.setExtrinsicState("白色"); shapes2.getInfo(); } }
|
测试结果如下,符合预期
最后补充说明下,该设计模型在实际开发中,一般应用的并不多。其使得系统变得较为复杂。在区分对象属性是内部状态还是外部状态的过程中,有时候并不容易。该设计模式比较典型的实践有Java的String类
参考文献
- Head First 设计模式 弗里曼著