前言
上一篇整理设计模式时,我越来越觉得,设计模式最难的部分不是代码,而是判断"这里到底需不需要用"。很多模式的结构看起来都像增加了一层接口和实现类,如果只背定义,到了项目里还是容易混淆。
这次选择的六种模式,刚好对应六类常见变化。
一、装饰器模式:不改原对象,动态叠加功能
假设系统中有一杯基础咖啡,后来需要支持加牛奶、加糖。如果为每一种组合都创建子类,很快就会出现"牛奶咖啡""加糖咖啡""牛奶加糖咖啡"等一堆类。
装饰器模式的思路,是让装饰对象和原对象实现同一个接口,并在内部持有被装饰对象:
public interface Coffee {
String description();
int price();
}
public class MilkDecorator implements Coffee {
private final Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String description() {
return coffee.description() + " + 牛奶";
}
@Override
public int price() {
return coffee.price() + 3;
}
}
使用时可以按需要组合:
Coffee coffee = new MilkDecorator(new SimpleCoffee());
装饰器比继承灵活的地方在于,功能可以在运行时组合,不需要提前为所有组合创建子类。
二、适配器模式:让不兼容的接口能够合作
项目需要接入第三方短信平台时,经常会遇到这种情况:系统希望统一调用 send(phone, content),供应商 SDK 却提供了完全不同的方法和参数对象。
如果业务代码到处直接调用供应商 SDK,后面切换平台会很痛苦。可以先定义项目自己的接口:
public interface SmsService {
void send(String phone, String content);
}
再编写适配器完成转换:
public class VendorSmsAdapter implements SmsService {
private final VendorClient client;
public VendorSmsAdapter(VendorClient client) {
this.client = client;
}
@Override
public void send(String phone, String content) {
VendorRequest request = new VendorRequest(phone, content);
client.pushMessage(request);
}
}
业务层只依赖 SmsService,并不知道第三方方法叫 pushMessage。以后替换供应商,只需增加新的适配器。
三、责任链模式:让请求依次经过多个处理器
提交订单前,可能需要进行参数校验、库存校验和风控校验。最直接的写法是把所有逻辑放进一个大方法,时间久了,这个方法会越来越长。
责任链模式把每一步拆成独立处理器,并让请求沿着链条向后传递:
public abstract class OrderHandler {
private OrderHandler next;
public OrderHandler next(OrderHandler next) {
this.next = next;
return next;
}
public void handle(Order order) {
check(order);
if (next != null) {
next.handle(order);
}
}
protected abstract void check(Order order);
}
组装链条:
OrderHandler param = new ParamHandler();
OrderHandler stock = new StockHandler();
OrderHandler risk = new RiskHandler();
param.next(stock).next(risk);
param.handle(order);
每个处理器只做一件事。校验失败时可以抛出异常或停止传递,新增校验也不需要修改其他处理器。
四、模板方法模式:固定整体流程,开放部分步骤
假设系统支持 Excel 和 CSV 文件导入。二者都需要经历读取文件、校验数据、保存数据三个阶段,区别主要在读取方式。
模板方法模式把稳定流程放在父类中,把有差异的步骤留给子类:
public abstract class DataImporter {
public final void importData(String file) {
List<Row> rows = read(file);
validate(rows);
save(rows);
}
protected abstract List<Row> read(String file);
protected void validate(List<Row> rows) {
// 通用校验
}
protected void save(List<Row> rows) {
// 通用保存
}
}
ExcelImporter 和 CsvImporter 只需要实现自己的 read()。importData() 通常声明为 final,避免子类随意打乱流程顺序。
五、策略模式:把可替换算法封装起来
电商系统可能根据普通会员、VIP 和企业客户使用不同优惠算法。把所有规则写在一个 if-else 中,新增类型时就要修改原方法。
策略模式为不同算法提供统一接口:
public interface DiscountStrategy {
BigDecimal calculate(BigDecimal amount);
}
@Component("vip")
public class VipDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.90"));
}
}
在 Spring 中,可以直接注入所有策略:
@Service
public class DiscountService {
private final Map<String, DiscountStrategy> strategies;
public DiscountService(Map<String, DiscountStrategy> strategies) {
this.strategies = strategies;
}
public BigDecimal calculate(String type, BigDecimal amount) {
DiscountStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("未知会员类型");
}
return strategy.calculate(amount);
}
}
新增优惠规则时,只需增加策略实现,原有计算流程基本不用改。
六、观察者模式:状态变化后通知多个对象
订单支付成功后,系统可能需要发送通知、增加积分、记录审计日志。如果支付服务直接依赖这些模块,每增加一个后续动作都要修改支付代码。
观察者模式让发布者只负责发布事件,由不同监听者分别处理:
public record OrderPaidEvent(Long orderId) {
}
@Service
public class PayService {
private final ApplicationEventPublisher publisher;
public void pay(Long orderId) {
// 完成支付
publisher.publishEvent(new OrderPaidEvent(orderId));
}
}
监听者只关注自己需要处理的动作:
@Component
public class PointListener {
@EventListener
public void addPoint(OrderPaidEvent event) {
// 增加积分
}
}
发布者不需要知道有多少监听者,新增通知逻辑时也不必改支付服务。这能降低模块之间的直接依赖。