Android中的设计模式-享元模式

我们在编程实践中,经常会遇到这样的场景:许多类或方法中,用到了一些代码模块,这些代码的逻辑结构完全一样。那么我们一般会对这些代码进行重构,把这些代码抽取出来,组成一个新的方法,并把它放到基类或者工具类中,作为通用方法,这样各个不同的类或方法能同时使用它们来完成功能,显然,这种方式提高了代码的复用性,减少了代码的数量。这个场景就是典型的享元模式场景。

该模式的定义非常简单:

运用共享技术有效的支持大量细粒度的对象。

Android中的设计模式-享元模式_第1张图片
“享”是共享的意思,“元”是元素,也就是细粒度的对象。该模式的英语名称是“flyweight”,意思是“蝇量”就是描述很微小的意思,也就是元素。

我们知道构成物质的化学元素是最小的物质单位,正是这100多个化学元素组成了这大千世界中的一切物质,尽管每个物质的特性都不一样,但组成它们的元素无一例外都是这100多个元素中的一个或几个,显然所有物质都共享这些元素。该模式称为“享元模式”一语中的,非常传神,精准地描述了该模式的作用和目的:共享细粒度的对象,细粒度的目的是为了共享,关键词:细粒度。

为什么要共享,因为有大量的对象,内存吃紧啊;为什么要细粒度呢,对象的粒度越小,相同的可能性越大。如果把字符串拆成单个字符,显然,全部的英文单词,共享52个字母字符就行了!共享好理解,可以通过对象池或者缓存来实现,那么怎么考虑细粒度呢?

一般在对该模式的解读中,都会提到对象的内部状态和外部状态,通常外部状态是一个对象独有的,而内部状态是这类对象固有的,不会变化的,而且各个对象的内部状态都相同,既然相同就可以共享!因此可以把它缓存起来共享使用,在使用时从缓存中得到一个共享对象,把外部状态传入进去和内部对象结合起来,就可以组合成一个新的对象使用了,尽管创建的新对象的数量很多,由于新对象共享了一部分数据(外部状态),内存就会节省很多。

因此可以认为这里所谓的细粒度指的是内部状态,实现该模式的关键是找到对象的内部状态,把它单独抽取出来,细化成享元对象,来进行共享使用。因此在设计对象类结构时,如果一个类在应用时有可能会产生大量的对象,想办法提取它们的内部状态组成享元对象,从而节省内存开销。对象的“内部状态和“外部状态”不太容易记忆,干脆把它们认为是组成对象的能被共享的状态和私有的状态就行了。

举个生活中的例子,图书馆馆藏的书籍一般不是孤本,同一种图书通常会有一批,我们看一下书的属性有哪些?首先是书的固有属性:书名、作者、出版社、价格、书号、ISBN编号等;另一种就是图书馆为了管理图书而设置的属性:馆藏编号、是否借出、借出人员姓名、图书位置所在等。显然图书的私有状态就是它的固有属性,同一种图书的每一本书完全一样,可以让它们共享;而那些管理属性就是它的共享状态,每一本书都不一样。因此,如果编程实现一个图书馆管理系统,图书的固有属性在同一种图书之间可以共享,只存储一份就行了,每本书只存储它的管理属性,并引用它的固有属性,从而达到了节省存储空间的目的。

从结构图中可以看到ConcreteFlyweight是享元对象,包含有共享状态,ConcreteCompositeFlyweight是组合享元对象,它包含了一个享元对象(注意:该享元对象是和其它组合对象共享的),并且拥有自己的私有状态。

举个例子说明:
假设我们在Android中要在ListView中列表展现一个文件夹下的所有文件,用户可以选择一个文件项查看它们的属性。因此需要在ListView的Adapter中保存一个目录下的所有文件。我们知道如果获取一个文件的属性,需要知道文件在磁盘上存储位置:全路径,一般这个路径会很长,如果所有文件的路径全部保存的话,会占用大量的内存。

怎么存储它们才能节省空间呢?如果我们仔细分析文件的路径构成,就会发现一个目录下所有文件的父目录完全相同。因此,如果把文件路径分成两部分:父目录和文件名,其中父目录看作是文件对象的内部状态,而文件名是文件对象的外部状态,是不是父目录就能共享了?这样就能把父目录单独作为一个享元对象,由于其它所有的子文件共有一个父目录,它们只保存自己的文件名就行了,从而达到了节省内存的目的。在这里保存父目录的对象是享元对象,而保存文件名和引用父目录对象的文件对象就是组合享元对象。

各个相关类定义如下:

abstract class Item {
    public abstract String getPath();
    .... //其它属性和方法
}

