当前位置: 首页 > news >正文

设计模式是什么鬼(享元)

//作者:凸凹里歐

 

元,始也,本初,根源之意,计算机中的二进制“元”其实就1和0,这两个东西组合起来有无穷无尽的可能,这便形成了计算机中的大千世界,正如“阴”和“阳”为万物之首一样,这也是为什么称其为二元。顾名思义,享元就是共享本元的意思,然而这个模式的英文叫做Flyweight,能飞起来一般的重量,轻量级的意思,“享元”其实并非意译,但这并不影响其对这个模式的最佳诠释。

我们来看一个实例,比如我们要开发一款RPG游戏,游戏地图通常非常大,而且有各种各样,有草地、沙漠、荒原,水路等等,在写代码之前,我们先思考下应该怎样去建模。

 

对于这种地图,我们加载一整张图片来做地图?如果地图太大,图片加载相当卡顿吧?而且大片地图上其实都是重复的图片素材,整图加载设计也有失灵活性。再仔细观察下,这地图无非就是很多小图片(元)拼起来的哦,这不就是类似于我们装修时贴马赛克嘛?

 

这可简单了!我们应该有个砖块类,持有“图片”,“位置”等属性信息,然后实例化这些砖块再调用其“绘制”方法把图片显示在地图某位置上即可。二话不说开始写代码。

 1 public class Tile {
 2  private String image;//地砖所用的图片材质
 3  private int x, y;//地砖所在坐标
 4  public Tile(String image, int x, int y) {
 5  this.image = image;
 6  System.out.print("从磁盘加载[" + image + "]图片,耗时半秒。。。");
 7  this.x = x;
 8  this.y = y;
 9  }
10  public void draw() {
11  System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
12  }
13 }

 

代码看起来非常简单,第3行的地砖材质图片我们用String来模拟代替,第7行初始化时我们把图片加载到内存,比如说这个IO操作要耗费半秒时间,好了我们先测试绘制第一行砖块,运行一下。

 1 public class Client {
 2  public static void main(String[] args) {
 3  //以绘制第一行为例
 4  new Tile("河流", 10, 10).draw();
 5  new Tile("河流", 10, 20).draw();
 6  new Tile("石路", 10, 30).draw();
 7  new Tile("草坪", 10, 40).draw();
 8  new Tile("草坪", 10, 50).draw();
 9  new Tile("草坪", 10, 60).draw();
10  new Tile("草坪", 10, 70).draw();
11  new Tile("草坪", 10, 80).draw();
12  /* 运行结果
13  从磁盘加载[河流]图片,耗时半秒。。。在位置[10:10]上绘制图片:[河流]
14  从磁盘加载[河流]图片,耗时半秒。。。在位置[10:20]上绘制图片:[河流]
15  从磁盘加载[石路]图片,耗时半秒。。。在位置[10:30]上绘制图片:[石路]
16  从磁盘加载[草坪]图片,耗时半秒。。。在位置[10:40]上绘制图片:[草坪]
17  从磁盘加载[草坪]图片,耗时半秒。。。在位置[10:50]上绘制图片:[草坪]
18  从磁盘加载[草坪]图片,耗时半秒。。。在位置[10:60]上绘制图片:[草坪]
19  从磁盘加载[草坪]图片,耗时半秒。。。在位置[10:70]上绘制图片:[草坪]
20  从磁盘加载[草坪]图片,耗时半秒。。。在位置[10:80]上绘制图片:[草坪]
21  */
22  }
23 }

 

有没有发现问题?每加载一张图都要耗费掉半秒钟,才画了8张地砖图就4秒钟流逝了,如果构建整张地图得多少时间?这就像是在慢性自杀,如此效率严重影响了游戏的用户体验,光卡顿在地图加载这给漫长的过程就已经让玩家失去兴趣了。

相信大家一定想到了《设计模式是什么鬼(原型)》模式吧?对,我们把相同的图共享出来,用克隆的方式代替物件图实例化的过程,从而加快初始化速度。再想想,共享元貌似没什么问题,速度也加快了,但对象数量貌似还是个严重问题,每一个小物件图都要对应一个对象,这么个小游戏用得着那么大的内存开销么,搞不好甚至会造成内存溢出,嗯,设计模式一定还是有问题。

 

 

