结构篇-享元模式


计算机世界中无穷无尽的可能,其本质都是由1和0两个“元”的组合变化而产生的。元,顾名思义,始也,有本初、根源的意思。“享元”则是共享元件的意思。享元模式的英文flyweight是轻量级的意思,这就意味着享元模式能使程序变得更加轻量化。当系统存在大量的对象,并且这些对象又具有相同的内部状态时,我们就可以用享元模式共享相同的元件对象,以避免对象泛滥造成资源浪费。


提示:以下是本篇文章正文内容,下面案例可供参考

一、马赛克

除了计算机世界,我们的真实世界也充满了各种“享元”的应用。很多人一定有过装修房子的经历,装修离不开瓷砖、木地板、马赛克等建筑材料。针对不同的房间会选择不同材质、花色的单块地砖或墙砖拼接成一个完整的面,尤其是马赛克这种建筑材料拼成的图案会更加复杂,近看好像显示器像素一样密密麻麻地排列在一起,虽然马赛克小块数量比较多,但经过观察我们会发现,归类后只有4种:黑色块、灰色块、灰白色块以及白色块。我们可以说,这就是4个“元”色块。

二、游戏地图

在早期的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 + "]");
    }
}

2.客户端

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();
    }
}
输出结果:
在磁盘加载[河流]图片,耗时半秒......在位置[10:10]上绘制图片:[河流]
在磁盘加载[道路]图片,耗时半秒......在位置[10:20]上绘制图片:[道路]
在磁盘加载[草地]图片,耗时半秒......在位置[10:30]上绘制图片:[草地]
在磁盘加载[河流]图片,耗时半秒......在位置[10:40]上绘制图片:[河流]
在磁盘加载[河流]图片,耗时半秒......在位置[10:50]上绘制图片:[河流]
在磁盘加载[草地]图片,耗时半秒......在位置[10:60]上绘制图片:[草地]
在磁盘加载[草地]图片,耗时半秒......在位置[10:70]上绘制图片:[草地]
在磁盘加载[道路]图片,耗时半秒......在位置[10:80]上绘制图片:[道路]
在磁盘加载[道路]图片,耗时半秒......在位置[10:90]上绘制图片:[道路]
在磁盘加载[河流]图片,耗时半秒......在位置[10:100]上绘制图片:[河流]

说明:

  1. 客户端将所有图块进行初始化并绘制出来,顺利完成地图拼接。然而,通过观察运行结果我们会发现一个问题每次加载一张图片都要耗费半秒时间,10张图块就要耗费5秒,如果加载整张地图将会耗费多长时间?如此糟糕的游戏体验简直就是在挑战玩家的忍耐力,缓慢的地图加载过程会让玩家失去兴趣。
  2. 面对解决加载卡顿的问题,有些读者可能已经想到我们之前学过的原型模式了。对,我们完全可以把相同的图块对象共享,用克隆的方式来省去实例化的过程,从而加快初始化速度。然而,对这几个图块克隆貌似没什么问题,地图加载速度确实提高了,但是构建巨大的地图一定会在内存中产生庞大的图块对象群,从而导致大量的内存开销。如果没有内存回收机制,甚至会造成内存溢出,系统崩溃。
  3. 用原型模式一定是不合适的,地图中的图块并非像游戏中动态的人物角色一样可以实时移动,它们的图片与坐标状态初始化后就固定下来了,简单讲就是被绘制出来后就不必变动了,即使要变也是将拼好的地图作为一个大对象整体挪动。图块一旦被绘制出来就不需要保留任何坐标状态,内存中自然也就不需要保留大量的图块对象了。

三、图件共享

继续分析地图,我们会发现每个图块的坐标是不同的,但有很大一部分图块的材质图(图片)是相同的,也就是说,同样的材质图会在不同的坐标位置上重复出现。于是我们可以得出结论,材质图是可以作为享元的,而坐标则不能。

既然要共享相同的图片,那么我们就得将图块类按图片拆分成更细的材质类,如河流类、草地类、道路类等。而坐标不能作为图块类的享元属性,所以我们就得设法把这个属性抽离出去由外部负责。

1. 绘图接口

public interface Drawable {
    void draw(int x, int y);// 绘图方法,接收地图坐标
}

说明:

  1. 我们定义了绘图接口,使坐标作为参数传递进来并进行绘图。当然,除了接口方式,我们还可以用抽象类抽离出更多的属性和方法,使子类变得更加简单。

2.图件类

我们再定义一系列材质类并实现此绘图接口。

//河流类
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 + "]");
    }
}

//草地类
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 + "]");
    }
}

//道路类
public class Road implements Drawable{

    private String image;// 道路图片材质

    public Road() {
        this.image = "道路";
        System.out.print("在磁盘加载[" + image + "]图片,耗时半秒......" );
    }

    @Override
    public void draw(int x, int y) {
        System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
    }
}

//房屋类
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.println("将图层切换到顶层......"); //房屋盖在地板上,所以切换到顶层
        System.out.println("在位置[" + x + ":" + y + "]上绘制图片:[" + image + "]");
    }
}

