zoukankan      html  css  js  c++  java
  • 设计模式是什么鬼(享元)

    //作者:凸凹里歐

    元,始也,本初,根源之意,计算机中的二进制“元”其实就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速度,内存轻量化同时做到了优化,整个游戏用户体验得到了极大的提升。

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

     

     

     

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

     
  • 相关阅读:
    高级(线性)素数筛
    Dijkstra(迪杰斯特拉)算法
    简单素数筛
    【解题报告】 POJ1958 奇怪的汉诺塔(Strange Tower of Hanoi)
    4 jQuery Chatting Plugins | jQuery UI Chatbox Plugin Examples Like Facebook, Gmail
    Web User Control Collection data is not storing
    How to turn on IE9 Compatibility View programmatically in Javascript
    从Javascrip 脚本中执行.exe 文件
    HtmlEditorExtender Ajax
    GRIDVIEW模板中查找控件的方式JAVASCRIPT
  • 原文地址:https://www.cnblogs.com/javazhiyin/p/10020433.html
Copyright © 2011-2022 走看看