class DirItem extends Item {
    private final String path; //保存目录路径

    public DirItem(String path) {
        this.path = path;
    }

    public String getPath() {
        return path;
    }
    ... //其它属性和方法
}

class FileItem extends Item {
    private final Item parent; //父目录路径,看作是内部状态,需要共享的
    private final String name; //文件名,看作是外部状态,自己私有

    public FileItem(Item parent, String name) {
        this.parent = parent;
        this.name = name;
    }
    .... //其它属性和方法
    public final String getPath() {
        return parent.getPath() + File.separatorChar + name;
    }
}

这样,数据成员parent只引用了一个共享的父目录路径,name只保存文件的文件名,显然比起全路径来,FileItem内存占用小多了。当需要文件的全路径时,那就调用方法getPath(),把parent和name组合起来返回。由于用户一般不会查看全部文件的属性,因此应用调用getPath()的次数不会很多,产生的全路径String对象比起FileItem对象的数量会少很多。

作为节省内存资源的大杀器,享元模式在Android应用开发中也少不了它的身影。例如,Android开发工程中,有一个res资源目录,里面除了layout之外,还有其他的文件夹,比如color、drawable、anim等资源目录。Android框架在运行的时候,会创建出相应的对象实例,比如Color、Drawable等类型的对象实例,并以它们的id作为索引号,保存在R.id包下。在layout中对View进行布局的时候,各个不同的View是可以同时引用它们的,显然各个View可以共享同一个Color、Drawable等资源对象。

总之,当需要节省内存时,或者生成的对象数量很多时,可以考虑这个模式,当然,如果对象中没有能够共享的享元对象,也就无法使用本模式。享元其实就是把原先的一个对象的数据属性进行了分割,对象只保存自己专属的数据,也就是将对象本身的数据和对象所使用的数据进行了分离,以达到节省内存的目的。在一般的对象设计中,对象自身保存全部的状态,从而实现自身信息的“完备”,而在享元模式中,对象的外部和内部状态是分开的,享元对象的使用者保存外部状态,享元对象自己只保存内部状态,它只引用外部状态。

需要指出的是,享元对象的状态不能改变,否则会引起错误,在Java中,一般使用final来修饰它,如示例中的parent就是用了final修饰符。

把对象共同的部分拆分出来共享,以节省存储空间,在软件编程实践中是常见的一种模式。例如Java中的String类,它有一个intern()方法,就是检查String常量中是否有与本字符串相同的字符串数据,如果有,就直接引用这个已有的字符串;Java虚拟机中有个称为“常量池”的存储模型,在里面存储了所加载类的一些常量数据,就是让Java虚拟机共享使用的;Java类中定义的所有成员方法都在方法区中存放,该类创建的每一个对象都能够共享使用它。虽然它们不是按照享元模式进行设计和实现的,但无一例外都是找出一些细粒度的对象,存储在常量池/方法区中,以供其它对象或模块共享使用,体现了享元模式的思想。

说起与“拆分”相关的模式,我们会发现桥梁模式也使用了“拆分”的方法:把一个对象的抽象角色和实现角色相分离。同桥梁模式相比,桥梁模式是把一个对象的行为按照抽象部分和实现部分进行拆分,让它们能够独立变化,实现部分可以被抽象部分共享和复用功能,而享元模式是让一个对象的属性按照内部状态和外部状态进行拆分,让外部状态被共享。前者拆分的是行为方法,后者拆分的是数据属性,目的都是为了共享。

注意该模式中的共享对象不是整个对象,而是对象中可以共享的部分(细粒度),网上大多数文章在介绍共享模式时,经常使用对象池来举例,个人认为描述的不准确。对象池技术解决的是对象复用,是重复使用,对象被使用的时候,相当于被占有了,别人是用不了的,只有使用完毕后又放回对象池,才能供下一次申请时使用(就像共享单车一样,虽然号称共享,实际上是复用,大家能同时共用一辆单车吗?)。因为复用对象一次只能被一个其它对象占有使用,它可以被改变状态。例如,我们在Andriod中发送Message时,通常会通过Message.obtain()方法从对象池中获取一个Message对象,并设置相关的参数值,在使用完成后归还回对象池时,需要复位Message对象的状态,虽然对象被复用了,但是每次使用同一个对象的时候,它的状态都不一样。

而享元模式解决的是对象共享,被共享的对象可以供大家同时一起使用,不会改变共享对象的状态,是只读的。它的本质核心是从对象中拆分出可以共享的部分,也就是小粒度的共享对象,这样凡是有相同部分的其它对象,就可以引用共享对象了。

你可能感兴趣的:(Android,设计模式,享元模式)