设计模式之享元模式:看19路棋盘如何做到一子千面


~犬📰余~
"我欲贱而贵,愚而智,贫而富,可乎? 曰:其唯学乎"


一、享元模式概述

\quad 在软件设计中,享元模式(Flyweight Pattern)的核心思想是通过共享来有效地支持大量细粒度对象的重用。这里的"享"体现在共享,"元"则体现在这些可以共享的基本元素上。正如共享单车系统一样,享元模式会维护一个对象池,其中存储可以复用的对象,当需要时直接从池中获取,而不是重新创建。

\quad 上图展示了享元模式的基本架构,我们可以看到多个客户端都在复用对象池中的共享对象。这些共享对象具有两种状态:

  • 内部状态:对象可共享的、固定不变的属性,就像自行车的基本构造。
  • 外部状态:对象不可共享的、随环境改变的属性,就像自行车的位置和使用状态。

\quad 这种模式特别适合处理需要创建大量相似对象的场景。通过识别对象的内部状态和外部状态,将可共享的部分集中管理,不仅可以显著减少内存占用,还能提升系统性能。

二、享元模式分类

\quad 在享元模式中,根据对象是否可以共享,我们可以将享元分为两种类型:共享享元和非共享享元,就像上图所展示的那样。

  • 共享享元是享元模式的核心,它代表那些可以被多个环境共享使用的对象。这类对象的特点是它们的内部状态是一致的,不会因为使用环境的改变而改变。就像围棋中的黑白棋子,每个黑棋的颜色和形状都是完全相同的,我们没必要为每个位置都创建新的棋子对象,而是可以共享使用现有的棋子,只需要改变它们的位置信息即可。
  • 非共享享元则是那些不能被共享的对象。这类对象可能具有特定的、不可共享的状态。虽然它们不共享,但仍然可以通过享元工厂来统一管理。比如在文字编辑器中,每个字符的字体样式可能都不相同,这时就需要使用非共享享元来处理这些特殊情况。

\quad 共享享元和非共享享元经常一起使用,它们各自处理不同的业务场景。共享享元主要用于那些需要大量创建相似对象的场景,通过共享来减少内存占用;而非共享享元则用于处理那些虽然结构相似,但状态必须独立的对象。

三、享元模式角色组成


\quad 如上图所示,享元模式主要由四个核心角色组成,它们共同协作来实现对象的高效共享和管理。

  • Flyweight(享元接口)是所有具体享元类的公共接口,它定义了享元对象需要实现的方法。这个接口通常包含一个传入外部状态的操作方法,使享元对象能够根据外部状态改变其行为。
  • ConcreteFlyweight(具体享元类)是实现了Flyweight接口的具体类。它包含内部状态,也就是那些可以共享的、不会随环境改变的信息。例如,在围棋程序中,棋子的颜色就是内部状态。这个类的实例会被多个客户端共享使用。
  • UnsharedConcreteFlyweight(非共享具体享元类)也实现了Flyweight接口,但它的实例不会被共享。这个类包含了不能共享的状态信息。比如在文本编辑器中,虽然字符'A'可以被共享,但如果这个'A'有特殊的样式,就需要使用非共享享元来处理。
  • FlyweightFactory(享元工厂)负责创建和管理享元对象。它通常维护一个享元池(用Map实现),用于存储已创建的享元对象。当客户端请求一个享元对象时,工厂会先检查池中是否存在满足要求的对象,如果存在就直接返回,否则才创建新的对象。这保证了相同内部状态的享元对象只会被创建一次。

四、享元模式案例

\quad 让我们通过实现一个简单的围棋程序来深入理解享元模式。在围棋中,棋子只有黑白两色,但要放置在棋盘的不同位置上。这里棋子的颜色就是内部状态,可以被共享;而位置则是外部状态,需要在使用时指定。

图片
\quad 首先定义棋子的位置类:

java 复制代码
// 棋子位置类-外部状态
public class Position {
    private int x;
    private int y;
    
