模式组合应用-享元模式

写在前面

Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!


享元模式

定义

结构型设计模式, 通过共享大量细粒度对象来最小化内存使用和计算开销。当系统中存在大量重复对象, 且这些对象的大部分状态可以外部化时, 享元模式通过共享这些对象的 内在状态 来减少对象的创建数量, 从而优化性能和资源利用。

基本结构

享元模式的核心思想是 分离内在状态与外在状态

  • 内在状态: 可被共享的、不随环境改变而改变的属性, 这些属性是享元对象固有的, 并且在享元对象之间共享。
  • 外在状态: 指不可被共享的、随环境改变而该百年的属性, 这些属性由客户端独立维护, 并在需要时传入享元对象。
使用时主要思考点
  • 识别内在及外在状态, 需要仔细分析对象的属性, 区别哪些是可以被共享的, 哪些是不可被共享的。内在状态是享元对象的核心, 而外在状态则由客户端负责管理和传递。
  • 由于享元模式是通过减少对象数量来节省内存, 并引入了享元工厂和状态分离的复杂性。在某些情况下, 如果共享的对象数量不多, 或者对象的内存占用很小, 引入享元模式将带来负面效果。
  • 享元工厂负责管理和创建享元对象, 其内部应该维护一个享元对象的池, 并在客户端请求时提供享元对象。工厂实现通常需要考虑线程安全问题。
  • 为保证享元对象内在状态能够被安全的共享, 享元对象的内在状态应该是不可变的。一旦享元对象被创建, 其内在状态就不应该再被修改。

享元模式+工厂方法模式

工厂方法模式

创建型设计模式, 定义了一个用于创建对象的接口, 但由子类决定实例化哪一个类, 使得类的实例化延迟到子类。

解决了直接实例化对象带来的高耦合问题, 使得系统在增加新的产品时, 无需修改客户端代码, 只需要增加新的具体产品类和对应的具体工厂类即可。

案例

在一个大型在线教育平台中, 存在大量的课程资源, 如视频、文档、测验等。为了提供用户体验, 平台需要为每种资源提供一个预览功能。不同类型的资源有不同的预览逻辑, 例如: 视频资源需要播放器预览, 文档资源需要文档阅读器预览, 测验资源需要交互式界面预览。同时, 当平台用户量变大时, 同一时间可能有大量用户预览各种资源, 如果每次预览都创建新的资源对象, 将导致巨大的内存开销。使用享元模式进行编码, 以解决上述问题。

模式职责

  • 享元模式: 负责共享课程资源的内在状态(如资源类型、通用预览逻辑), 避免重复创建大量相同的资源对象
  • 工厂方法模式: 根据不同的课程资源类型, 创建对应的具体享元对象, 将对象的创建与使用解耦。

代码结构

抽象享元角色
arduino 复制代码
public interface CourseResource {

    void preview(String extrinsicState);

    String getType();

}
  • 定义了所有具体享元对象必须实现的接口。
  • preview 方法接收一个 extrinsicState 参数, 用于标识外在状态。
  • getType() 用于获取享元对象的内在状态(类型)。
具体享元角色
typescript 复制代码
public class DocumentResource implements CourseResource {

    private final String type;

    public DocumentResource(String type) {
        this.type = type;
        System.out.println("创建了一个新的文档资源对象: " + type);
    }

    @Override
    public void preview(String extrinsicState) {
        System.out.println("正在预览 " + type + " 文档, 页码: " + extrinsicState);
    }

    @Override
    public String getType() {
        return type;
    }
}

public class QuizResource implements CourseResource {

    private final String type;

    public QuizResource(String type) {
        this.type = type;
        System.out.println("创建了一个新的测验资源对象: " + type);
    }

    @Override
    public void preview(String extrinsicState) {
        System.out.println("正在预览 " + type + " 测验, 题目: " + extrinsicState);
    }

    @Override
    public String getType() {
        return type;
    }
}

public class VideoResource implements CourseResource {

    private final String type;

    public VideoResource(String type) {
        this.type = type;
        System.out.println("创建了一个新的视频资源对象: " + type);
    }

