大家知道我最近结合AI+DDD
开发了一个包子铺系统,谁说写业务代码就一定是CURD?再简单的项目也可以用设计模式,这就来盘点一下我在包子铺项目中用到的设计模式。
事先声明:我并没有为了炫技而引入一些复杂的设计,是因为项目中确实有必要用到。
关于包子铺系统的源码 ,如果你感兴趣,可以戳我查看源码获取方式
设计模式的类型
先回顾一下八股文,设计模式一共23种,分为三大类:
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
设计模式到底有没有用?我们来结合案例说一说。
工厂模式 Factory
设计模式中与工厂相关的其实有三种:简单工厂 Simple Factory 、工厂方法 Factory Method 、抽象工厂 Abstract Factory。
这三种模式的复杂度依次递增,如果你实在分不清,就这么记:
- 简单工厂就是每个对象都有自己的工厂

- 工厂方法就是要生产一类产品,且这类产品存在比较大共性

- 抽象工厂就是要创建很多类产品,并且这些产品构成了一个产品族
图我就不画了,老实说抽象工厂是我认为在设计模式中为数不多用处不大的模式,因为这里面有过度设计之嫌,应用在实际业务中有些太臃肿了。
在包子铺项目的应用
包子铺项目中绝大多数用的是简单工厂,只有交易订单的创建用到了工厂方法,因为包子铺有三类订单:直营渠道订单、外卖渠道订单、线下门店订单,刚好形成了一类"产品",这就比较适合用工厂方法:

工厂模式有什么用
要知道对象创建的过程也是包含逻辑的,工厂的意义就是你只需要提供原材料,工厂负责生产出商品。
举个例子,在创建订单的时候,下单时间、用户ID、用户购买的商品类型和数量,这些都是明确知道的,也就是原材料,而订单的总金额、订单是否涉及配送则需要计算得出。把对象的完整创建过程封装在工厂内,以保证对象创建的内聚性。
建造者模式 Builder
这个模式有用过Lombok的应该不陌生,Builder模式的好处是把复杂对象的创建分解成多个子步骤,这种情况下就很适合Builder。
Builder的另一个好处是代码编写上可以用链式调用,语法简洁清晰。
为什么有工厂模式还要用Builder
确实这二者都是构造型模式,选其一就可以了。
严格来说,包子铺只用到了Factory,至于Builder只是用到了Lombok提供的语法糖。通常领域模型是充血模型 Rich Model,因此不会提供Setter方法。从用户请求进来转化成模型我会用Factory模式,但是从物理数据模型转成领域模型更多还是会用Builder。
java
// 用Lombok的两个注解
@Getter
@Builer
public class SomeEntity {
private ID id;
private Money transAmount;
//...
}
单例模式 Singleton
单例模式的作用是保证对象的全局唯一性 ,主要用在 领域服务 Domain Service 、 应用服务App Service 和一些全局配置上。,
默认情况下,Spring框架的@Controller
、@Service
、@Repository
、@Component
几个注解声明的Bean都是单例的。这里就不再做过多的说明。
装饰器 Decorator
Decorator这个词也有粉刷匠的意思,试想一下一个粉刷匠,用各色的油漆在你家的房子上装点,刷了一层又一层,房子还是那个房子(功能不变),但是外观变漂亮了。
在设计模式中,装饰器主要用于在一个组件上叠加新的能力,同时保证组件的基本能力不变。

我们可以在不影响原有组件能力的基础上为其增加新能力,这个模式真的是太棒了!
在包子铺项目的应用
每个Entity都有一个ID,为了统一,所有的ID都需要实现Identifier
,但ID生成其实是存在共性的,如果是你怎么处理这些共性?
很多同学大概会把共性"抽出来",写一个类似IDUtil的工具类,这种是面向过程 的思路,而我使用了面向对象的解法。

ID其实有很多种类型:
- 基于日期的流水型数据,适合用
DateBasedID
- 数据量不大的配置型数据,适合用
AutoIncreasedID
我这里用的是装饰器模式的变体,OrderID、ProductID、ConfigID都是装饰器,通过包装DateBasedID或AutoIncreasedID就可以轻松扩展其能力,并且代码的层次结构得以清晰地展现。
观察者 Observer
观察者模式常用于不同组件、模块间的解耦,A模块本需要调用B模块,但B模块的职责并没有那么重要,A不希望强依赖B,那么A就会通过发布一个领域事件,由B来订阅消费,这时候B就成了一个观察者。
包子铺中用到的观察者模式:

这一套机制可以很好地把事件的发布和订阅解耦,并且预留了足够的扩展性,事件分发默认基于数据库来实现,当然将来也可以增加其他的实现机制(例如基于MQ来实现)
关于领域事件的用法,很多人其实搞不明白,滥用事件的后果就是事件满天飞,代码跳来跳去影响可读性;但有些时候又是必须要用领域事件的。怎么掌握好这个度?这里埋一个坑,后面有机会我会讲讲这个话题。
外观 Facade
外观模式应该是所有模式里最好理解的了,如果你设计了一个系统,并通过对外暴露一些接口来提供服务。这就是外观模式。
外观模式的思想基础是六大设计原则中的最少知识原则 Least Knowledge Principle,就好像火车或飞机购票一样,其背后是由一套非常复杂的系统在运作,但顾客不需要关心那些细节,顾客只需要与售票窗口的售票员交流就可以买到票。
在包子铺中,所有对外的接口都已Facade的形式提供,就连命名上也是xxxFacade,这样看起来会一目了然。

模板方法 Template Method
包子铺中有许多定时任务,为了更好地管理这些任务,我给这些任务的执行定义了一套标准,大致如下:
java
public abstract class AbstractTask implements Runnable {
private TaskID taskId;
public void run() {
Task task = findTaskWithLock(taskId);
Assert.notNull(task);
if (task.isDone()) {
return;
}
changeTaskToRunning(task);
try {
runTask(task);
changeTaskToSuccess(task);
} catch ... {
changeTaskToFailed(task);
} finally {
}
}
// 提供给子类的扩展点
protected abstract void runTask(Task task);
}
AbstractTask
给所有的定时任务提供了一套运行模板,包括怎么做状态管理,记录错误信息,是否要重试等。 而每个具体的任务,就只需要扩展runTask()
即可。

适配器 Adapter
用过MacBook的朋友都知道,最新版MacBook都只支持Type-C接口了,这时如果你恰好有一个U盘需要连接到电脑是不行的,那么你就需要一个USB转Type-C的转换器,这个转换器就是一个适配器 Adapter。
适配器模式就是要将一种接口转换成另一种接口的模式,让原本不兼容的接口可以适配。
在包子铺中用户下单成功需要调用短信通道发送短信,但是应用层不希望感知具体的短信通道细节,因为那会使业务与具体的短信通道耦合性太高,这时就需要引入适配器:

常见的适配器有两种,上述例子给出的是基于接口的适配器模式,还有一种是基于类的适配器,感兴趣可以自行查资料研究一下。
策略模式 Strategy
依然是上面的短信发送例子,我们对接了多个短信通道,但每个通道并不都是那么稳定,因此我们希望通过动态调配比例来实现通道双活。
只需要简单增加一个类就实现了策略模式:

策略模式的作用在于定义了一个算法的多个实现(这里指的就是多个短信通道实现),并且算法之间可以互相替换,客户端可以根据不同的策略调整算法。
后记
其实包子铺用到的设计模式远不止如此,由于篇幅原因就先总结到这里。设计模式是非常神奇的东西,写代码也远不止是CRUD,希望对看到这篇文章的你有所启发。