模式组合应用-享元模式
写在前面
Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!
享元模式
定义
结构型设计模式, 通过共享大量细粒度对象来最小化内存使用和计算开销。当系统中存在大量重复对象, 且这些对象的大部分状态可以外部化时, 享元模式通过共享这些对象的 内在状态 来减少对象的创建数量, 从而优化性能和资源利用。
基本结构
享元模式的核心思想是 分离内在状态与外在状态
。
内在状态: 可被共享的、不随环境改变而改变的属性, 这些属性是享元对象固有的, 并且在享元对象之间共享。
外在状态: 指不可被共享的、随环境改变而该百年的属性, 这些属性由客户端独立维护, 并在需要时传入享元对象。
使用时主要思考点
识别内在及外在状态, 需要仔细分析对象的属性, 区别哪些是可以被共享的, 哪些是不可被共享的。内在状态是享元对象的核心, 而外在状态则由客户端负责管理和传递。
由于享元模式是通过减少对象数量来节省内存, 并引入了享元工厂和状态分离的复杂性。在某些情况下, 如果共享的对象数量不多, 或者对象的内存占用很小, 引入享元模式将带来负面效果。
享元工厂负责管理和创建享元对象, 其内部应该维护一个享元对象的池, 并在客户端请求时提供享元对象。工厂实现通常需要考虑线程安全问题。
为保证享元对象内在状态能够被安全的共享, 享元对象的内在状态应该是不可变的。一旦享元对象被创建, 其内在状态就不应该再被修改。
享元模式+工厂方法模式
工厂方法模式
创建型设计模式, 定义了一个用于创建对象的接口, 但由子类决定实例化哪一个类, 使得类的实例化延迟到子类。
解决了直接实例化对象带来的高耦合问题, 使得系统在增加新的产品时, 无需修改客户端代码, 只需要增加新的具体产品类和对应的具体工厂类即可。
案例
在一个大型在线教育平台中, 存在大量的课程资源, 如视频、文档、测验等。为了提供用户体验, 平台需要为每种资源提供一个预览功能。不同类型的资源有不同的预览逻辑, 例如: 视频资源需要播放器预览, 文档资源需要文档阅读器预览, 测验资源需要交互式界面预览。同时, 当平台用户量变大时, 同一时间可能有大量用户预览各种资源, 如果每次预览都创建新的资源对象, 将导致巨大的内存开销。使用享元模式进行编码, 以解决上述问题。
模式职责
享元模式: 负责共享课程资源的内在状态(如资源类型、通用预览逻辑), 避免重复创建大量相同的资源对象
工厂方法模式: 根据不同的课程资源类型, 创建对应的具体享元对象, 将对象的创建与使用解耦。
代码结构
抽象享元角色
public interface CourseResource {void preview(String extrinsicState);String getType();}
定义了所有具体享元对象必须实现的接口。
preview
方法接收一个extrinsicState
参数, 用于标识外在状态。getType()
用于获取享元对象的内在状态(类型)。
具体享元角色
public class DocumentResource implements CourseResource {private final String type;public DocumentResource(String type) {this.type = type;System.out.println("创建了一个新的文档资源对象: " + type);}@Overridepublic void preview(String extrinsicState) {System.out.println("正在预览 " + type + " 文档, 页码: " + extrinsicState);}@Overridepublic String getType() {return type;}
}public class QuizResource implements CourseResource {private final String type;public QuizResource(String type) {this.type = type;System.out.println("创建了一个新的测验资源对象: " + type);}@Overridepublic void preview(String extrinsicState) {System.out.println("正在预览 " + type + " 测验, 题目: " + extrinsicState);}@Overridepublic String getType() {return type;}
}public class VideoResource implements CourseResource {private final String type;public VideoResource(String type) {this.type = type;System.out.println("创建了一个新的视频资源对象: " + type);}@Overridepublic void preview(String extrinsicState) {System.out.println("正在预览 " + type + " 视频, 进度: " + extrinsicState);}@Overridepublic String getType() {return type;}
}
实现了
CourseResource
接口, 并实现各自内在状态 和 预览逻辑。DocumentResource
(文档资源享元类)、QuizResource
(测验资源享元类)、VideoResource
(视频资源享元类)。
享元工厂角色
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
用于验证当前工厂中实际创建的享元对象数量。
抽象工厂角色
public interface ResourcePreviewerFactory {CourseResource createResource();}
定义了一个抽象的工厂方法
createResource()
, 方法返回一个CourseResource
类型的对象, 接口将对象的创建过程抽象化, 使得客户端无需关心具体对象的创建细节。
具体工厂角色
public class VideoPreviewerFactory implements ResourcePreviewerFactory {@Overridepublic CourseResource createResource() {return CourseResourceFactory.getResource("Video");}
}
public class QuizPreviewerFactory implements ResourcePreviewerFactory {@Overridepublic CourseResource createResource() {return CourseResourceFactory.getResource("Quiz");}
}
public class DocumentPreviewerFactory implements ResourcePreviewerFactory {@Overridepublic CourseResource createResource() {return CourseResourceFactory.getResource("Document");}
}
实现了
ResourcePreviewerFactory
接口, 每个具体工厂负责创建特定CourseResource
类型的对象, 通过享元工厂进行对象的创建(CourseResourceFactory.getResource()
)。
测试类
public class FlyweightTest {@Testpublic 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题");}}
运行结果
用户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”)会被大量用户同时使用。
模式职责
享元模式: 负责共享价格计算策略的内在状态(即策略本身), 减少策略对象的创建数量。
策略模式: 负责定义价格计算的算法族, 使得客户端可以灵活地选择和切换不同的价格计算策略。
代码结构
抽象享元/抽象策略
public interface PriceCalculationStrategy {double calculatePrice(double originalPrice);
}
定义了所有价格计算策略的公共接口
calculatePrice(double originalPrice)
, 所有具体策略必须实现, 用于计算最终价格。
抽象享元/抽象策略
public class DiscountStrategy implements PriceCalculationStrategy {// 内在状态 折扣率private double discountRate;public DiscountStrategy(double discountRate) {this.discountRate = discountRate;System.out.println("创建新的折扣策略享元: " + (discountRate * 100) + "%");}@Overridepublic 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);}@Overridepublic double calculatePrice(double originalPrice) {if (originalPrice >= fullAmount) {return originalPrice - reductionAmount;}return originalPrice;}
}
DiscountStrategy
打折价格计算策略, 实现了PriceCalculationStrategy
接口, 并包含了discountRate
作为内在状态, 相同 折扣率的DiscountStrategy
对象可以被共享, 构造函数中打印信息用于验证享元对象的创建。FullReductionStrategy
满减价格计算策略, 同样实现了PriceCalculationStrategy
接口, 包含了fullAmount
和reductionAmount
作为内在状态。
享元工厂
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
, 然后检查映射中是否存在, 若存在则返回; 否则创建新的策略对象, 并将其放入映射中。
策略上下文
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
方法委托当前设置的策略对象来计算最终价格。
测试类
public class PriceCalculationTest {@Testpublic 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());}}
运行结果
创建新的折扣策略享元: 10.0%
使用策略进行结算...
购物车1结算金额: 180.0创建新的满减策略享元: 满100.0 减 50.0
使用策略进行结算...
购物车2结算金额: 130.0使用策略进行结算...
购物车3结算金额: 250.0创建新的折扣策略享元: 20.0%
使用策略进行结算...
购物车4结算金额: 400.0Process finished with exit code 0
组合优势
通过享元模式, 相同的策略对象可以被多个上下文复用, 避免了重复创建, 提供了代码的复用性。
享元模式解决了大量重复策略对象造成的内存消耗问题, 而策略模式则提供了灵活切换算法的能力。两者结合, 可以在管理多种算法的同时, 有效 奖励内存占用。
当前代码结构在技术上是合理的, 但从 "内存优化" 角度来看, 享元模式的引入存在 “过度设计”, 因为它增加了代码的复杂性而带来的收益并不明显。