【设计模式】享元模式的使用场景及与其他共享技术的对比-程序员宅基地

技术标签: java  # 设计模式  享元模式  架构与设计  设计模式  

1.概述

享元模式(Flyweight Pattern)是一种非常常用的结构型设计模式,通过共享对象的方式,减少系统中的重复对象,提高内存使用效率

2.享元模式

2.1.核心概念

先看一下设计模式中对享元模式的定义:

Use sharing to support large numbers of fine-grained objects efficiently

翻译过来就是享元模式突出对细粒度对象共享,需要说明一下这里的细粒度对象。

在软件工程中,通常是指那些职责单一、功能细化的小型对象,也就是将大的实体或概念分解为多个小的、独立的对象。

在享元模式中,享元类一般有两种状态,分别是:

  • 内部状态(Intrinsic State):不可变部分,通常是作为类的成员变量存储在享元类(Flyweight)的实例中,在创建享元对象时通过构造方法进行初始化,在整个生命周期内保持不变或由享元类自身管理。
  • 外部状态(Extrinsic State):可变部分,不由享元对象直接维护,在方法调用时,客户端负责提供当前需要应用的外部状态信息

注:享元模式中的外部状态并不是必须存在的。

2.2.实现案例

下面以扑克牌为例子来解释一下这两种状态,在常规的扑克牌游戏中,一共有4种花色和13种点数。除了花色与点数之外,扑克牌还有一些属性,例如:牌的大小和价值、牌在哪张牌桌上、在哪个玩家手上、是否在牌堆中,等等。
我们将扑克牌类创建为享元类,按照上述的定义方式,将属性拆解为不同的状态,其中:

  • 内部状态:花色、点数,这部分属性恒定不变,可以由扑克牌类自行维护。
  • 外部状态:牌桌号、玩家对象、牌的规则价值等,这部分属性会随着游戏的变化而变化,不由扑克牌类维护。

2.2.1.内部状态实现

首先看代码中是如何定义的内部状态的:

  • 由于花色和点数是恒定的,此处先定义两个枚举:
    @Getter
    public enum SuitsEnum {
          
        HEART("红桃"),
        SPADE("黑桃"),
        DIAMOND("方片"),
        CLUB("梅花");
    
        private final String name;
        SuitsEnum(String name) {
          
            this.name = name;
        }
    
    }
    
    @Getter
    public enum PointEnum {
          
        THREE("3"),
        FOUR("4"),
        FIVE("5"),
        SIX("6"),
        SEVEN("7"),
        EIGHT("8"),
        NINE("9"),
        TEN("10"),
        J("J"),
        Q("Q"),
        K("K"),
        A("A"),
        TWO("2");
    
        private final String name;
        PointEnum(String name) {
          
            this.name = name;
        }
    }
    
  • 定义扑克牌的享元类,里面只有花色和点数两个属性:
    /**
     * 扑克享元类
     */
    @Getter
    public class Poker {
          
        private SuitsEnum suitsEnum;
        private PointEnum pointEnum;
    
        public Poker(SuitsEnum suitsEnum, PointEnum pointEnum) {
          
            this.suitsEnum = suitsEnum;
            this.pointEnum = pointEnum;
        }
    }
    
  • 最后我们定义一个扑克牌工厂,用于共享已生成的扑克牌对象
    /**
     * 扑克享元工厂
     */
    public class PokerFactory {
          
    
        private static final Poker[][] pokers = new Poker[13][4];
    
        static {
          
            init();
        }
    
        public static void init() {
          
            for (int i = 0; i < 13; i++) {
          
                for (int j = 0; j < 4; j++) {
          
                    pokers[i][j] = new Poker(SuitsEnum.values()[j], PointEnum.values()[i]);
                }
            }
        }
    
        public static Poker getPoker(int point, int suit) {
          
            return pokers[point][suit];
        }
    	
        /**
         * 创建牌堆
         */
        public static List<Poker> createPokers() {
          
            List<Poker> pokerList = new ArrayList<>();
            for (int i = 0; i < 13; i++) {
          
                pokerList.addAll(Arrays.asList(pokers[i]));
            }
            return pokerList;
        }
    
    }
    
    其实,所谓的共享,就是用一个数据结构将已生成的对象缓存起来的,数据结构可以是数组,也可以是的MapList等等,由于扑克牌的数量和花色、点数是恒定的,所以使用了一个二维数组存储并做了初始化,客户端可以通过点数+花色的方式来获取扑克对象。

在这里插入图片描述

2.2.2.外部状态实现