    @Override
    public void preview(String extrinsicState) {
        System.out.println("正在预览 " + type + " 视频, 进度: " + extrinsicState);
    }

    @Override
    public String getType() {
        return type;
    }
}
  • 实现了 CourseResource 接口, 并实现各自内在状态 和 预览逻辑。
  • DocumentResource(文档资源享元类)、QuizResource(测验资源享元类)、VideoResource(视频资源享元类)。
享元工厂角色
typescript 复制代码
public class CourseResourceFactory {

    private static final Map<String, CourseResource> resourceMap = new HashMap<>();

    public static CourseResource getResource(String type) {

        CourseResource resource = resourceMap.get(type);
        if (resource == null) {
            switch (type) {
                case "Video":
                    resource = new VideoResource(type);
                    break;
                case "Document":
                    resource = new DocumentResource(type);
                    break;
                case "Quiz":
                    resource = new QuizResource(type);
                    break;
                default:
                    throw new IllegalArgumentException("未知资源类型: " + type);
            }
            resourceMap.put(type, resource);
        }

        return resource;
    }

    public static int getResourceCount() {
        return resourceMap.size();
    }

}
  • getResource 方法负责管理和提供享元对象(CourseResource), 若存在则返回, 若不存在则创建一个新的具体享元对象, 并将其放入 resourceMap 中。
  • getResourceCount 用于验证当前工厂中实际创建的享元对象数量。

抽象工厂角色
csharp 复制代码
public interface ResourcePreviewerFactory {

    CourseResource createResource();

}
  • 定义了一个抽象的工厂方法 createResource(), 方法返回一个 CourseResource 类型的对象, 接口将对象的创建过程抽象化, 使得客户端无需关心具体对象的创建细节。
具体工厂角色
typescript 复制代码
public class VideoPreviewerFactory implements ResourcePreviewerFactory {

    @Override
    public CourseResource createResource() {
        return CourseResourceFactory.getResource("Video");
    }
}
public class QuizPreviewerFactory implements ResourcePreviewerFactory {

    @Override
    public CourseResource createResource() {
        return CourseResourceFactory.getResource("Quiz");
    }
}
public class DocumentPreviewerFactory implements ResourcePreviewerFactory {

    @Override
    public CourseResource createResource() {
        return CourseResourceFactory.getResource("Document");
    }
}
  • 实现了 ResourcePreviewerFactory 接口, 每个具体工厂负责创建特定 CourseResource 类型的对象, 通过享元工厂进行对象的创建( CourseResourceFactory.getResource() )。

测试类
ini 复制代码
public class FlyweightTest {

    @Test
    public void test_courseResource() {

        ResourcePreviewerFactory videoFactory = new VideoPreviewerFactory();

        ResourcePreviewerFactory documentFactory = new DocumentPreviewerFactory();

        ResourcePreviewerFactory quizFactory = new QuizPreviewerFactory();

        System.out.println("\n用户A预览视频: ");
        CourseResource video1 = videoFactory.createResource();
        video1.preview("10%");

        System.out.println("\n用户B预览视频: ");
        CourseResource video2 = videoFactory.createResource();
        video2.preview("50%");

        System.out.println("\n用户C预览文档: ");
        CourseResource document1 = documentFactory.createResource();
        document1.preview("第3页");

        System.out.println("\n用户D预览文档: ");
        CourseResource document2 = documentFactory.createResource();
        document2.preview("第10页");

        System.out.println("\n用户E预览测验: ");
        CourseResource quiz1 = quizFactory.createResource();
        quiz1.preview("第1题");

        System.out.println("\n用户F预览测验");
        CourseResource quiz2 = quizFactory.createResource();
        quiz2.preview("第5题");

    }

}
运行结果
makefile 复制代码
用户A预览视频: 
创建了一个新的视频资源对象: Video
正在预览 Video 视频, 进度: 10%

用户B预览视频: 
正在预览 Video 视频, 进度: 50%

用户C预览文档: 
创建了一个新的文档资源对象: Document
正在预览 Document 文档, 页码: 第3页

用户D预览文档: 
正在预览 Document 文档, 页码: 第10页

用户E预览测验: 
创建了一个新的测验资源对象: Quiz
正在预览 Quiz 测验, 题目: 第1题

