Java编程之享元模式

引言

本篇将以一个例子来带你了解享元模式。
假设你开了一家面馆,面条的种类(如拉面、刀削面)就像是享元模式中的内部状态,这些是面的基底,可以提前准备好并共享。而每碗面不同的配料(如牛肉、鸡蛋、蔬菜)则像是外部状态,这些是每次顾客点单时可能不同的部分。

享元的核心意图

  • 减少大量相似对象的创建,节约内存占用

  • 把对象状态分为:

    • 内部状态(Intrinsic):可共享、不可变。

    • 外部状态(Extrinsic):因场景不同而变化,不共享。

  • 通过共享内在状态,只要外部状态不同,就组合成一个完整对象,兼顾节省和多样。

该模式组成角色

  1. Flyweight(享元接口/抽象类)
    定义接受外部状态的方法,如 operation(extrinsicState)
    无论是拉面还是刀削面,都需要实现一个“做面”的操作。操作是享元接口,定义制作面条的基本步骤,比如煮面、加配料等。
  2. ConcreteFlyweight(具体享元类)
    保存内部状态,如拉面和刀削面就像是具体享元类。
    它们各自有固定的面条种类(内部状态),但都实现了“做面”的操作。当顾客点单时,面馆会根据选择的面条种类,使用相应的具体享元类来制作面条。
  3. UnsharedFlyweight(非享元类,可选)
    代表那些不允许共享的状态,通常作为客户端独立处理。
    可乐不属于正常面馆应该常备的食材,根据不同的顾客要求来做一定的定制化进货,可乐就是非享元类。
  4. FlyweightFactory(享元工厂)
    利用缓存(通常是哈希结构),管理创建与共享逻辑,客户端通过 factory 获取享元对象。
    面馆厨房就像是享元工厂,负责准备和提供各种面条种类。当顾客点单时,厨房会根据订单中的面条种类,直接提供已经准备好的面条基底。
  5. Client(客户端)
    维护和传递外部状态,调用 factory 获取享元,再执行操作。
    顾客点单时,会选择面条种类和配料。面馆会根据顾客的选择,使用厨房提供的面条基底(享元对象),并加上顾客指定的配料(外部状态),来完成一碗面的制作。

模式结构图

在这里插入图片描述

如上图所示:

  • Client 依赖 FlyweightFactory

  • Factory 缓存并复用 ConcreteFlyweight

  • ClientextrinsicState 传入享元实例,并调用 operation()

核心流程代码实例

1. 识别状态

  • 内部状态:面条基底(如拉面、刀削面)。

  • 外部状态:顾客为面条选择的配料(如牛肉、鸡蛋、蔬菜、豆腐、海鲜等)。

2. 创建享元接口


interface Flyweight {
    void operation(Map<String, String> extrinsicData);
}

3. 享元对象实现

// 具体享元实现(拉面)
class LaMian implements Flyweight {
    private String intrinsicState = "拉面基底"; // 内部状态:拉面基底
 
    @Override
    public void operation(Map<String, String> extrinsicData) {
        // 操作:制作拉面,加入外部状态(配料)
        System.out.println("使用" + intrinsicState + ",加入" + extrinsicData.get("配料") + ",开始制作拉面。");
    }
}
// 具体享元实现(刀削面)
class DaoXiaoMian implements Flyweight {
    private String intrinsicState = "刀削面基底"; // 内部状态:刀削面基底
 
    @Override
    public void operation(Map<String, String> extrinsicData) {
        // 操作:制作刀削面,加入外部状态(配料)
        System.out.println("使用" + intrinsicState + ",加入" + extrinsicData.get("配料") + ",开始制作刀削面。");
    }
}
//LaMian和DaoXiaoMian类作为具体享元类,它们包含了面条基底作为内部状态(intrinsicState)。
//这些内部状态在享元对象创建时被初始化,并在后续的操作中共享。不同的顾客订单可以共享相同的面条基底对象,只要他们选择的面条种类相同。
// 非享元类(可乐)
class UnsharedFlyweight {
    public void unsharedOperation(String drink) {
        // 处理非享元类的操作,例如检查饮料是否可用
        if (drink.equals("可乐") && !isAvailable()) {
            System.out.println("抱歉,可乐已售罄。");
        } else {
            System.out.println("享受您的" + drink + "。");
        }
    }
 