沿着共享的思路我们再看下到底需不需要这么多对象?这些对象不同的地方在于其坐标的不同,再就是材质的不同,也就是图的不同了,能不能从这些对象里抽取出来一些共同点呢?首先每个图的坐标都不一样,是没办法共享的,但是材质图是重复出现的,是可以共享的,同样的材质图会在不同的坐标位置上重复出现,那么这个材质图是可以做成共享元的。

既然坐标不能共享,那就不做为材质类的共享元属性,由客户端维护这些坐标并作为参数传入好了,而且这些材质都有绘制能力,那就先定义一个接口吧。

1 public interface Drawable {
2 
3 void draw(int x, int y);//绘制方法,接收地图坐标。
4 
5 }

 

当然,我们也可以用抽象类抽出更多的属性和方法代替接口,使子类变得简单,这里为了清晰说明问题就用接口。接下来是材质类们,统统实现这个绘制接口。

 1 public class Water implements Drawable {
 2 
 3 private String image;//河流图片材质
 4 
 5 public Water() {
 6 
 7 this.image = "河流";
 8 
 9 System.out.print("从磁盘加载[" + image + "]图片,耗时半秒。。。");
10 
11 }
12 
13 @Override
14 
15 public void draw(int x, int y) {
16 
17 System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
18 
19 }
20 
21 }

 

注意第6行因为是河流材质类,所以初始化我们直接加载河流图片素材,这就是类内部即将做共享的“元”数据了,也叫做“内蕴状态”,至于“外蕴状态”就是坐标了,只作为参数从外部传入不做共享。接下来是草地、石子路等等。

 1 public class Grass implements Drawable {
 2  private String image;//草坪图片材质
 3  public Grass() {
 4  this.image = "草坪";
 5  System.out.print("从磁盘加载[" + image + "]图片,耗时半秒。。。");
 6  }
 7  @Override
 8  public void draw(int x, int y) {
 9  System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
10  }
11 }

 

 1 public class Stone implements Drawable {
 2  private String image;//石路图片材质
 3  public Stone() {
 4  this.image = "石路";
 5  System.out.print("从磁盘加载[" + image + "]图片,耗时半秒。。。");
 6  }
 7  @Override
 8  public void draw(int x, int y) {
 9  System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
10  }
11 }

 

 1 public class House implements Drawable {
 2  private String image;//房子图片材质
 3  public House() {
 4  this.image = "房子";
 5  System.out.print("从磁盘加载[" + image + "]图片,耗时一秒。。。");
 6  }
 7  @Override
 8  public void draw(int x, int y) {
 9  System.out.println("将图层切到最上层。。。");//房子盖在地上,所以切换到顶层图层。
10  System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
11  }
12 }

注意上面这个的房子类有所不同,它有自己特有的绘制行为方法,也就是在地板图层之上绘制房子,覆盖掉下面的地板,使其变得更加立体。这也就是为什么我们非要用接口或抽象类来做引用,使实现类可以有自己独特的行为方式,多态的好处立竿见影。接下来就是实现“元之共享”的关键了,我们来做一个简单工厂类,看代码。

 1  public class Factory {//图件工厂
 2  private Map<String, Drawable> images;//图库
 3  public Factory() {
 4  images = new HashMap<String, Drawable>();
 5  }
 6  public Drawable getDrawable(String image) {
 7  //缓存里如果没有图件,则实例化并放入缓存。
 8  if(!images.containsKey(image)){
 9  switch (image) {
10  case "河流":
11  images.put(image, new Water());
12  break;
13  case "草坪":
14  images.put(image, new Grass());
15  break;
16  case "石路":
17  images.put(image, new Stone());
18  }
19  }
20  //缓存里必然有图,直接取得并返回。
21  return images.get(image);
22  }
23 }

 