用户F预览测验
正在预览 Quiz 测验, 题目: 第5题

Process finished with exit code 0

组合优势

  • 享元模式专注于对象的共享和内存优化, 而工厂方法模式专注于对象的创建过程, 各司其职, 易于理解和维护。
  • 工厂模式将对象的创建逻辑集中到 CourseResourceFactory 中, 使得客户端无需关心具体享元对象的创建细节。同时也负责享元对象的复用, 确保了享元模式的有效实施。

享元模式+策略模式

策略模式(行为型)

定义了一系列算法, 并将每个算法封装起来, 使得他们可以相互替换, 策略模式使得算法可以独立于使用它的客户端而变化。

案例

假设我们正在开发一个大型电商平台的订单系统。该平台每天会进行大量的促销活动,包括各种折扣、满减、秒杀等。用户在购物时,购物车中的商品总价需要根据当前生效的促销策略进行计算。由于平台用户量巨大,同时进行的订单和购物车数量也非常庞大,而且许多促销策略(例如"全场9折"、"满200减20")会被大量用户同时使用。

模式职责

  • 享元模式: 负责共享价格计算策略的内在状态(即策略本身), 减少策略对象的创建数量。
  • 策略模式: 负责定义价格计算的算法族, 使得客户端可以灵活地选择和切换不同的价格计算策略。

代码结构

抽象享元/抽象策略
csharp 复制代码
public interface PriceCalculationStrategy {

    double calculatePrice(double originalPrice);
}
  • 定义了所有价格计算策略的公共接口 calculatePrice(double originalPrice), 所有具体策略必须实现, 用于计算最终价格。
抽象享元/抽象策略
java 复制代码
public class DiscountStrategy implements PriceCalculationStrategy {

    // 内在状态 折扣率
    private double discountRate;

    public DiscountStrategy(double discountRate) {
        this.discountRate = discountRate;
        System.out.println("创建新的折扣策略享元: " + (discountRate * 100) + "%");
    }

    @Override
    public double calculatePrice(double originalPrice) {
        return originalPrice * (1 - discountRate);
    }
}
public class FullReductionStrategy implements PriceCalculationStrategy {

    private double fullAmount;

    private double reductionAmount;

    public FullReductionStrategy(double fullAmount, double reductionAmount) {
        this.fullAmount = fullAmount;
        this.reductionAmount = reductionAmount;
        System.out.println("创建新的满减策略享元: 满" + fullAmount + " 减 " + reductionAmount);
    }

    @Override
    public double calculatePrice(double originalPrice) {

        if (originalPrice >= fullAmount) {
            return originalPrice - reductionAmount;
        }

        return originalPrice;
    }
}
  • DiscountStrategy 打折价格计算策略, 实现了 PriceCalculationStrategy 接口, 并包含了 discountRate 作为内在状态, 相同 折扣率的 DiscountStrategy 对象可以被共享, 构造函数中打印信息用于验证享元对象的创建。
  • FullReductionStrategy 满减价格计算策略, 同样实现了 PriceCalculationStrategy 接口, 包含了 fullAmountreductionAmount 作为内在状态。
享元工厂
typescript 复制代码
public class StrategyFactory {

    private static final Map<String, PriceCalculationStrategy> strategies = new HashMap<>();

    public static PriceCalculationStrategy getStrategy(String type, double... params) {

        String key = type + "-" + Arrays.toString(params);

        PriceCalculationStrategy strategy = strategies.get(key);

        if (strategy == null) {
            switch (type) {
                case "Discount":
                    strategy = new DiscountStrategy(params[0]);
                    break;
                case "FullReduction":
                    strategy = new FullReductionStrategy(params[0], params[1]);
                    break;
                default:
                    throw new IllegalArgumentException("未知策略类型: " + type);
            }

            strategies.put(key, strategy);
        }

        return strategy;

    }

}
  • 维护一个 Map 来存储已经创建的策略享元对象。
  • getStrategy 方法, 根据策略类型和参数生成一个唯一 key, 然后检查映射中是否存在, 若存在则返回; 否则创建新的策略对象, 并将其放入映射中。
