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

技术标签: 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

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签