目录
-
-
- 10、享元模式
-
- [10.1 享元模式](#10.1 享元模式)
- [10.2 举例](#10.2 举例)
-
- [10.2.1 马赛克](#10.2.1 马赛克)
- [10.2.2 游戏地图(以草原地图作为范例)](#10.2.2 游戏地图(以草原地图作为范例))
- [10.3 总结](#10.3 总结)
-
10、享元模式
10.1 享元模式
- "享元"则是共享元件的意思
- 享元模式的英文flyweight是轻量级的意思,这就意味着享元模式能使程序变得更加轻量化
- 当系统存在大量的对象,并且这些对象又具有相同的内部状态时,我们就可以用享元模式共享相同的元件对象,以避免对象泛滥造成资源浪费。
- 测试类结构
10.2 举例
10.2.1 马赛克
- 虽然马赛克小块数量比较多,但经过观察我们会发现,
- 分析组成,进行归类 后,找到元件只有4种:黑色块、灰色块、灰白色块以及白色块。
- 我们可以说,这就是4个"元"色块
10.2.2 游戏地图(以草原地图作为范例)
-
对组成部分进行归类分析,找到原件
- 游戏地图都是由一个个小的单元图块组成的
- 其中除房屋比较大之外,其他图块的尺寸都一样,它们分别为河流、草地、道路,这些图块便是4个元图块
-
分析建模
- 定义一个图块类来描述图块,具体属性应该包括"图片"和"位置"信息,并且具备按照这些信息去绘制图块的能力:Segment
javapackage flyweight; /** * @Description 图块类 */ public class Segment { /** * 材质图 */ private String image; /** * 位置坐标 */ private int x,y; /** * 显式带参构造方法:初始化各参数 * @param image 材质图 * @param x 横坐标 * @param y 纵坐标 */ public Segment(String image, int x, int y) { this.image = image; System.out.println("从磁盘加载[" + image + "]图片......"); this.x = x; this.y = y; } /** * 图块绘制方法:按坐标位置绘制在地图上 */ public void draw() { System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]"); } }
- 在地图第一行随便绘制一些图块,Client.test1()
- 在这一步会发现,图片加载很慢,一张图片加载要半秒,10张图块就要耗费5秒,影响用户体验
javapackage flyweight; /** *@Description 测试类 */ public class Client { private static void test1() { //在地图第一行随便绘制一些图块 new Segment("河流", 10, 10).draw(); new Segment("河流", 10, 20).draw(); new Segment("道路", 10, 30).draw(); new Segment("草地", 10, 40).draw(); new Segment("草地", 10, 50).draw(); new Segment("草地", 10, 60).draw(); new Segment("草地", 10, 70).draw(); new Segment("草地", 10, 80).draw(); new Segment("道路", 10, 90).draw(); new Segment("道路", 10, 100).draw(); } }
- 图片与坐标状态初始化后就固定下来了,简单讲就是被绘制出来后就不必变动了,即使要变也是将拼好的地图作为一个大对象整体挪动
-
图件共享(优化)
- 继续分析每个图块的坐标是不同 的,但有很大一部分图块的材质图(图片)是相同的
- 于是我们可以得出结论,材质图是可以作为享元的,而坐标则不能
- 既然要共享相同的图片,那么我们就得将图块类按图片拆分成更细的材质类,如河流类、草地类、道路类等
- 而坐标不能作为图块类的享元属性,所以我们就得设法把这个属性抽离出去由外部负责
- 代码实战
- 首先需要定义一个接口,规范这些材质类的绘图标准(接口:规范标准Drawable)
- 当然,除了接口方式,我们还可以用抽象类抽离出更多的属性和方法,使子类变得更加简单
- 首先需要定义一个接口,规范这些材质类的绘图标准(接口:规范标准Drawable)
javapackage flyweight; /** * @Description 绘图接口: 规范这些材质类的绘图标准 * 当然,除了接口方式,我们还可以用抽象类抽离出更多的属性和方法,使子类变得更加简单 */ public interface DrawAble { /** * 绘图方法,接收地图坐标 * @param x 横坐标 * @param y 纵坐标 */ void draw(int x, int y); }
-
定义一系列材质类并实现此绘图接口
- 河流类River
javapackage flyweight.texture; import flyweight.DrawAble; /** * @Description 河流类 */ public class River implements DrawAble { /** * 享元属性: 河流图片材质作为内部属性 */ private String image; /** * 类构造器中加载河流图片 * 这就是类内部即将共享的"元"数据了,我们通常称之为"内蕴状态" */ public River() { this.image = "河流"; System.out.print("从磁盘加载[" + image + "]图片,耗时......"); } /** * 重写绘图方法 * 而作为"外蕴状态"的坐标是无法作为享元的,由外部传入 * @param x 横坐标 * @param y 纵坐标 */ @Override public void draw(int x, int y) { System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]"); } }
- 草地类Grass
javapackage flyweight.texture; import flyweight.DrawAble; /** * @Description 草地类 */ public class Grass implements DrawAble { /** * 享元属性:草地图片材质 */ private String image; public Grass() { this.image = "草地"; System.out.print("从磁盘加载[" + image + "]图片,耗时......"); } @Override public void draw(int x, int y) { System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]"); } }
- 道路类Road
javapackage flyweight.texture; import flyweight.DrawAble; /** * @Description 道路类 */ public class Road implements DrawAble { /** * 道路图片材质 */ private String image; public Road() { this.image = "道路"; System.out.println("从磁盘加载[" + image + "]图片,耗时..."); } @Override public void draw(int x, int y) { System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]"); } }
- 房屋类House
javapackage flyweight.texture; import flyweight.DrawAble; /** * @Description 房屋类 */ public class House implements DrawAble { private String image;//房屋图片材质 public House() { this.image = "房屋"; System.out.print("从磁盘加载[" + image + "]图片,耗时......"); } @Override public void draw(int x, int y) { System.out.print("将图层切换到顶层......");//房屋盖在地板上,所以切换到顶层图层 System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]"); } }
-
"元之共享"的关键:
- 定义一个图件工厂类
javapackage flyweight.factory; import flyweight.DrawAble; import flyweight.texture.Grass; import flyweight.texture.House; import flyweight.texture.River; import flyweight.texture.Road; import java.util.HashMap; import java.util.Map; /** * @Description 图件工厂类 */ public class SegmentFactory { /** * 图库: 维护着所有的图件元对象 */ private Map<String, DrawAble> images; public SegmentFactory() { 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); } }
- 并将各种图件对象提前放入内存中共享,如此便可以避免每次从磁盘重新加载
-
测试:Client.test2()
javaprivate static void test2() { //先实例化图件工厂 SegmentFactory factory = new SegmentFactory(); /* * 随便绘制一列为例: * 抛弃了利用"new"关键字随意制造对象的方法, * 改用这个图件工厂类来构建并共享图件元,外部需要什么图件直接向图件工厂索要即可 */ 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); }
-
小结
- 相同部分可以作为享元,如在构造器中加载的,作为内部类即将共享的元数据,通常称为"内蕴状态"
- 不同部分不能作为享元,如在实现房中作为参数传入的属性,称为"外蕴状态"
10.3 总结
- 享元模式让图件对象将可共享的内蕴状态 "图片"维护起来,将外蕴状态 "坐标"抽离出去并定义于接口参数中
- 基于此,享元工厂便可以顺利将图件对象共享,以供外部随时使用。
- 享元模式的各角色定义如下
- Flyweight(享元接口):所有元件的高层规范,声明与外蕴状态互动的接口标准。如:DrawAble。
- ConcreteFlyweight(享元实现):
- 享元接口的元件实现类,自身维护着内蕴状态,且能接受并响应外蕴状态,
- 可以有多个实现。如:河流类River、草地类Grass、道路类Road等。
- 一个享元对象可以被称作一个"元"
- FlyweightFactory(享元工厂):用来维护享元对象的工厂,负责对享元对象实例进行创建与管理,并对外提供获取享元对象的服务。SegmentFactory
- Client(客户端):享元的使用者,负责维护外蕴状态。Client
- "享元"的理念其实就是萃取事物的本质
- 将对象的内蕴状态与外蕴状态剥离开来,其中内蕴状态成为真正的"元"数据,而外蕴状态则被抽离出去由外部负责维护