设计模式(三)-结构型模式(6)-享元模式

发布时间:2023年12月22日

一、为何需要享元模式(Flyweight)?

假如在网页中渲染这样的一个画面:大小不一的星星铺满了整个画布,并且都在不断的进行移动闪烁着。一批星星消失了,另一批又从另一边缘处出现。

请添加图片描述

要实现这样的渲染效果,在程序中就得需要创建这些星星,然后将它们一个个画上去。

有个问题就是,如果我们还得按照平常创建对象的方式,对每一颗出现的星星都创建一遍。一旦星星从画布上消失,就销毁并释放内存。这么做的话,系统就得要开销大量的内存空间,而且频繁进行创建销毁的操作会影响程序运行的性能。到最后我们只能看到这样的效果:一打开网页后,画布上的那些星星隔了一段时间才全部出现,滚动网页也不流畅。

为了解决“隔了一段时间才全部出现”和“滚动网页也不流畅”的问题(因为存储大量对象而导致开销大量内存、频繁创建销毁对象而导致程序的性能下降),我们就应该尽可能少的生成那些存在相同状态的星星。

其实在繁星点点的夜空中,总会有很多相似或者相同的星星存在着。因此我们可以只创建一颗星星作为一个共享对象,来代表这些与之相似的同类星星。在渲染的时候,只对这个共享对象重复的画上去就行了。

享元模式的定义:运用共享技术有效的支持大量细粒度的对象。

从以上的定义,我们先分析“享元”和”细粒度“这两个关键词。

  • 享元即共享元对象。元这个词,就好比数据库表中的一组若干个元数据,元数据是数据的最小单位。所以元对象的意思也就是,一组同类对象中的每一个元对象。既然这些同类对象都是相同状态的对象,我们只需要创建一个共享元对象来代表它们。
  • 细粒度如颗粒大小一样,即这个对象并不庞大而复杂。所以享元对象就应当是结构简单的对象。

但是我们怎么区分对象之间是否存在相同状态,然后将他们归类为某一组同类对象呢。于是可以对这些对象内的某个属性作为唯一标识,来判断是否为同类对象。比如星星,唯一标识可以是星星的半径值,也可以是速度值等。在这里我们选择的是以半径作为唯一标识。如图所示:
(在享元对象容器,每一个享元对象之间的半径值不相同,都代表着各自一组与自己半径相同的星星。)

请添加图片描述

享元对象是有了,但是我们编程中,起码也要保证唯一标识的半径值,是不能因为程序的变化而变化吧,即对象的外部不能对半径进行修改。这时候就有了外部状态和内部状态的区别。

外部状态: 存在于享元对象的外部。因环境变化而变化。(环境变化即客户端发生的状态变化)
内部状态: 存在于享元对象的内部。不能因环境变化而变化。

  • 外部状态是客户端进行的活动变化,比如为星星的位置进行随机布局,控制星星消失和出现的个数等等。

  • 内部状态是对象固有的状态,即一颗半径大小为4的星星,半径大小是固有的,客户端把它画上去时,不能强行使它变大变小。要保持内部状态,对象内的所有属性和状态都应当被保护起来,如对象内的所有字段都被设置为私有的访问机制。

特点:

  • 减少创建对象的数量,使用享元对象来代表一组同类对象。

结构:
抽象享元(Flyweight):具体享元类的基类。规定了具体享元要实现的方法,,也可以接受并作用于外部状态。(接受并作用于:传外部状态的参数到该方法内,以处理外部状态,但不改变享元内部状态。)
具体享元类(ConcreteFlyweight):实现抽象享元的方法。如果存在内部状态(即在享元容器里没有存在该享元时,就实例化一个),则增加存储空间。
享元工厂类(FlyweightFactory):创建和管理享元对象。
客户端类(Client):所有享元对象的引用,并存储对应的外部状态。(引用:把星星画上去;存储外部状态:存储星星的位置和出现的个数等)