策略上下文
csharp 复制代码
public class ShoppingCart {

    private PriceCalculationStrategy strategy;

    private double totalAmount;

    public ShoppingCart(double totalAmount) {
        this.totalAmount = totalAmount;
    }

    public void setStrategy(PriceCalculationStrategy strategy) {
        this.strategy = strategy;
    }

    public double checkout() {
        if (strategy == null) {
            System.out.println("未设置价格计算策略, 按原价结算。");
            return totalAmount;
        }

        System.out.println("使用策略进行结算...");
        return strategy.calculatePrice(totalAmount);
    }

}
  • 持有 PriceCalculationStrategy 的引用, 并包含 totalAmount 作为外在状态, 并负责调用其 calculatePrice 方法。
  • setStrategy 方法允许客户端动态地设置或切换价格计算策略。
  • checkout 方法委托当前设置的策略对象来计算最终价格。
测试类
csharp 复制代码
public class PriceCalculationTest {

    @Test
    public void test_priceCalculation() {

        ShoppingCart cart1 = new ShoppingCart(200.0);
        cart1.setStrategy(StrategyFactory.getStrategy("Discount", 0.1));
        System.out.println("购物车1结算金额: " + cart1.checkout() + "\n");

        ShoppingCart cart2 = new ShoppingCart(180.0);
        cart2.setStrategy(StrategyFactory.getStrategy("FullReduction", 100.0, 50.0));
        System.out.println("购物车2结算金额: " + cart2.checkout() + "\n");

        ShoppingCart cart3 = new ShoppingCart(300.0);
        cart3.setStrategy(StrategyFactory.getStrategy("FullReduction", 100.0, 50.0));
        System.out.println("购物车3结算金额: " + cart3.checkout() + "\n");

        ShoppingCart cart4 = new ShoppingCart(500.0);
        cart4.setStrategy(StrategyFactory.getStrategy("Discount", 0.2));
        System.out.println("购物车4结算金额: " + cart4.checkout());

    }

}
运行结果
makefile 复制代码
创建新的折扣策略享元: 10.0%
使用策略进行结算...
购物车1结算金额: 180.0

创建新的满减策略享元: 满100.0 减 50.0
使用策略进行结算...
购物车2结算金额: 130.0

使用策略进行结算...
购物车3结算金额: 250.0

创建新的折扣策略享元: 20.0%
使用策略进行结算...
购物车4结算金额: 400.0

Process finished with exit code 0

组合优势

  • 通过享元模式, 相同的策略对象可以被多个上下文复用, 避免了重复创建, 提供了代码的复用性。
  • 享元模式解决了大量重复策略对象造成的内存消耗问题, 而策略模式则提供了灵活切换算法的能力。两者结合, 可以在管理多种算法的同时, 有效 奖励内存占用。

当前代码结构在技术上是合理的, 但从 "内存优化" 角度来看, 享元模式的引入存在 "过度设计", 因为它增加了代码的复杂性而带来的收益并不明显。

相关推荐
对象存储与RustFS2 小时前
零基础小白手把手教程:用Docker和MinIO打造专属私有图床,并完美搭配PicGo
后端
德育处主任2 小时前
文字识别:辛辛苦苦练模型,不如调用PP-OCRv5
后端·图像识别
TeamDev2 小时前
用一个 prompt 搭建带 React 界面的 Java 桌面应用
java·前端·后端
知其然亦知其所以然2 小时前
国产大模型也能无缝接入!Spring AI + 智谱 AI 实战指南
java·后端·算法
悟空码字2 小时前
阿里通义开源啦,源码地址+部署脚本,让AI会“做研究”
后端·阿里巴巴
GalaxyMeteor2 小时前
nodejs (express / koa)项目用ghooks + validate-commit-msg 实现 git提交时校验eslint+提交消息规范验证
后端
生无谓3 小时前
@AutoConfiguration和@Configuration区别
后端
IT_陈寒4 小时前
SpringBoot 3.2新特性实战:这5个隐藏技巧让你的启动速度提升50%
前端·人工智能·后端
阿杆5 小时前
国产神级开源 OCR 模型,GitHub 55k Star!再次起飞!
后端·github·图像识别