    public Position(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() { return x; }
    public int getY() { return y; }
}

\quad 接下来定义围棋的棋子共享接口以及实现类:

java 复制代码
// 围棋棋子接口
public interface GoChessPiece {
    void display(Position position);
}
// 具体的围棋棋子类
public class ConcreteChessPiece implements GoChessPiece {
    private String color; // 内部状态
    
    public ConcreteChessPiece(String color) {
        this.color = color;
    }
    
    @Override
    public void display(Position position) {
        System.out.printf("棋子颜色:%s,位置:(%d, %d)%n", 
            color, position.getX(), position.getY());
    }
}

\quad 然后是工厂类:

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class GoChessPieceFactory {
    private static final Map<String, GoChessPiece> pieces = new HashMap<>();
    
    public static GoChessPiece getChessPiece(String color) {
        GoChessPiece piece = pieces.get(color);
        if (piece == null) {
            piece = new ConcreteChessPiece(color);
            pieces.put(color, piece);
        }
        return piece;
    }
}

\quad 使用示例:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 获取白色棋子并放在(2, 3)位置
        GoChessPiece white1 = GoChessPieceFactory.getChessPiece("白色");
        white1.display(new Position(2, 3));

        // 获取另一个白色棋子放在(3, 6)位置
        GoChessPiece white2 = GoChessPieceFactory.getChessPiece("白色");
        white2.display(new Position(3, 6));

        // 判断是否是同一个对象
        System.out.println("两个白棋是否共享同一个对象:" + (white1 == white2));
    }
}

\quad 测试结果:

\quad 在这个案例中,我们可以看到:

  1. 棋子的颜色(内部状态)被共享,每种颜色的棋子只会创建一个对象

  2. 棋子的位置(外部状态)在使用时由客户端指定

五、享元模式优缺点

优点

\quad 享元模式的核心优势是通过共享对象来减少内存占用,特别适合需要创建大量相似对象的场景。使用享元模式可以集中管理可复用的对象,使得对象的创建和维护更加规范和高效。

缺点

\quad 这种模式增加了系统的复杂度,需要额外的工厂类来管理对象池,同时还需要仔细区分内部状态和外部状态。在对象数量较少的场景下,这种模式带来的收益可能无法抵消其带来的开发成本。

六、享元模式适用场景

\quad 享元模式最适合应用在系统需要创建大量相似对象,且这些对象可以分离出共享部分的场景。典型的应用场景包括:

  • 文字编辑器中的字符渲染:相同的字符可以共享字形信息,只需要改变位置和样式
  • 游戏中的素材管理:相同的游戏素材(如树木、建筑)可以在不同位置重复使用
  • 地图应用中的图标:相同类型的地标可以共享图标资源
  • 网页中的图片缓存:相同的图片可以在多处被重复使用

\quad 当系统中存在大量重复对象,且这些对象的大部分状态都可以外部化时,使用享元模式可以显著降低内存占用并提高性能。

七、总结

\quad 享元模式通过对象共享来提高系统性能,是一种以时间换空间的设计模式。它将对象的状态分为内部状态和外部状态,通过共享内部状态来减少对象创建。在实现时,需要通过享元工厂来统一管理对象池,确保相同内部状态的对象只被创建一次。这种模式特别适合需要大量创建相似对象的场景,但在使用时需要权衡其带来的复杂性和收益。


关注犬余,共同进步
技术从此不孤单

相关推荐
BillKu35 分钟前
Java + Spring Boot + Mybatis 插入数据后,获取自增 id 的方法
java·tomcat·mybatis
全栈凯哥36 分钟前
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
java·算法·leetcode·链表
chxii37 分钟前
12.7Swing控件6 JList
java
全栈凯哥38 分钟前
Java详解LeetCode 热题 100(27):LeetCode 21. 合并两个有序链表(Merge Two Sorted Lists)详解
java·算法·leetcode·链表
YuTaoShao39 分钟前
Java八股文——集合「List篇」
java·开发语言·list
PypYCCcccCc44 分钟前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐1 小时前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦1 小时前
ThreadLocal 线程变量
java·后端
BillKu2 小时前
Java后端检查空条件查询
java·开发语言
jackson凌2 小时前
【Java学习笔记】String类(重点)
java·笔记·学习