适合应用场景特点:

  • 需要大量细粒度的对象,来完成某个功能。
  • 大量对象有存在着相同的状态。
  • 实际例子:线程池中的共享线程,解决频繁创建销毁线程的问题,字符串常量池中的共享字符串,解决重复创建存在值相同的字符串的问题…

请添加图片描述

二、例子

需求:

在视频剪辑中,用户想要在画布上实现满天星的效果,就做了如下参数的设置:
1)星星的总数为1000000,并且这些星星的大小和个数都是随机的;
2)星星的半径大小在 1~10 范围;
3)勾选默认星星的速度、闪烁频率、亮度都跟半径的大小有关,所以不用用户来自定义这些参数的值。

设计分析:

  • 以半径作为唯一标识,一个享元对象对应一组半径相同的对象。
  • 享元对象最多有 10 个。需要渲染的对象有1000000 个。

1、定义抽象享元和具体享元类


    //Flyweight:抽象享元(星星抽象)
    public interface IStar
    {
        void draw();
    }

    //ConcreteFlyweight:具体享元类(星星)
    public class Star:IStar
    {
        private int Radius;
        private double Brightness;
        private double Twinkle;
        private double Speed;

        public Star(int radius)
        {
            Radius = radius;
            //Brightness = ...;
            //Twinkle = ...;
            //Speed = ...; 
        }

        //1、args:外部状态可作为参数传入,但不能改变 Star 的内部状态
        public void draw(/*args:若有外部实例传入*/)
        {
            //处理一些与外部有关的逻辑...
            //外部参数不能改变 Star 类的所有属性值和其他内部状态
        }

        //2、如果该方法在客户端进行调用,而不是在享元工厂创建 Star 类的享元对象时调用:
        //那么该方法的内部逻辑是错误的,是因为不能通过外部 val 改变 Brightness 值。
        public void setBrightness(double val)
        {
            Brightness = val;
        }
    }


2、定义享元工厂类


    //FlyweightFactory:享元工厂类(创建和管理享元对象)
    public class FlyweightFactory
    {
        //享元对象的容器
        private Dictionary<int, IStar> StarsDict = new Dictionary<int, IStar>();
        //创建和获取享元对象,radius 为标识享元对象的参数。
        public IStar getStar(int radius)
        {
            IStar star = null;
            //在容器中是否存在跟 radius 值相同的对象
            StarsDict.TryGetValue(radius,out star);

            if(star == null)//若不存在,则创建一个享元并存入到容器里
            {
                star = new Star(radius);
                StarsDict.Add(radius,star);
            }

            return star;
        }
    }

3、主程序


//主程序
    class Program
    {
        static void Main(string[] args)
        {
            Random random = new Random(10);

            //享元模式-----------------
            FlyweightFactory factory = new FlyweightFactory();
            for (int i = 0; i < 1000000; i++)
            {
                //随机生成半径大小为1~10范围的星星。
                var star = factory.getStar(random.Next(1, 10));
                star.draw();//对外部状态的影响
            }
            //-------------------------

            //非享元模式----------------
            List<IStar> starList = new List<IStar>();
            for (int i = 0; i < 1000000; i++)
            {
                //创建了 1000000 个对象,执行整个循环的时间久。
                var star = new Star(random.Next(1, 10));
                starList.Add(star);
                star.draw();
            }
            //--------------------------

            //验证非享元模式和享元模式,在当前程序里使用内存大小的情况:
            var process = Process.GetCurrentProcess();
            var memorySize = process.PrivateMemorySize64 / (1024 * 1024);//单位为 M.
            Console.WriteLine(memorySize);

            //某次运行验证的结果:
            //享元模式,内存占用:17M;非享元模式,内存占用:60M;
            
            Console.ReadLine();
        }
    }

文章来源:https://blog.csdn.net/chen1083376511/article/details/135155001
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。