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

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

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

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

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

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

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

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

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

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

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

(在享元对象容器,每一个享元对象之间的半径值不相同,都代表着各自一组与自己半径相同的星星。)

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

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

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

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

特点:

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

结构:

抽象享元(Flyweight):具体享元类的基类。规定了具体享元要实现的方法,,也可以接受并作用于外部状态。(接受并作用于:传外部状态的参数到该方法内,以处理外部状态,但不改变享元内部状态。)

具体享元类(ConcreteFlyweight):实现抽象享元的方法。如果存在内部状态(即在享元容器里没有存在该享元时,就实例化一个),则增加存储空间。

享元工厂类(FlyweightFactory):创建和管理享元对象。

客户端类(Client):所有享元对象的引用,并存储对应的外部状态。(引用:把星星画上去;存储外部状态:存储星星的位置和出现的个数等)

适合应用场景特点:

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

二、例子

需求:

在视频剪辑中,用户想要在画布上实现满天星的效果,就做了如下参数的设置:

1)星星的总数为1000000,并且这些星星的大小和个数都是随机的;

2)星星的半径大小在 1~10 范围;

3)勾选默认星星的速度、闪烁频率、亮度都跟半径的大小有关,所以不用用户来自定义这些参数的值。

设计分析:

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

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

c 复制代码
    //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、定义享元工厂类

c 复制代码
    //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、主程序

c 复制代码
//主程序
    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();
        }
    }
相关推荐
卡尔特斯3 小时前
Android Kotlin 项目代理配置【详细步骤(可选)】
android·java·kotlin
白鲸开源3 小时前
Ubuntu 22 下 DolphinScheduler 3.x 伪集群部署实录
java·ubuntu·开源
ytadpole3 小时前
Java 25 新特性 更简洁、更高效、更现代
java·后端
纪莫3 小时前
A公司一面:类加载的过程是怎么样的? 双亲委派的优点和缺点? 产生fullGC的情况有哪些? spring的动态代理有哪些?区别是什么? 如何排查CPU使用率过高?
java·java面试⑧股
JavaGuide4 小时前
JDK 25(长期支持版) 发布,新特性解读!
java·后端
用户3721574261354 小时前
Java 轻松批量替换 Word 文档文字内容
java
白鲸开源4 小时前
教你数分钟内创建并运行一个 DolphinScheduler Workflow!
java
晨米酱5 小时前
JavaScript 中"对象即函数"设计模式
前端·设计模式
Java中文社群5 小时前
有点意思!Java8后最有用新特性排行榜!
java·后端·面试
代码匠心5 小时前
从零开始学Flink:数据源
java·大数据·后端·flink