• 结构型设计模式07-享元模式


    结构型设计模式07-享元模式

    1、享元模式介绍

    享元模式是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能。它主要用于处理大量细粒度对象的情况,其中许多对象具有相似的属性和行为。

    在享元模式中,对象分为两种类型:内部状态(Intrinsic State)和外部状态(Extrinsic State)。

    内部状态是对象的固有属性,它们不随外部环境的改变而改变。

    外部状态取决于外部环境,它们在运行时可以改变。

    享元模式的核心思想是将具有相同内部状态的对象共享,以减少内存占用。当需要创建一个对象时,首先检查是否已经存在具有相同内部状态的对象。如果存在,则重用该对象,而不是创建一个新的对象。如果不存在,则创建一个新的对象并将其添加到共享池中,以供以后使用。

    1.1 享元模式基本实现

    享元模式代码结构图:

    img

    Flyweight类

    是所有具体享元类的超类或接口,通过这个接口, Flyweight可以接受并作用于外部状态。

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:06
     * Flyweight类 - 接受外部状态
     */
    public abstract class Flyweight {
        public abstract void operation(int extrinsicState);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ConcreteFlyweight是继承Flyweight超类或实现Flyweight接口,并为内部状态增加存储空间。

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:09
     * 需要共享的具体Flyweight子类
     */
    public class ConcreteFlyweight extends Flyweight {
        @Override
        public void operation(int extrinsicState) {
            System.out.println("需要共享的具体Flyweight子类:" + extrinsicState);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    UnsharedConcreteFlyweight是指那些不需要共享的Flyweight子类。因为Flyweight接口共享成为可能,但它并不强制共享。

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:09
     * 需要共享的具体Flyweight子类
     */
    public class UnsharedConcreteFlyweight extends Flyweight {
        @Override
        public void operation(int extrinsicState) {
            System.out.println("不需要共享的具体Flyweight子类:" + extrinsicState);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    FlyweightFactory是一个享元工厂,用来创建并管理Flyweight对象。它主要是用来确保合理地共享Flyweight,当用户请求一个Flyweight时, FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:12
     * 享元工厂
     */
    public class FlyweightFactory {
        private Hashtable flyweights = new Hashtable();
    
        /**
         * 初始化工厂三个实例
         */
        public FlyweightFactory() {
            flyweights.put("A", new ConcreteFlyweight());
            flyweights.put("B", new ConcreteFlyweight());
            flyweights.put("C", new ConcreteFlyweight());
        }
    
        /**
         * 根据客户端请求,获得已生成的实例
         *
         * @param key
         * @return
         */
        public Flyweight getFlyweight(String key) {
            return flyweights.get(key);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    客户端代码:

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:16
     */
    public class FlyweightClient {
        public static void main(String[] args) {
            int extrinsicState = 22;
            FlyweightFactory factory = new FlyweightFactory();
    
            Flyweight flyweightA = factory.getFlyweight("A");
            flyweightA.operation(--extrinsicState);
            Flyweight flyweightB = factory.getFlyweight("B");
            flyweightB.operation(--extrinsicState);
            Flyweight flyweightC = factory.getFlyweight("C");
            flyweightC.operation(--extrinsicState);
            // 不要共享的
            UnsharedConcreteFlyweight unsharedFly = new UnsharedConcreteFlyweight();
            unsharedFly.operation(--extrinsicState);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果:

    img

    FlyweightFactory根据客户需求返回早已生成好的对象,但一定要事先生成对象实例吗?

    实际上是不一定需要的,完全可以初始化时什么也不做,到需要时,再去判断对象是否为null来决定是否实例化。

    还有个问题,为什么要有UnsharedConcreteFlyweight的存在呢?

    这是因为尽管我们大部分时间都需要共享对象来降低内存的损耗,但个别时候也有可能不需要共享,那么此时的UnsharedConcreteFlyweight子类就有存在的必要了,它可以解决那些不需要共享对象的问题。

    2、具体例子说明

    接单做网站:要求做产品展示网站,有的人希望是新闻发布形式的,有的人希望是博客形式的,也有还是原来的产品图片加说明形式的。

    因为他们找我们来做的人的需求只是有一些小小的差别。

    但是不可能有100家企业来找你做网站,你难道去申请100个服务器,用100个数据库,然后用类似的代码复制100遍,去实现吗?

    2.1 不使用享元模式 - 接单做网站
    //网站
    public class WebSite {
        private String name = "";
        public WebSite(String name) {
            this.name = name;
        }
        public void use() {
            System.out.println("网站分类:" + name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    客户端:

    public class Test {
        public static void main(String[] args) {
            WebSite fx = new WebSite("产品展示");
            fx.use();
            
            WebSite fy = new WebSite("产品展示");
            fy.use();
            
            WebSite fz = new WebSite("产品展示");
            fz.use();
            
            WebSite fl = new WebSite("博客");
            fl.use();
    
            WebSite fm = new WebSite("博客");
            fm.use();
    
            WebSite fn = new WebSite("博客");
            fn.use();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    结果显示:

    img

    如果要做三个产品展示,三个博客的网站,就需要六个网站类的实例,而其实它们本质上都是一样的代码,如果网站增多,实例 也就随着增多,这对服务器的资源浪费得很严重。

    可以利用用户ID的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。

    首先这些客户,他们需要的网站结构相似度很高,而且都不是那种高访问量的网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这造成服务器的大量资源浪费,当然更实际的其实就是钞票的浪费,如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源,而对于代码,由于是一份实 例,维护和扩展都更加容易。

    2.2 适用享元模式 - 接单做网站
    2.2.1 第一版

    网站抽象类:

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:32
     */
    public abstract class WebSite {
        public abstract void use();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    具体网站类:

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:32
     */
    public class ConcreteWebSite extends WebSite {
    
        private String name = "";
    
        public ConcreteWebSite(String name) {
            this.name = name;
        }
    
        @Override
        public void use() {
            System.out.println("网站分类:" + name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    网站工厂类:

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:33
     */
    public class WebSiteFactory {
        private Hashtable flyweights = new Hashtable<>();
    
        /**
         * 获得网站分类
         *
         * @param key
         * @return
         */
        public WebSite getWebSiteCategory(String key) {
            if (!flyweights.contains(key)) {
                flyweights.put(key, new ConcreteWebSite(key));
            }
            return flyweights.get(key);
        }
    
        /**
         * 获得网站实例个数
         *
         * @return
         */
        public int getWebSiteCount() {
            return flyweights.size();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    客户端:

    public class WebStateClient1 {
        public static void main(String[] args) {
            WebSiteFactory webSiteFactory = new WebSiteFactory();
            WebSite siteCategory = webSiteFactory.getWebSiteCategory("产品展示");
            siteCategory.use();
            WebSite siteCategory1 = webSiteFactory.getWebSiteCategory("产品展示");
            siteCategory1.use();
            WebSite siteCategory2 = webSiteFactory.getWebSiteCategory("产品展示");
            siteCategory2.use();
            WebSite siteCategory3 = webSiteFactory.getWebSiteCategory("博客");
            siteCategory3.use();
            WebSite siteCategory4 = webSiteFactory.getWebSiteCategory("博客");
            siteCategory4.use();
            WebSite siteCategory5 = webSiteFactory.getWebSiteCategory("博客");
            siteCategory5.use();
            System.out.println("总共创建了 " + webSiteFactory.getWebSiteCount() + " 个实例");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    结果:

    img

    基本实现了享元模式的共享对象的目的,也就是说,不管建个网站,只要是 ‘产品展示’ ,都是一样的,只要是 ‘博客’ ,也是完全相同的,但这样是有问题的,以上这样建的网站不是一家客户的,它们的数据不会相同,所以至少它们都应该有不同的账号。

    这样写没有体现对象间的不同,只体现了它们共享 的部分。

    2.2.2 内部状态 - 外部状态 - 享元模式 - 接单做网站

    在享元对象内部并且不会随环境改变而改变的共享部分,可以称为享元对象的内部状态,随环境改变而改变的、不可以共享的状态就是外部状态。

    事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个 参数外基本上都是相同的,有时就能够大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来, 就可以通过共享大幅度地减少单个实例的数目。

    也就是说,享元模式 Flyweight 执行时所需的状态有内部的也可能有外部的,内部状态存储于 ConcreteFlyweight 对象之中,而外部对象则应该考虑由客户端对象存储或计 算,当调用Flyweight对象的操作时,将该状态传递给它。

    客户的账号就是外部状态,应该由专门的对象来处理。代码结构图:

    img

    用户类,用于网站的客户账号,是"网站"类的外部状态。

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:49
     */
    public class User {
        private String name;
    
        public User(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    网站抽象类:

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:32
     */
    public abstract class WebSite {
        public abstract void use(User user);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    具体网站类:

    /**
     * @author Shier
     * CreateTime 2023/5/22 16:32
     */
    public class ConcreteWebSite extends WebSite {
    
        private String name = "";
    
        public ConcreteWebSite(String name) {
            this.name = name;
        }
    
        @Override
        public void use(User user) {
            System.out.println("网站分类:" + name + " 来自客户:" + user.getName() + "的需求");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    网站工厂类不要改变同上。

    客户端:

    最后得到的结果:

    img

    这样就可以协调内部与外部状态了。由于用了享元模 式,哪怕接手了1000个网站的需求,只要要求相同或类似,你的实际开发代码也就是分类的那几种,对于服务器来说,占用的硬盘空间、内存、CPU资 源都是非常少的,这确实是很好的一个方式。money自然就到手了。

    3、享元模式总结

    3.1 享元模式应用

    如果一个应用程序使用了大量的对象,而大 量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大 多数状态可以是外部状态,如果删除对象的外部状态,那么可以用相对较少 的共享对象取代很多组对象,此时可以考虑使用享元模式。

    实际上在Java中,字符串String就是运用了Flyweight模式。举个例子吧。== 可以用来确定 titleA 与 titleB 是否是相同的实例,返回值为 boolean 值。当用 new String() 方法时,两个对象 titleA 和 titleB 的引用地址是不相同的,但当 titleC 和 titleD 都使用赋值的方式时,两个字符串的引用地址竟然是相同的。

    c580419c246d0995dd1b40f6a20916e6.png

    得出结果:

    img

    享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的。

    比如说休闲游戏开发中,像围棋、五子棋、跳棋等,它们都有大量的棋子对象,分析一下,它们的内部状态和外部状态各是什么?

    围棋和五子棋只有黑白两色、跳棋颜色略多一些,但也是不太变化的, 所以颜色应该是棋子的内部状态,而各个棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态。

    像围棋,一盘棋理论上有361个空位可以放棋子,那如果用常规的面向对象方式编程,每盘棋都可能有两三百个棋子对象产生,一台服务器就很难支持更多的玩家玩围棋游戏了,毕竟内存空间还是有限的。如果用了享元模式来处理棋子,那么棋子对象可以减少到只有两个实例。

    在某些情况下,对象的数量可能会太多,从而导致了运行时的资源与性能损耗。

    那么我们如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题。

    享元模式,可以运用共享技术有效地支持大量细 粒度的对象。不过,使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系 统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。

    因此,应当在有足够多的对象实例可供共享时才值得使用享元模式。

    3.2 优缺点、适用场景总结
    优点

    内存优化:通过共享对象的内部状态,可以大大减少对象的数量,从而节省内存空间。

    性能提升:减少对象数量和内存占用可以提高系统的性能,特别是在处理大量细粒度对象时。

    可扩展性:享元模式可以轻松扩展以支持新的外部状态,而无需修改现有的共享对象。

    对象复用:通过共享对象,可以实现对象的复用,避免重复创建相同的对象,提高系统的效率和资源利用率。

    缺点:

    共享对象可能引入线程安全问题:如果多个线程同时访问和修改共享对象的状态,需要考虑线程安全性,以避免并发访问引发的问题。

    对象状态的外部化:为了实现对象共享,部分对象状态需要外部化,这可能会增加代码复杂性和维护成本。

    适用场景:

    当系统中存在大量相似对象,且这些对象的内部状态相同或相似,而外部状态有所不同时,可以考虑使用享元模式。

    当需要创建大量对象,但创建和销毁对象的代价很高时,可以使用享元模式来复用已有对象,提高系统性能。

    当系统需要缓存对象以提高性能,并且可以忍受一定程度的对象状态变化时,享元模式也是一个不错的选择。

    当对象的状态可以被多个对象共享,并且这些对象之间的外部状态可以相对独立时,可以使用享元模式。

    使用享元模式的一个常见例子是文本编辑器中的字符对象。在一个文本文件中,可能有大量的字符对象,它们的外部状态(例如位置、字体、颜色等)可能不同,但内部状态(例如字符代码、字符宽度等)是相同的。通过共享具有相同字符代码的字符对象,可以大大减少内存使用

    总之,享元模式适用于大量细粒度对象的场景,通过共享对象的内部状态来减少对象数量和内存占用,从而提高系统性能和资源利用率。它在需要重复创建相似对象的情况下特别有用,并且适用于多线程环境,但需要注意线程安全性。

    这可能会增加代码复杂性和维护成本。

    适用场景:

    当系统中存在大量相似对象,且这些对象的内部状态相同或相似,而外部状态有所不同时,可以考虑使用享元模式。

    当需要创建大量对象,但创建和销毁对象的代价很高时,可以使用享元模式来复用已有对象,提高系统性能。

    当系统需要缓存对象以提高性能,并且可以忍受一定程度的对象状态变化时,享元模式也是一个不错的选择。

    当对象的状态可以被多个对象共享,并且这些对象之间的外部状态可以相对独立时,可以使用享元模式。

    使用享元模式的一个常见例子是文本编辑器中的字符对象。在一个文本文件中,可能有大量的字符对象,它们的外部状态(例如位置、字体、颜色等)可能不同,但内部状态(例如字符代码、字符宽度等)是相同的。通过共享具有相同字符代码的字符对象,可以大大减少内存使用

    总之,享元模式适用于大量细粒度对象的场景,通过共享对象的内部状态来减少对象数量和内存占用,从而提高系统性能和资源利用率。它在需要重复创建相似对象的情况下特别有用,并且适用于多线程环境,但需要注意线程安全性。

  • 相关阅读:
    CRC校验(模型、手算、程序编写)
    【JVM】MySQL驱动加载如何打破双亲委派机制
    Java系列 | 如何讲自己的JAR包上传至阿里云maven私有仓库【云效制品仓库】
    css详细笔记
    .NET使用QuestPDF高效地生成PDF文档
    Postman如何做接口自动化测试?
    MySQL 解决数据重复添加
    Java常见设计模式总结
    IB课程四大要领,一起来学习学习
    2024年腾讯云优惠政策_腾讯云TOP10优惠活动
  • 原文地址:https://blog.csdn.net/weixin_46294086/article/details/134366045