3

享元模式是一种结构型设计模式,大家对数据库和缓冲池有了解的同学应该很明白这些都是对享元模式比较好的应用,所以说享元模式是池技术的重要实现方式。

比如我们每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存开销就会很大,内存开销也会相对的变大,所以如果第一次创建了一个对象,下次再使用到相同结构的对象的时候,只需要把它的引用指向到这个对象上,这样就实现了这个对象在内存中的共享。

比如我们们要做一个五子棋的有系,如果用户每下一个棋子都要创建一个对象,那么一盘棋下来可能会创建上百个对象,这种使用享元模式再合适不过了,将棋子对象减少到几个实例。

什么是享元模式

享元模式:是一种软件设计模式。它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于只是因重复而导致使用无法令人接受的大量内存的大量物件。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。 —— 节选自百度百科

享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片的对象,通过在应用中设置该图片出现的位置,可以实现该图片在不同地方多次重复出现。

享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。

因为要求细粒度对象,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。内部状态指对象共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变;外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

享元模式优缺点

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态, 以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比, 内在状态的变体要少很多), 因此你所需的对象数量会大大削减。

优点
  1. 享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率
  2. 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
缺点
  1. 提高了系统复杂性,需要分离出内部状态和外部填充,并且外部状态具有固化特性,不应该随内部状态改变,否则会导致系统的逻辑混乱
  2. 执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据,为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长

示例

享元模式只是一种优化。在应用该模式之前,你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。

享元模式的主要角色如下:

  1. 享元:类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为内在状态。 传递给享元方法的状态被称为外在状态
  2. 情景:类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
  3. 客户端:负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。
  4. 享元工厂:会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。

通常情况下, 原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。

类图如下所示:

image

代码示例:

abstract class Flyweight {
    public abstract doOperation(extrinsicState : string) : void;
}

class ConcreteFlyweight extends Flyweight {
    private intrinsicState : string;
    constructor(intrinsicState : string) {
        super();
        this.intrinsicState = intrinsicState;
    }

    public doOperation(extrinsicState : string) : void {
        console.log(`这是具体享元角色,内部状态为${this.intrinsicState},外部状态为${extrinsicState}`);
    }
}

interface flyweightObject {
    [key : string] : Flyweight
}

class FlyweightFactory {
    private flyweights : flyweightObject;
    constructor() {
        this.flyweights = {};
    }

    public getFlyweight(intrinsicState : string) : Flyweight {
        if (!this.flyweights[intrinsicState]) {
            const flyweight : Flyweight = new ConcreteFlyweight(intrinsicState);
            this.flyweights[intrinsicState] = flyweight;
        }
        return this.flyweights[intrinsicState];
    }
}

function main() {
    const factory : FlyweightFactory = new FlyweightFactory();
    const flyweight1 : Flyweight = factory.getFlyweight("aa");
    const flyweight2 : Flyweight = factory.getFlyweight("aa");
    flyweight1.doOperation('x');
    flyweight2.doOperation('y');
}

main();

总结

如果程序里使用大量相同或者类似对象,造成内存的消耗过大,且大多都是外部状态,这时候应该考虑使用享元模式了。如果你能将对象的所有共享状态简化为一个享元对象,那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。只会有一个单例实体,但是享元类可以有多个实体, 各实体的内在状态也可以不同。单例对象可以是可变的。 享元对象是不可变的。


Aaron
4k 声望6.1k 粉丝

Easy life, happy elimination of bugs.