上面我们提到了,外部状态不由享元对象直接维护,说的更具体一点就是指那些与享元对象关联但不由该对象控制的信息,例如一个扑克牌游戏的玩家需要持有某张牌,需要经过发牌器发到玩家的手上,这里的发牌器对象与玩家对象都可以视为扑克牌对象的外部状态。

  • 玩家类
    public class Player {
          
    
        private String name;
    
        public Player(String name) {
          
            this.name = name;
        }
    
        private List<Poker> pokers = new ArrayList<>();
    
        public void addPoker(Poker poker) {
          
            pokers.add(poker);
        }
    
        public void showPokers() {
          
            String msg = name + ":";
            for (Poker poker : pokers) {
          
                msg += poker.getSuitsEnum().getName() + poker.getPointEnum().getName() +  " ";
            }
            System.out.println(msg);
        }
    
    }
    
  • 发牌器,假设当前是个炸金花的游戏,给每个玩家发三张牌
    public class Shuffler {
          
    
        public static void deal(List<Player> playerList) {
          
            List<Poker> pokers = PokerFactory.createPokers();
            // 打乱牌堆
            Collections.shuffle(pokers);
    
            // 每人发3张牌
            for (int i = 0; i < 3; i++) {
          
                for (Player player : playerList) {
          
                    player.addPoker(pokers.remove(0));
                }
            }
    
        }
    }	
    
  • 游戏服务
    public class GameServer {
          
    
        public static void main(String[] args) {
          
            List<Player> list = Arrays.asList(new Player("张三"), new Player("李四"), new Player("王五"));
            Shuffler.deal(list);
    
            for (Player player : list) {
          
                player.showPokers();
            }
    
        }
    }
    

执行之后的结果,很明显李四以一对J获得胜利。

张三:梅花3 梅花9 梅花6
李四:红桃4 梅花J 黑桃J
王五:黑桃5 红桃3 方片A

发牌器获取到牌堆时,扑克牌对象属于发牌器对象,而在发牌的过程中,某一些牌对象的关联关系由发牌器对象转移到了玩家对象
通过上面的例子,可以感受到外部状态并不是在指某个具体的属性,而是享元对象与其他对象之间的关联关系,这部分关系随时可能发生变化。

2.3.更多场景

看到这里,如果熟悉工厂和单例模式的话就很容易发现,享元模式的这种实现方式其实就是工厂模式+单例模式的一种拓展实现,在之前的博客《SpringBoot优雅使用策略模式》中关于选择器的实现思路,结合Spring的依赖注入,注入全局唯一的处理器,也可以看作是享元模式。

此外,有一道关于Integer的经典面试题,如下代码中,分别会打印出什么:

public static void main(String[] args) {
    
    Integer i = 100;
    Integer j = 100;
    System.out.println(i == j);
    Integer i1 = 300;
    Integer j1 = 300;
    System.out.println(i1 == j1);
}

分别打印出:

true
false

这是因为给Integer赋值的时候,会自动装箱,即Integer i = 100等价于Integer i = Integer.valueOf(100);在源码中:
在这里插入图片描述
这里有个IntegerCache,默认会将-128到127之间的值创建为Integer对象,放入到池中,使用这个区间内的值获取到的是同一个Integer对象,这也是享元模式的一种体现。

3.享元模式的一些对比

相信大家已经发现了,享元模式的实现方式与缓存、池化技术是高度类似的,那么它们之间有什么样的差别呢?

3.1.与缓存的区别

两者之间主要是使用目的上的区别,可以通过以下的判断方式做区分。

  • 享元模式的存在主要是为了复用对象、减少内存的消耗。
  • 缓存的主要目的是针对常用对象做更细粒度的存储,从而提高访问的效率,降低查询时间等。

3.2.与池化技术的区别

两者的使用目的似乎都是为了复用,是的,在一部分资料中确实是将享元模式与池化技术画等号的,但两者之间的复用还有一定的区别。

  • 享元模式的复用,是让服务中的不同对象,都可以同时使用到享元对象,是一种共享的概念。
  • 池化技术的复用,更多的是讲究重复使用,即在使用了一部分连接后,可以放回池中让其他对象可以获取到,而不是断开连接,让后面的对象重新做一次连接操作。

从上面的角度来讲,池化技术中的每一个重复使用的对象,同时只会让一个对象持有。例如下面这个简单的jdbc连接池Demo,在getConnection时会获取到连接并从池中移除,在release时又会将之前获取到的链接重新放回到池子中。

public class CollectionPool {
    

    private Vector<Connection> pool = new Vector<>();

    private String driverClassName = "com.mysql.cj.jdbc.Driver";
    private String url = "jdbc:mysql://localhost:3306/pattern";
    private String userName = "xxx";
    private String password = "xxx";

    private CollectionPool() {
    
        try {
    
            Class.forName(driverClassName);
            for (int i = 0; i < 2; i++) {
    
                Connection conn = DriverManager.getConnection(url, userName, password);
                pool.add(conn);
            }
        } catch (Exception e) {
    
            e.printStackTrace();
        }
    }

    public static CollectionPool getInstance() {
    
        return InnerClass.POOL;
    }

    public Connection getConnection() {
    
        if (pool.size() > 0) {
    
            Connection conn = pool.get(0);
            pool.remove(conn);
            return conn;
        }
        return null;
    }

    public synchronized void release(Connection conn) {
    
        pool.add(conn);
    }

    private static class InnerClass {
    
        private static CollectionPool POOL = new CollectionPool();
    }
}

4.总结

本文主要讲了享元模式的概念、使用场景以及与其他技术的对比。
在使用方式上,与缓存、池化技术是高度类似的,都是创建好对象并存储起来,在后续想要使用的时候直接从存储的数据结构中获取,而不用重新创建。
它与缓存、池化技术之间的区别,更多的是在于使用目的上的区别,只要能判断出,当前的对象是在通过共享对象的方式,减少系统中的重复对象,提高内存使用效率,就可以判断这是一个享元模式的实现。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_38249409/article/details/136364364

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签