说明:

  1. 以河流类举例子,河流类中只定义了图片作为内部属性。在类构造器中加载河流图片,这就是类内部即将共享的“元”数据了,我们通常称之为“内蕴状态”。而作为“外蕴状态”的坐标是无法作为享元的,所以将其作为参数由第实现的绘图方法中由外部传入。
  2. 房屋类与其他类有所区别,它拥有自己特定的绘图方法,调用后会在地板图层之上绘制房屋,覆盖下面的地板(房屋图片比其他图片要大一些),以使地图变得更加立体化。

3.图件工厂类

public class TileFactory {
    private Map<String, Drawable> image;// 图库

    public TileFactory() {
        this.image = new HashMap<>();
    }

    public Drawable getDrawable(String name) {
        //缓存池里如果没有图件,则实例化并放入缓存池
        if (!image.containsKey(name)) {
            switch (name) {
                case "河流":
                    image.put(name, new River());
                    break;
                case "草地":
                    image.put(name, new Grass());
                    break;
                case "道路":
                    image.put(name, new Road());
                    break;
                case "房屋":
                    image.put(name, new House());
            }
        }
        //至此,缓存池里必然有图件,直接取得返回
        return image.get(name);
    }
}

说明:

  1. 图件工厂类类似于一个图库管理器,其中维护着所有的图件元对象。首先在构造方法中初始化一个散列图的“缓存池”,然后通过懒加载模式来维护它。
  2. 当客户端调用获取图件方法getDrawable()时,程序首先会判断目标图件是否已经实例化并存在于缓存池中,如果没有则实例化并放入图库缓存池供下次使用,到这里目标图件必然存在于缓存池中了。最后直接从缓存池中获取目标图件并返回。
  3. 如此,无论外部需要什么图件,也无论外部获取多少次图件,每类图件都只会在内存中被加载一次,这便是“元共享”的秘密所在。

4.客户端类

public class Client {
    public static void main(String[] args) {
        TileFactory tileFactory = new TileFactory();

        tileFactory.getDrawable("河流").draw(10 , 10);
        tileFactory.getDrawable("河流").draw(10 , 20);
        tileFactory.getDrawable("草地").draw(10 , 30);
        tileFactory.getDrawable("河流").draw(10 , 40);
        tileFactory.getDrawable("河流").draw(10 , 50);
        tileFactory.getDrawable("草地").draw(10 , 60);
        tileFactory.getDrawable("草地").draw(10 , 70);
        tileFactory.getDrawable("道路").draw(10 , 80);
        tileFactory.getDrawable("道路").draw(10 , 90);
        tileFactory.getDrawable("房屋").draw(10 , 100);
    }
}
输出结果:
在磁盘加载[河流]图片,耗时半秒......在位置[10:10]上绘制图片:[河流]
在位置[10:20]上绘制图片:[河流]
在磁盘加载[草地]图片,耗时半秒......在位置[10:30]上绘制图片:[草地]
在位置[10:40]上绘制图片:[河流]
在位置[10:50]上绘制图片:[河流]
在位置[10:60]上绘制图片:[草地]
在位置[10:70]上绘制图片:[草地]
在磁盘加载[道路]图片,耗时半秒......在位置[10:80]上绘制图片:[道路]
在位置[10:90]上绘制图片:[道路]
在磁盘加载[房屋]图片,耗时半秒......将图层切换到顶层......
在位置[10:100]上绘制图片:[房屋]

说明:

  1. 我们抛弃了利用“new”关键字随意制造对象的方法,改用这个图件工厂类来构建并共享图件元,外部需要什么图件直接向图件工厂索要即可。
  2. 此外,图件工厂类返回的图件实例也不再包含坐标信息这个属性了,而是将其作为绘图方法的参数即时传入。结果立竿见影,从输出中可以看到,每个图件对象在初次实例化时会耗费半秒时间,而下次请求时就不会再出现加载图片的耗时操作了,也就是从图库缓存池直接拿到了。

总结

提示:这里对文章进行总结:

  1. 至此,享元模式的运用让程序运行更加流畅,地图加载再也不会出现卡顿现象了,加载图片时的I/O流操作所导致的CPU效率及内存占用的问题同时得以解决,游戏体验得以提升和改善。享元模式让图件对象将可共享的内蕴状态“图片”维护起来,将外蕴状态“坐标”抽离出去并定义于接口参数中,基于此,享元工厂便可以顺利将图件对象共享,以供外部随时使用。
  2. 享元模式的类结构:
  • Flyweight(享元接口):所有元件的高层规范,声明与外蕴状态互动的接口标准。对应本章例程中的绘图接口Drawable。
  • ConcreteFlyweight(享元实现):享元接口的元件实现类,自身维护着内蕴状态,且能接受并响应外蕴状态,可以有多个实现,一个享元对象可以被称作一个“元”。对应本章例程中的河流类River、草地类Grass、道路类Road等。
  • FlyweightFactory(享元工厂):用来维护享元对象的工厂,负责对享元对象实例进行创建与管理,并对外提供获取享元对象的服务。
  • Client(客户端):享元的使用者,负责维护外蕴状态。对应本章例程中的图件工厂类TileFactory。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhixuChen200

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值