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,防止出现“无策略可处理”的运行时异常。
策略模式扩展审批节点 策略模式:开闭原则在审批节点扩展中的应用 ApprovalStrategyDispatcher <<接口>> ApprovalStrategy ManagerApprovalStrategy 部门经理审批 FinanceApprovalStrategy 财务审批(预算校验) LegalApprovalStrategy 法务审批(合同审核) NewApprovalStrategy 新增节点(无需修改调度器) 新增审批节点时,只需实现 ApprovalStrategy 接口并注册为 Spring Bean,调度器无需修改

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 完全不需要改动。
依赖倒置原则架构图 依赖倒置:高层模块依赖抽象,低层模块实现抽象 ApprovalEngine (高层) <<接口>> PermissionValidator <<接口>> ChangeLogRepository RbacValidator 基于角色权限 AbacValidator 基于属性权限 MysqlChangeLogRepo MySQL 存储 EsChangeLogRepo Elasticsearch 存储 高层 ApprovalEngine 只依赖接口,低层实现可独立替换,符合依赖倒置原则

4.4 原则协同:一个完整的审批流设计示例

将三个原则组合使用,可以构建出高度可维护的审批流系统。下表总结了每个原则在具体模块中的落地方式:

设计原则 应用模块 具体做法 变更场景
单一职责 (SRP) 变更记录 将审批逻辑与日志记录分离到不同 Service,通过事件驱动解耦 日志存储从 MySQL 切换为 MongoDB,只改 ChangeLogService
开闭原则 (OCP) 审批节点 定义 ApprovalStrategy 接口,每个节点一个实现类,通过策略调度器路由 新增“总监审批”节点,只需新增策略类,无需修改现有代码
依赖倒置 (DIP) 权限校验 高层 ApprovalEngine 依赖 PermissionValidator 接口,低层实现可配置 从 RBAC 切换为 ABAC 权限模型,通过 @ConditionalOnProperty 切换实现
实践建议: 在项目初期,不要过度设计。先识别出“最可能变化”的维度(通常是审批节点类型和权限模型),针对这些维度应用 OCP 和 DIP;对于相对稳定的变更记录格式,先应用 SRP 分离职责即可。随着业务演进,再逐步引入更复杂的抽象。