设计模式-享元模式

定义

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

适用场景

例如,数据库连接,线程的创建开销都比较大,并且创建频率也非常高,因此就需要用到数据库连接池技术和线程池技术来共享数据库连接和线程。再例如,应用系统中通常存在最多的就是字符串,并且往往会大量重复,如果每次都创建新的字符串,可能会导致内存溢出、GC阻塞等性能问题,因此也就有了字符串驻留池技术。应用场景虽然天差地别,但是无论哪一种场景,往往都会具备如下两个特点:

  • 系统会用到大量相同或相似的对象;
  • 对象创建比较耗时。

目的

而享元模式正是为了应对上述问题,并达到如下两个目的而存在的:

  • 减少创建对象的数量;
  • 对象全局共享。

示例

其实,说到享元模式,我们最先应该提到的就是活字印刷术,因为它就是享元模式在生活中的一种最佳实践。我们知道,出版一本哪怕百万字的著作,其实常用汉字也不过三千多个,这其中会有大量重复。传统的雕版印刷,每次印刷都需要先花大量的时间刻雕版,并且还不能重复使用,但是活字印刷就将共享和复用的特点发挥到了极致,省去了大量的时间。

其实,这种例子生活中并不罕见,例如,图书馆借书,共享单车,共享雨伞,共享马扎等哪个不是享元模式思想的体现?因为享元模式的核心思想正是共享

我们下面还是以活字印刷举例,通过代码的方式来实现一个印刷HELLO WORLD的例子加以说明。

模式演进

首先,我们先把字模刻出来:

public abstract class Typeface
{
    public abstract string Print();
}

public class DTypeface : Typeface
{
    public override string Print()
    {
        return "D";
    }
}

public class ETypeface : Typeface
{
    public override string Print()
    {
        return "E";
    }
}

...

上面是简单的示意,其他字母以此类推,表示一个个的字模。通过这些字模我们就可以印刷出版了。

static void Main(string[] args)
{
    Typeface h = new HTypeface();
    Typeface e = new ETypeface();
    Typeface l = new LTypeface();
    Typeface o = new OTypeface();
    Typeface w = new WTypeface();
    Typeface r = new RTypeface();
    Typeface d = new DTypeface();

    Console.WriteLine($"{h.Print()}{e.Print()}{l.Print()}{l.Print()}{o.Print()} {w.Print()}{o.Print()}{r.Print()}{l.Print()}{d.Print()}");
}

但是很遗憾,虽然印刷成功了,但是这些字模并不能全局共享,说到底还是一次性的,换一个地方还得重新创建一次。不过说到全局共享,我们前面好像就有一种模式可以办到,没错,就是单例模式。我们不妨先用单例模式试试看:

public class ETypeface : Typeface
{
    private static readonly Typeface _instance = new ETypeface();

    private ETypeface() { }

    public static Typeface Instance => _instance;

    public override string Print()
    {
        return "E";
    }
}

将每个字模都实现成如上所示的单例,再看看调用的地方:

static void Main(string[] args)
{
    Console.WriteLine($"{HTypeface.Instance.Print()}" +
        $"{ETypeface.Instance.Print()}{LTypeface.Instance.Print()}" +
        $"{LTypeface.Instance.Print()}{OTypeface.Instance.Print()}");
}

印刷成功了,并且也全局共享了。不过中国汉字何其多,全部实现成单例,类爆炸了,一个系统中成千上万个单例,想想都可怕。不过好在处理类爆炸我们是有经验的,没错,就是合并:

public class TypefaceFactory
{
    private static readonly Typeface _h = new HTypeface();
    private static readonly Typeface _e = new ETypeface();
    private static readonly Typeface _l = new LTypeface();
    private static readonly Typeface _o = new OTypeface();

    public static Typeface H => _h;
    public static Typeface E => _e;
    public static Typeface L => _l;
    public static Typeface O => _o;
}

