写在前面
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
接口, 包含了fullAmount
和reductionAmount
作为内在状态。
享元工厂
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
组合优势
- 通过享元模式, 相同的策略对象可以被多个上下文复用, 避免了重复创建, 提供了代码的复用性。
- 享元模式解决了大量重复策略对象造成的内存消耗问题, 而策略模式则提供了灵活切换算法的能力。两者结合, 可以在管理多种算法的同时, 有效 奖励内存占用。
当前代码结构在技术上是合理的, 但从 "内存优化" 角度来看, 享元模式的引入存在 "过度设计", 因为它增加了代码的复杂性而带来的收益并不明显。