    private boolean isAvailable() {
        // 模拟检查饮料是否可用的逻辑
        // 这里可以添加实际的库存检查逻辑
        return false; // 假设可乐已售罄
    }
}

4. 享元工厂

class FlyweightFactory {
    private Map<String, Flyweight> flyweights = new HashMap<>(); // 存储享元对象的字典
 
    public Flyweight get(String key) {
        // 根据键(面条种类)获取或创建享元对象
        if (!flyweights.containsKey(key)) {
            if (key.equals("拉面")) {
                flyweights.put(key, new LaMian());
            } else if (key.equals("刀削面")) {
                flyweights.put(key, new DaoXiaoMian());
            } else {
                throw new IllegalArgumentException("未知的面条种类");
            }
        }
        return flyweights.get(key);
    }
}

当flyweights.containsKey()方法发现请求的面条种类不存在于映射中时,它会创建一个新的享元对象(如LaMian或DaoXiaoMian的实例),并将其添加到映射中。
这个过程确保了相同类型的面条基底对象只被创建一次,后续的请求将共享这个已存在的对象。

5. 客户端使用

class Client {
    private FlyweightFactory factory = new FlyweightFactory(); // 创建享元工厂实例
    private UnsharedFlyweight unsharedFlyweight = new UnsharedFlyweight(); // 创建非享元类实例
 
    public void orderNoodles(String noodleType, String toppings) {
        // 顾客点单面条,根据面条种类和配料制作面条
        Flyweight noodle = factory.get(noodleType); // 从工厂获取享元对象
        Map<String, String> extrinsicData = new HashMap<>();
        extrinsicData.put("配料", toppings);
        noodle.operation(extrinsicData); // 调用享元对象的操作,传入外部状态(配料)
    }
 
    public void orderDrink(String drink) {
        // 顾客点单饮料,使用非享元类处理
        unsharedFlyweight.unsharedOperation(drink);
    }
}

6. 运行结果

public class Main {
    public static void main(String[] args) {
        Client client = new Client();
        client.orderNoodles("拉面", "牛肉, 鸡蛋"); // 顾客1点拉面,加牛肉和鸡蛋
        client.orderNoodles("刀削面", "蔬菜, 豆腐"); // 顾客2点刀削面,加蔬菜和豆腐
        client.orderNoodles("拉面", "海鲜"); // 顾客3点拉面,加海鲜
        client.orderDrink("可乐"); // 顾客4点可乐
    }
}

在这里插入图片描述
通过享元工厂的管理和享元对象的共享,代码示例展示了享元模式如何有效地减少内存消耗。相同类型的面条基底对象只被创建一次,避免了重复创建和存储相同对象的内存开销。同时,通过共享享元对象,代码也提高了性能,因为创建和初始化对象的操作被减少,从而加快了订单处理速度。
此即典型缓存 + 不变享元的高级实现 。

7. UML图

在这里插入图片描述

适用场景

  • 对象数量巨大,但它们有一部分状态完全相同。
    图形应用中大量相同纹理模型;
    文本编辑器中的字符渲染;
  • 内外状态可以明确区分,且内部状态支持共享。
    缓存模式(如数据库连接池、对象池
  • 重复创建对象非常耗内存或影响性能 。
    Java 中的 String 常量池机制。

优缺点分析

优点

  • 大幅减少重复对象、节省内存;

  • 提高系统性能,对小而多重复对象尤其有效;

  • 强制实现对象状态不可变,安全性提升。

缺点

  • 增加设计复杂度,需拆分状态;

  • 客户端必须管理外部状态,使用更繁;

  • 如果外部状态多且多变,分享性下降,复杂度增加。

进阶要点

  1. 状态不可变性:保证享元对象共享安全,如建成 final,无 setter。

  2. 缓存机制优化:可使用弱引用(如 WeakHashMap)自动回收闲置享元。

小结

享元模式通过 将相同的部分抽出共享,保留变化的外部状态,有效减少大规模对象创建带来的内存和性能问题。

它本质上是一种结构优化模式,适用于大量相同对象场景。设计时要权衡复杂度与性能收益,合理拆分状态和部署工厂模式。

参考

《23种设计模式概览》
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

勤奋的知更鸟

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值