我们额外定义一个类,把所有单例字模都合并进去,不过我们这时静态属性如果还叫Instance就命名冲突了,直接以字母命名好了,这样我们就把所有单例都消灭了。虽然有所改善,不过字模太多的问题依然严峻,因为这个类中会封装成千上万的字模,并且随时可能更改,这导致这个类极不稳定。不过好在所有字模都继承自同一个基类,因此,我们可以用一个字典存储,并且通过一个静态方法获取字模:

public class TypefaceFactory
{
    private static readonly IDictionary<Type, Typeface> _typefaces
        = new Dictionary<Type, Typeface>();

    public static Typeface GetTypeface<TTypeface>() where TTypeface : Typeface
    {
        Type type = typeof(TTypeface);
        if (_typefaces.ContainsKey(type))
        {
            return _typefaces[type];
        }

        Typeface typeface = Activator.CreateInstance(typeof(TTypeface)) as Typeface;
        _typefaces.Add(type, typeface);
        return typeface;
    }
}

这样的话就好多了,可以管理大量细粒度的对象,并且也可以全局共享了,满足了我们的需求,不知大家有没有发现,这里非常像简单工厂模式,只不过这里用到了一个静态字典做缓存,并非每次都全新创建对象,其实这就是享元模式。

UML类图

再来抽象一下,看看享元模式的类图:

  • FlyweightFactory:享元工厂,用来创建并管理Flyweight对象
  • Flyweight:享元类的基类或接口
  • ConcreteFlyweight:具体的Flyweight子类
  • UnsharedConcreteFlyweight:不需要共享的Flyweight子类

在本例中,UnsharedConcreteFlyweight并没有用到,但是作为享元模式中的一个角色确实是存在的,只是不可共享而已。例如,字模中有规范汉字,也有非规范汉字,但是出版刊物必须使用规范汉字,而不能使用非规范汉字。不过,我们软件开发中会较少用到,因为,既然用不到,就没必要去实现了。

优缺点

优点

  • 节省内存空间,因为全局共享一个或者少数几个对象而已;
  • 提高效率,因为不用每次都进行费时的初始化操作。

缺点

增加了系统的复杂度,其实我们通过线程池和数据库连接池就不难发现,确实复杂了很多。

改进

其实,到这里我们并没有结束,如果还记得单例模式的话,我们知道这种实现是存在并发问题的,没错,既然同样是用静态字段做共享,那么这里同样存在这并发问题,不过这里并发的是一个代码段,而不是简单的一个字段,因此就不能简单的通过Lazy关键字解决了,这里必须使用双检锁:

public class TypefaceFactory
{
    private static readonly IDictionary<Type, Typeface> _typefaces
        = new Dictionary<Type, Typeface>();

    private static readonly object _locker = new object();
    public static Typeface GetTypeface<TTypeface>() where TTypeface : Typeface
    {
        Type type = typeof(TTypeface);
        if (!_typefaces.ContainsKey(type))
        {
            lock (_locker)
            {
                if (!_typefaces.ContainsKey(type))
                {
                    Typeface typeface = Activator.CreateInstance(typeof(TTypeface)) as Typeface;
                    _typefaces.Add(type, typeface);
                }
            }
        }

        return _typefaces[type];
    }
}

好了,这次完美了。

不过呢,不知大家有没有疑惑,从上面演进步骤看,享元模式好像是单例模式和简单工厂模式的综合运用,为什么享元模式会归类到结构型模式而不是创建型模式呢?其实,原因很简单,从表面上看,好像享元模式的享元工厂也在负责创建对象,但实际上,享元模式最主要的目的是对象的管理而不是创建,例如,我们还可以通过如下方式实现享元模式:

public class TypefaceFactory
{
    private static readonly IDictionary<string, Typeface> _typefaces
        = new Dictionary<string, Typeface>();

    private static readonly object _locker = new object();

    public static void SetTypeface(string key, Typeface typeface)
    {
        if (!_typefaces.ContainsKey(key))
        {
            lock (_locker)
            {
                if (!_typefaces.ContainsKey(key))
                {
                    _typefaces.Add(key, typeface);
                }
            }
        }
    }

