持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第30天,点击查看活动详情
概念
“享元”即为 共享元件 的意思。享元模式的英文 flyweight 是轻量级的意思,这就意味着享元模式 能使程序变得更加轻量化。当系统存在大量的对象,并且这些对象又具有相同的内部状态时,我们就可以用享元模式共享相同的元件对象,以避免对象泛滥造成资源浪费。
实例演示
注:本文内容参考 《秒懂设计模式》一书,本文对其做了概括凝练,主要是为了自身学习使用,如果无法理解,建议查看原书。
在我们的日常生活中,也充满了各种“享元”的应用,比如瓷砖、马赛克建筑等等。当然,在计算机世界中,例子更是数不胜数,以我们常见的 RPG 游戏为例,地图就是一个很好的应用享元的例子,在地图中,常见的场景有河流、山川、草地等等,为了避免问题的复杂化,我们就以草原地图作为范例,我们可以发现整张游戏地图都是由一个个小的单元图块组成的,那么我们把草地抽象为河流、草地、道路、房屋等 4 个元素块。接着我们来模拟地图建模的流程来介绍享元模式。
1. 建模
在开始代码实战之前,我们先思考怎样去建模。首先我们应该定义一个图块类来描述图块,具体属性应该包括“图片”和“位置”信息,并且具备按照这些信息去绘制图块的能力,代码如下所示:
public class Tile {
private String image;//图块所用的材质图
private int x, y;//图块所在坐标
public Tile(String image, int x, int y) {
this.image = image;
System.out.print("从磁盘加载[" + image + "]图片,耗时半秒……");
this.x = x;
this.y = y;
}
public void draw() {
System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
}
}
接下来假设我们用 string 来模拟建模过程:
public class Client {
public static void main(String[] args) {
//在地图第一行随便绘制一些图块
new Tile("河流", 10, 10).draw();
new Tile("河流", 10, 20).draw();
new Tile("道路", 10, 30).draw();
new Tile("草地", 10, 40).draw();
new Tile("草地", 10, 50).draw();
new Tile("草地", 10, 60).draw();
new Tile("草地", 10, 70).draw();
new Tile("草地", 10, 80).draw();
new Tile("道路", 10, 90).draw();
new Tile("道路", 10, 100).draw();
}
十张土块的话就需要耗时 5 秒,如果加载整张地图将会耗费多长时间?如此糟糕的游戏体验简直就是在挑战玩家的忍耐力。这是不可以接受的。
2. 原型模式处理
面对解决加载卡顿的问题,有些同学可能已经想到我们之前学过的原型模式了。对,我们完全可以把相同的图块对象共享,用克隆的方式来省去实例化的过程,从而加快初始化速度。然而,对这几个图块克隆貌似没什么问题,地图加载速度确实提高了,但是构建巨大的地图一定会在内存中产生庞大的图块对象群,从而导致大量的内存开销。如果没有内存回收机制,甚至会造成内存溢出,系统崩溃。
而且,用原型模式一定是不合适的,地图中的图块并非像游戏中动态的人物角色一样可以实时移动,它们的图片与坐标状态初始化后就固定下来了,简单讲就是被绘制出来后就不必变动了,即使要变也是将拼好的地图作为一个大对象整体挪动。图块一旦被绘制出来就不需要保留任何坐标状态,内存中自然也就不需要保留大量的图块对象了。
3. 图件共享(享元)
要提高游戏性能,我们只能利用少量的对象拼接整张地图。继续分析地图,我们会发现每个图块的坐标是不同的,但有很大一部分图块的材质图(图片)是相同的,也就是说,同样的材质图会在不同的坐标位置上重复出现。于是我们可以得出结论,材质图是可以作为享元的,而坐标则不能。
既然要共享相同的图片,那么我们就得将图块类按图片拆分成更细的材质类,如河流类、草地类、道路类等。而坐标不能作为图块类的享元属性,所以我们就得设法把这个属性抽离出去由外部负责。
首先我们先定义绘图接口,使坐标作为参数传递进来并进行绘图:
public interface Drawable {
void draw(int x, int y);//绘图方法,接收地图坐标
}
接着假设我们定义河流类,河流类实现了 Drawable 类,我们只需要实现河流类图片材质即可,至于坐标类,参数从外部传入就行,代码如下所示:
public class River implements Drawable {
private String image;//河流图片材质
public River() {
this.image = "河流";
System.out.print("从磁盘加载[" + image + "]图片,耗时半秒……");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
}
}
其他对象实现方式也是一样,这里不一一赘述。
4. 图片工厂
接下来就是实现“元之共享”的关键了,我们得定义一个图件工厂类,并将各种图件对象提前放入内存中共享,如此便可以避免每次从磁盘重新加载。
public class TileFactory {
private Map<String, Drawable> images;//图库
public TileFactory() {
images = new HashMap<String, Drawable>();
}
public Drawable getDrawable(String image) {
//缓存池里如果没有图件,则实例化并放入缓存池
if(!images.containsKey(image)){
switch (image) {
case "河流":
images.put(image, new River());
break;
case "草地":
images.put(image, new Grass());
break;
case "道路":
images.put(image, new Road());
break;
case "房屋":
images.put(image, new House());
}
}
//至此,缓存池里必然有图件,直接取得并返回
return images.get(image);
}
}
图件工厂类类似于一个图库管理器,其中维护着所有的图件元对象。首先在构造方法中初始化一个散列图的“缓存池”,然后通过懒加载模式来维护它。当客户端调用取图件方法 getDrawable() 时,程序首先会判断目标图件是否已经实例化并存在于缓存池中,如果没有则实例化并放入图库缓存池供下次使用,到这里目标图件必然存在于缓存池中了。最后直接从缓存池中获取目标图件并返回。如此,无论外部需要什么图件,也无论外部获取多少次图件,每类图件都只会在内存中被加载一次,这便是“元共享”的秘密所在。
最后,我们来看下客户端代码是如何实现的:
public class Client {
public static void main(String[] args) {
//先实例化图件工厂
TileFactory factory = new TileFactory();
//随便绘制一列为例
factory.getDrawable("河流").draw(10, 10);
factory.getDrawable("河流").draw(10, 20);
factory.getDrawable("道路").draw(10, 30);
factory.getDrawable("草地").draw(10, 40);
factory.getDrawable("草地").draw(10, 50);
factory.getDrawable("草地").draw(10, 60);
factory.getDrawable("草地").draw(10, 70);
factory.getDrawable("草地").draw(10, 80);
factory.getDrawable("道路").draw(10, 90);
factory.getDrawable("道路").draw(10, 100);
//绘制完地板后接着在顶层绘制房屋
factory.getDrawable("房子").draw(10, 10);
factory.getDrawable("房子").draw(10, 50);
}
}
我们抛弃了利用“new”关键字随意制造对象的方法,改用这个图件工厂类来构建并共享图件元,外部需要什么图件直接向图件工厂索要即可。此外,图件工厂类返回的图件实例也不再包含坐标信息这个属性了,而是将其作为绘图方法的参数即时传入。这样答答缩短了构造地图的时间
总结
上面我们已经了解到了享元模式是如何提升响应时间和内存等问题的了,享元模式让图件对象将可共享的内蕴状态“图片”维护起来,将外蕴状态“坐标”抽离出去并定义于接口参数中,基于此,享元工厂便可以顺利将图件对象共享,以供外部随时使用。
接下来我们再来看啊看享元模式下各个角色,享元模式的各角色定义如下:
Flyweight(享元接口):所有元件的高层规范,声明与外蕴状态互动的接口标准。对应本章例程中的绘图接口 Drawable。ConcreteFlyweight(享元实现):享元接口的元件实现类,自身维护着内蕴状态,且能接受并响应外蕴状态,可以有多个实现,一个享元对象可以被称作一个“元”。对应本章例程中的河流类 River、草地类 Grass、道路类 Road 等。FlyweightFactory(享元工厂):用来维护享元对象的工厂,负责对享元对象实例进行创建与管理,并对外提供获取享元对象的服务。Client(客户端):享元的使用者,负责维护外蕴状态。对应本章例程中的图件工厂类 TileFactory。
参考文档
- 《秒懂设计模式》—— 刘韬