4. 系统设计原则
在构建审查意见驱动的审批工作流时,系统设计原则是保证代码可维护性、可扩展性和稳定性的基石。本章将聚焦于单一职责原则(SRP)、开闭原则(OCP)和依赖倒置原则(DIP),并结合审批流中的变更记录与权限控制场景,展示如何将这些原则落地为具体的工程实践。
核心观点: 审批流本质上是一个“状态机 + 策略集”。设计原则帮助我们解耦状态转移逻辑、审批策略和持久化行为,使得每次新增审批节点或变更权限规则时,无需修改核心引擎代码。
4.1 单一职责原则:分离审批节点与变更记录
单一职责原则要求一个类或模块只负责一项明确的职责。在审批流中,最常见的错误是将“审批逻辑”与“变更记录日志”混写在同一个方法内。
反例(职责耦合):
// 一个方法同时处理审批和日志,职责不清晰
public void approve(Long orderId, String reviewer) {
// 1. 业务逻辑:更新订单状态
Order order = orderRepository.findById(orderId);
order.setStatus("APPROVED");
order.setReviewer(reviewer);
orderRepository.save(order);
// 2. 日志逻辑:记录变更
ChangeLog log = new ChangeLog();
log.setOrderId(orderId);
log.setAction("APPROVE");
log.setOperator(reviewer);
log.setTimestamp(LocalDateTime.now());
changeLogRepository.save(log);
}
正例(职责分离):
// 审批服务只负责业务决策
@Service
public class ApprovalService {
public void approve(ApprovalCommand command) {
Order order = orderRepository.findById(command.getOrderId());
order.approve(command.getReviewer()); // 委托给领域模型
orderRepository.save(order);
// 不在此处写日志
}
}
// 变更记录服务独立负责审计
@Service
public class ChangeLogService {
@EventListener
public void handleOrderApproved(OrderApprovedEvent event) {
ChangeLog log = ChangeLog.createFrom(event);
changeLogRepository.save(log);
}
}
工程收益: 当变更记录的存储介质从 MySQL 切换到 Elasticsearch 时,只需修改
ChangeLogService,审批核心逻辑完全不受影响。
4.2 开闭原则:通过策略模式扩展审批节点
开闭原则强调“对扩展开放,对修改关闭”。在审批流中,审批节点(如“部门经理审批”、“财务审批”、“法务审批”)是频繁变化的维度。通过策略模式,我们可以将每个审批节点封装为独立的策略实现。
策略接口定义:
public interface ApprovalStrategy {
boolean supports(ApprovalContext context);
ApprovalResult process(ApprovalContext context);
}
具体策略实现:
public class ManagerApprovalStrategy implements ApprovalStrategy {
@Override
public boolean supports(ApprovalContext context) {
return "MANAGER".equals(context.getNodeType());
}
@Override
public ApprovalResult process(ApprovalContext context) {
// 部门经理审批逻辑
return ApprovalResult.approved("经理审批通过");
}
}
public class FinanceApprovalStrategy implements ApprovalStrategy {
@Override
public boolean supports(ApprovalContext context) {
return "FINANCE".equals(context.getNodeType());
}
@Override
public ApprovalResult process(ApprovalContext context) {
// 财务审批逻辑,包含预算校验
return ApprovalResult.approved("财务审批通过");
}
}
策略注册与调度:
// 通过依赖注入收集所有策略
@Service
public class ApprovalStrategyDispatcher {
private final List<ApprovalStrategy> strategies;
public ApprovalStrategyDispatcher(List<ApprovalStrategy> strategies) {
this.strategies = strategies;
}
public ApprovalResult dispatch(ApprovalContext context) {
return strategies.stream()
.filter(s -> s.supports(context))
.findFirst()
.orElseThrow(() -> new UnsupportedNodeException("未知审批节点"))
.process(context);
}
}
避坑提示: 策略模式要求每个策略的
supports() 方法必须互斥且完备。建议在单元测试中通过参数化测试覆盖所有已知的 nodeType,防止出现“无策略可处理”的运行时异常。
4.3 依赖倒置原则:抽象权限校验与变更存储
依赖倒置原则要求高层模块不应依赖低层模块,二者都应依赖抽象。在审批流中,权限校验和变更记录存储是两个典型的“易变”低层模块。
抽象权限校验接口:
public interface PermissionValidator {
boolean hasPermission(String userId, String permissionCode, ApprovalContext context);
}
高层审批引擎依赖抽象:
// 审批引擎只依赖 PermissionValidator 接口,不依赖具体实现
@Service
public class ApprovalEngine {
private final PermissionValidator permissionValidator;
private final ChangeLogRepository changeLogRepository; // 也是抽象接口
public ApprovalEngine(PermissionValidator permissionValidator,
ChangeLogRepository changeLogRepository) {
this.permissionValidator = permissionValidator;
this.changeLogRepository = changeLogRepository;
}
public void execute(ApprovalTask task) {
if (!permissionValidator.hasPermission(task.getOperator(), "APPROVE", task.getContext())) {
throw new PermissionDeniedException("无审批权限");
}
// ... 执行审批逻辑
changeLogRepository.save(ChangeLog.success(task));
}
}
低层实现可灵活替换:
// 基于 RBAC 的实现
@Component
public class RbacPermissionValidator implements PermissionValidator {
@Override
public boolean hasPermission(String userId, String permissionCode, ApprovalContext context) {
// 查询用户角色,判断是否拥有 permissionCode
return true; // 简化示例
}
}
// 基于 ABAC 的实现(属性基权限控制)
@Component
@ConditionalOnProperty(name = "permission.model", havingValue = "abac")
public class AbacPermissionValidator implements PermissionValidator {
@Override
public boolean hasPermission(String userId, String permissionCode, ApprovalContext context) {
// 基于用户属性、资源属性、环境属性动态计算
return true;
}
}
设计效果: 当企业从 RBAC 迁移到 ABAC 权限模型时,只需新增一个
AbacPermissionValidator 实现,并通过配置开关切换,ApprovalEngine 完全不需要改动。
4.4 原则协同:一个完整的审批流设计示例
将三个原则组合使用,可以构建出高度可维护的审批流系统。下表总结了每个原则在具体模块中的落地方式:
| 设计原则 | 应用模块 | 具体做法 | 变更场景 |
|---|---|---|---|
| 单一职责 (SRP) | 变更记录 | 将审批逻辑与日志记录分离到不同 Service,通过事件驱动解耦 | 日志存储从 MySQL 切换为 MongoDB,只改 ChangeLogService |
| 开闭原则 (OCP) | 审批节点 | 定义 ApprovalStrategy 接口,每个节点一个实现类,通过策略调度器路由 | 新增“总监审批”节点,只需新增策略类,无需修改现有代码 |
| 依赖倒置 (DIP) | 权限校验 | 高层 ApprovalEngine 依赖 PermissionValidator 接口,低层实现可配置 | 从 RBAC 切换为 ABAC 权限模型,通过 @ConditionalOnProperty 切换实现 |
实践建议: 在项目初期,不要过度设计。先识别出“最可能变化”的维度(通常是审批节点类型和权限模型),针对这些维度应用 OCP 和 DIP;对于相对稳定的变更记录格式,先应用 SRP 分离职责即可。随着业务演进,再逐步引入更复杂的抽象。