    public static Typeface GetTypeface(string key)
    {
        if (_typefaces.ContainsKey(key))
        {
            return _typefaces[key];
        }

        return null;
    }
}

看到了吗?这里就把对象的创建交给了客户端完成,而享元工厂只负责对象的管理,并不负责对象创建了。

与单例模式的区别

  • 享元模式是共享大量类的大量实例,而单例是一个类一个实例;
  • 单例模式针对的是对象的创建,而享元模式针对的是对象的管理;
  • 单例模式不能单独创建,而享元模式中的类可以单独创建。

实际上,享元模式也可以用来实现单例模式。

与简单工厂模式的区别

  • 享元模式在简单工厂模式的基础上加入了缓存;
  • 简单工厂模式的作用仅仅是创建对象,而享元模式虽然也创建对象,但其主要作用是管理和共享对象。

总结

享元模式实现起来非常灵活,它更重要体现的是一种思想,它不仅在生活中被广泛运用,在软件开发过程中也被广泛运用。不妨把上述享元工厂再换一个场景,例如把静态字典换成Redis,再把GetTypeface方法换成高并发环境下的查询接口,再去看看执行流程。发现了吧?就是我们每天都在写的代码。
用心发现,享元模式真的是无处不在!

源码链接

(0)

相关推荐

  • 单例模式的实现

    记一下学习单例模式的笔记: 单例就是要保证该类仅有一个实例.实现完全封闭的单例(外部不能new)其实就要两点要求: 全局访问:需要一个该类型的全局静态变量,每次获取实例时都要判断它是否null,不存在 ...

  • PHP设计模式之享元模式

    PHP设计模式之享元模式 享元模式,"享元"这两个字在中文里其实并没有什么特殊的意思,所以我们要把它拆分来看."享"就是共享,"元"就是元素 ...

  • [PHP小课堂]PHP设计模式之享元模式

    [PHP小课堂]PHP设计模式之享元模式 关注公众号:[硬核项目经理]获取最新文章 添加微信/QQ好友:[DarkMatterZyCoder/149844827]免费得PHP.项目管理学习资料

  • 设计模式之享元模式

    享元模式 Flyweight Intro 享元是指一个可复用的对象,通过复用这个享元来减少应用中的内存分配. 享元模式是为了减少内存占用,尽可能复用已有对象的设计模式,一般来说会把这个可复用的对象放到 ...

  • 无废话设计模式(9)结构型模式--享元模式

    0-前言 享元模式定义:运用共享技术有效地支持大量细粒度的对象. 1-实现 1-1.简单UML图:  1-2.代码实现 //1.抽象父类(网站父类) abstract class Website { ...

  • 设计模式(11) 享元模式

    基于面向对象思想设计的应用程序有时遇到需要场景大量相同或显示对象实例的场景,这些数量庞大的实例很可能会消耗很多系统资源,最直接的就是内存了.比如要一款围棋游戏,如果每次落子都新建一个对象,将会占用大量 ...

  • 设计模式 --面试高频之享元模式

    前言 享元模式是非常常用的一种结构性设计模式. 特别是在面试的时候.当我们把这一节内容掌握,我相信不管是工作中还是面试中这一块内容绝对是一大亮点. 什么是享元模式 所谓"享元",顾 ...

  • 泡图书馆,我想到了 享元模式

    回复"000"获取程序员必备电子书 大家好,我是老田,今天我给大家分享设计模式中的享元模式.用贴切的生活故事,以及真实项目场景来讲设计模式,最后用一句话来总结这个设计模式. 另外, ...

  • Flyweight享元模式

    >>返回<C#常用设计模式> 1. 简介 2. 示例 1. 简介 定义 使用共享对象可有效地支持大量的细粒度的对象. 解决问题 面向对象技术可以很好地解决一些灵活性或可扩展性问 ...

  • 结构型模式之享元模式

    在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题.创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈.例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网 ...