这个图件工厂维护着所有元对象的图库,构造方法于第5行会初始化一个哈希图的缓存”池“,当客户端于第8行需要实例化图件的时候,我们先观察这个图库池里存在不存在已实例化过的图件,也就是看有无已做共享的图元,如果没有则实例化并加入图库共享池供下次使用,这便是”元之共享“的秘密了。巧夺天工的设计一气呵成,已经迫不及待去运行了。

 1 public class Client {
 2  public static void main(String[] args) {
 3  //先实例化图件工厂
 4  Factory factory = new Factory();
 5  //以第一行为例
 6  factory.getDrawable("河流").draw(10, 10);
 7  factory.getDrawable("河流").draw(10, 20);
 8  factory.getDrawable("石路").draw(10, 30);
 9  factory.getDrawable("草坪").draw(10, 40);
10  factory.getDrawable("草坪").draw(10, 50);
11  factory.getDrawable("草坪").draw(10, 60);
12  factory.getDrawable("草坪").draw(10, 70);
13  factory.getDrawable("草坪").draw(10, 80);
14  /*运行结果
15  从磁盘加载[河流]图片,耗时半秒。。。在位置[10:10]上绘制图片:[河流]
16  在位置[10:20]上绘制图片:[河流]
17  从磁盘加载[石路]图片,耗时半秒。。。在位置[10:30]上绘制图片:[石路]
18  从磁盘加载[草坪]图片,耗时半秒。。。在位置[10:40]上绘制图片:[草坪]
19  在位置[10:50]上绘制图片:[草坪]
20  在位置[10:60]上绘制图片:[草坪]
21  在位置[10:70]上绘制图片:[草坪]
22  在位置[10:80]上绘制图片:[草坪]
23  */
24  }
25 }

 

可以看到,我们抛弃了利用new关键字肆意妄为地制造对象,而是改用这个图件工厂去帮我们把元构建并共享起来。显而易见,我们看到运行结果中每次实例化对象会耗费半秒时间,再次请求对象时就不再会加载图片耗费时间了,也就是从共享图池直接拿到了,不再造次。更妙的是,如果画完整个地图只需要实例化需要用到的某些元素材而已,即使是那个大房子图件也只需要实例化一次就够了。至此,CPU速度,内存轻量化同时做到了优化,整个游戏用户体验得到了极大的提升。

享元的精髓当然重点不止于”享“,更重要的是对于元的辨识,例如那个从外部客户端传入的坐标参数,如果我们依然把坐标也当作共享对象元数据(内蕴状态)的话,那么这个结构将无元可享,大量的对象就如同世界上没有相同的两片树叶一样多不胜数,最终会导致图库池被撑爆,享元将变得毫无意义。所以,对于整个系统数据结构的分析、设计、规划显得尤为重要。

 

 

 

内外相济,里应外合,以不变应万变的化繁为简,元,万变不离其宗,享之。

 

转载于:https://www.cnblogs.com/javazhiyin/p/10020433.html

相关文章:

  • rsync+sersync实现数据实时同步
  • 「Main」
  • 8.XML相关对象
  • 野生前端的数据结构基础练习(8)——图
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • HyperLeger Fabric SDK开发(二)——Fabric SDK配置
  • Python函数高级
  • JVM 参数调优
  • 参数为空取全部数据的几种做法
  • Chisel3 - 基本数据类型
  • 实验五 编写调试具有多个段的程序
  • JSAAS 平台实现 微信类似的TOKEN机制
  • kafka集群Controller竞选与责任设计思路架构详解-kafka 商业环境实战
  • Linux C编程之一:Linux下c语言的开发环境
  • 写给高年级小学生看的《Bash 指南》
  • [LeetCode] Wiggle Sort
  • golang 发送GET和POST示例
  • HashMap剖析之内部结构
  • MySQL-事务管理(基础)
  • node 版本过低
  • python 装饰器(一)
  • SpiderData 2019年2月23日 DApp数据排行榜
  • Stream流与Lambda表达式(三) 静态工厂类Collectors
  • Web标准制定过程
  • - 概述 - 《设计模式(极简c++版)》
  • 工作中总结前端开发流程--vue项目
  • 开放才能进步!Angular和Wijmo一起走过的日子
  • 前端存储 - localStorage
  • 前端攻城师
  • 入门到放弃node系列之Hello Word篇
  • 深入浅出webpack学习(1)--核心概念
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 学习笔记TF060:图像语音结合,看图说话
  • 智能合约Solidity教程-事件和日志(一)
  • ionic异常记录
  • ​决定德拉瓦州地区版图的关键历史事件
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • #gStore-weekly | gStore最新版本1.0之三角形计数函数的使用
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • (2)STL算法之元素计数
  • (ZT)薛涌:谈贫说富
  • (二)Pytorch快速搭建神经网络模型实现气温预测回归(代码+详细注解)
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (理论篇)httpmoudle和httphandler一览
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (全注解开发)学习Spring-MVC的第三天
  • (一)RocketMQ初步认识
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • *(长期更新)软考网络工程师学习笔记——Section 22 无线局域网
  • *1 计算机基础和操作系统基础及几大协议
  • .NET Core 实现 Redis 批量查询指定格式的Key
  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表