【设计模式】享元模式的使用场景及与其他共享技术的对比

慈云数据 1年前 (2024-03-15) 技术支持 84 0

文章目录

  • 1.概述
  • 2.享元模式
    • 2.1.核心概念
    • 2.2.实现案例
      • 2.2.1.内部状态实现
      • 2.2.2.外部状态实现
      • 2.3.更多场景
      • 3.享元模式的一些对比
        • 3.1.与缓存的区别
        • 3.2.与池化技术的区别
        • 4.总结

          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  其实,所谓的共享,就是用一个数据结构将已生成的对象缓存起来的,数据结构可以是数组,也可以是的Map,List等等,由于扑克牌的数量和花色、点数是恒定的,所以使用了一个二维数组存储并做了初始化,客户端可以通过点数+花色的方式来获取扑克对象。
                

                在这里插入图片描述

                2.2.2.外部状态实现

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

                • 玩家类
                  public class Player {
                      private String name;
                      public Player(String name) {
                          this.name = name;
                      }
                      private List 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 playerList) {
                          List pokers = PokerFactory.createPokers();
                          // 打乱牌堆
                          Collections.shuffle(pokers);
                          // 每人发3张牌
                          for (int i = 0; i  
                • 游戏服务
                  public class GameServer {
                      public static void main(String[] args) {
                          List 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 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  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.总结

                      本文主要讲了享元模式的概念、使用场景以及与其他技术的对比。

                      使用方式上,与缓存、池化技术是高度类似的,都是创建好对象并存储起来,在后续想要使用的时候直接从存储的数据结构中获取,而不用重新创建。

                      它与缓存、池化技术之间的区别,更多的是在于使用目的上的区别,只要能判断出,当前的对象是在通过共享对象的方式,减少系统中的重复对象,提高内存使用效率,就可以判断这是一个享元模式的实现。

微信扫一扫加客服

微信扫一扫加客服