状态模式-住院患者状态管理

状态模式在住院患者管理中的实战应用

今天聊一聊状态模式。之前我们聊过策略模式,解决的是"同一行为有多种算法实现"的问题;状态模式解决的是另一个高频问题------对象的行为依赖于它的状态,状态一变,行为跟着变。

状态模式(State Pattern)的定义是:

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),是一种对象行为型模式。

标准实现示例:

typescript 复制代码
// 状态抽象接口
interface OrderState {
    void pay(OrderContext ctx);
    void cancel(OrderContext ctx);
}
​
// 上下文:订单
class OrderContext {
    private OrderState state;
​
    public OrderContext(OrderState initState) {
        this.state = initState;
    }
​
    public void setState(OrderState state) {
        this.state = state;
    }
​
    // 委托给状态
    public void pay() {
        state.pay(this);
    }
​
    public void cancel() {
        state.cancel(this);
    }
}
​
// 状态1:待支付
class WaitPayState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        System.out.println("待支付 -> 支付成功,切换到已支付");
        ctx.setState(new PaidState());
    }
​
    @Override
    public void cancel(OrderContext ctx) {
        System.out.println("待支付 -> 取消订单,切换到已取消");
        ctx.setState(new CanceledState());
    }
}
​
// 状态2:已支付
class PaidState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        // 【重复行为示例】已支付不能再次支付,和已取消的pay逻辑一模一样
        System.out.println("当前状态不支持重复支付");
    }
​
    @Override
    public void cancel(OrderContext ctx) {
        System.out.println("已支付 -> 发起退款,切换到已取消");
        ctx.setState(new CanceledState());
    }
}
​
// 状态3:已取消
class CanceledState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        // 和PaidState的pay()完全相同!代码重复
        System.out.println("当前状态不支持重复支付");
    }
​
    @Override
    public void cancel(OrderContext ctx) {
        System.out.println("已经取消,不能重复取消");
    }
}
​
// 测试
public class StateNormalDemo {
    public static void main(String[] args) {
        OrderContext order = new OrderContext(new WaitPayState());
        order.pay();
        order.pay(); // 已支付,提示不能支付
        order.cancel();
        order.pay(); // 已取消,同样提示不能支付
    }
}
​

这是状态模式的经典定义。我的理解是将状态与行为绑定,有点像我们常说的"到什么时候,做什么事情"。

判断一段代码是否适合状态模式,主要看两点:一是代码中是否出现大量与状态有关的条件判断(每个业务方法里都散落着 if (status == ...)),二是对象的行为是否会随状态不同而变化。这里的行为变化有两个层次:1、当前状态允许做什么、不允许做什么;2、同一个行为在不同状态下采用不同的处理方式。

设计模式的标准实现如果放到 Spring Boot 项目中该怎么落地?下面以医院 HIS 中典型的住院患者全生命周期管理业务为例,完成状态模式的落地。


一、业务场景描述

住院患者从入院到出院,生命周期会经过以下状态(这不是唯一标准,不同 HIS 厂商的称呼可能不同,这里抽象的是一个最简流程):

  • 入院登记(REGISTERED) :患者办理入院手续,尚未入科;
  • 已入科/待分床(ADMITTED) :已进入病区,等待护士分床;
  • 在床/住院诊疗中(IN_BED) :核心状态,普通医嘱的下达、分解、执行等主要操作在这里发生;
  • 预出院(PRE_DISCHARGE) :医生已开出院医嘱,长期医嘱自动停止,只允许出院带药相关操作和结算;
  • 已出院(DISCHARGED) :终态,一切医疗操作禁止。

下面整理了一个患者在不同状态下的业务行为定义:

操作 入院登记 待分床 在床 预出院 已出院
入科 允许 → 待分床 --- --- --- ---
分床/换床 拒绝(先入科) 允许 → 在床 允许(换床,状态不变) 拒绝 拒绝
下达医嘱 拒绝 拒绝 允许 仅出院带药 拒绝
医嘱分解/执行 拒绝 拒绝 允许 仅出院带药 拒绝
申请出院 拒绝 拒绝 允许 → 预出院 拒绝(勿重复) 拒绝
结算 拒绝 拒绝 拒绝 允许 → 已出院 拒绝

如果不使用设计模式,按照上面的需求直接翻译成代码,大概的结构如下:

typescript 复制代码
@Service
public class PatientService {
​
    @Autowired
    private PatientMapper patientMapper;
​
    public void assignBed(String patientId, String bedNo, String operatorId) {
        PatientPO po = patientMapper.selectById(patientId);
        if ("ADMITTED".equals(po.getStatusCode())) {
            // 分床
            po.setBedNo(bedNo);
            po.setStatusCode("IN_BED");
            patientMapper.updateById(po);
            // 记流水......
        } else if ("IN_BED".equals(po.getStatusCode())) {
            // 换床:状态不变,只改床位
            if (bedNo.equals(po.getBedNo())) {
                throw new IllegalStateException("患者已在该床位");
            }
            po.setBedNo(bedNo);
            patientMapper.updateById(po);
            // 记流水......
        } else {
            throw new IllegalStateException("当前状态不允许分床");
        }
    }
​
    public void createOrder(String patientId, Order order, String doctorId) {
        PatientPO po = patientMapper.selectById(patientId);
        if ("IN_BED".equals(po.getStatusCode())) {
            // 正常开医嘱......
        } else if ("PRE_DISCHARGE".equals(po.getStatusCode())) {
            // 只能开出院带药
            if (!"DISCHARGE_MED".equals(order.getOrderType())) {
                throw new IllegalStateException("预出院只能开出院带药");
            }
            // ......
        } else {
            throw new IllegalStateException("当前状态不允许下达医嘱");
        }
    }
​
    // applyDischarge、settle、executeOrder......每个方法都重复一遍同样的状态判断
}

有的人会说,状态多了容易乱。的确,状态多会增加复杂度,但我个人认为,最主要的原因还是结构不清晰,需要按状态和行为进一步抽象。

大概总结一下,如下:

  • 状态判断散落各处 :每个方法都要把全部状态判断一遍,新增一个操作就要写一整段 if......else if......;
  • 状态转换规则不集中:想知道"预出院"到底允许哪些操作,必须翻遍所有方法才能拼凑出来;
  • 业务行为与状态校验混杂:改数据、改状态、记流水、发通知全部堆在 Service 里,Service 越来越臃肿;
  • 扩展性差:新增一种状态容易出现漏改的情况。

经典定义主要说明状态与行为的抽象,具体业务如何落地仍需要结合项目结构设计。最容易出现的问题之一,就是把状态模式中的状态类退化为校验器。下面我们一步步拆解,看看如何用"状态模式 + DDD"让住院患者的状态、行为和转换规则很好地协同工作。


二、贫血模型实现状态模式

既然是贫血模型,先把业务对象定义一下。

2.1 定义患者实体

arduino 复制代码
/**
 * 住院患者(贫血模型):纯数据载体,无任何业务方法
 */
@Data
public class Patient {
​
    private String patientId;  // 患者id
    private String name;       // 患者姓名
    private String idCard;     // 身份证号
    private String wardCode;   // 病区编码
    private String bedNo;      // 床号
    private String statusCode;             // 状态编码
    private LocalDateTime wardAdmissionTime;   // 入科时间
    private LocalDateTime dischargeTime;   // 出院时间
    ......
}

患者实体是贫血模型对象,其实主要是数据模型,用来为业务行为提供数据。

2.2 抽象业务行为

arduino 复制代码
/**
 * 患者业务行为接口(Context 角色)
 * 状态类守卫通过后,回调这里的方法执行真正的业务行为
 */
public interface PatientOperations {
​
    void doAdmitToWard(Patient patient, String wardCode);
​
    void doAssignBed(Patient patient, String bedNo, String operatorId);
​
    void doChangeBed(Patient patient, String bedNo, String operatorId);
​
    void doCreateOrder(Patient patient, Order order, String doctorId);
​
    void doDecomposeOrder(Order order, String nurseId);
​
    void doExecuteOrder(Order order, String nurseId);
​
    void doApplyDischarge(Patient patient, String advice, String doctorId);
​
    void doSettle(Patient patient, Settlement settlement, String cashierId);
}

这个就是我们在三层架构中定义的服务接口(XxxxService),定义业务行为。

2.3 抽象状态

typescript 复制代码
/**
 * 住院患者状态接口(贫血路线:Service 作为 Context)
 *
 * 设计要点:
 * 1. 第一个参数 ops 是行为回调接口------Context 角色,由 Service 实现
 * 2. 守卫通过后回调 ops.doXxx(),从统一入口调用时可保证"守卫 → 执行"的顺序
 * 3. 默认实现即"当前状态不支持该操作"
 */
public interface PatientState {
​
    String getStateCode();
​
    default void admitToWard(PatientOperations ops, Patient patient, String wardCode) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许入科");
    }
​
    default void assignBed(PatientOperations ops, Patient patient, String bedNo, String operatorId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许分配床位");
    }
​
    default void createOrder(PatientOperations ops, Patient patient, Order order, String doctorId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许下达医嘱");
    }
​
    /** 医嘱由 Service 先行加载并作为参数传入,状态类不接触 OrderService */
    default void decomposeOrder(PatientOperations ops, Patient patient, Order order, String nurseId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许分解医嘱");
    }
​
    default void executeOrder(PatientOperations ops, Patient patient, Order order, String nurseId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许执行医嘱");
    }
​
    default void applyDischarge(PatientOperations ops, Patient patient, String advice, String doctorId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许申请出院");
    }
​
    default void settle(PatientOperations ops, Patient patient, Settlement settlement, String cashierId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许结算");
    }
​
    default String getStateName() {
        return PatientStateEnum.of(getStateCode()).getName();
    }
}

2.4 利用枚举实现状态工厂

状态在数据库里是一个编码字段,枚举集中维护"编码 ↔ 中文名 ↔ 状态对象"的映射。状态类无状态(不持有任何患者数据),天然是单例。 通过业务对象的状态值,可以拿到当前状态对象。

arduino 复制代码
/**
 * 住院患者状态枚举
 *
 * 枚举实例直接持有对应的状态对象(无状态单例),
 * "编码 → 状态"的查找由枚举完成,状态类不需要 Spring 管理
 */
public enum PatientStateEnum {
​
    REGISTERED("REGISTERED", "入院登记", new RegisteredState()),
    ADMITTED("ADMITTED", "已入科待分床", new AdmittedState()),
    IN_BED("IN_BED", "在床诊疗中", new InBedState()),
    PRE_DISCHARGE("PRE_DISCHARGE", "预出院", new PreDischargeState()),
    DISCHARGED("DISCHARGED", "已出院", new DischargedState());
​
    private final String code;
    private final String name;
    private final PatientState state;      // 状态对象是无状态单例,跟着枚举走
​
    PatientStateEnum(String code, String name, PatientState state) {
        this.code = code;
        this.name = name;
        this.state = state;
    }
​
    public String getCode() {
        return code;
    }
​
    public String getName() {
        return name;
    }
​
    /** 取出该编码对应的状态对象 */
    public PatientState state() {
        return state;
    }
​
    public static PatientStateEnum of(String code) {
        for (PatientStateEnum e : values()) {
            if (e.code.equals(code)) {
                return e;
            }
        }
        throw new IllegalArgumentException("未知的患者状态编码:" + code);
    }
}

2.5 具体状态实现

具体状态方法的通常形态是:允许的操作在校验通过后回调业务行为,不允许的操作抛出异常或沿用默认拦截。全部五个状态示例如下(业务行为只是举例,不一定符合实际住院业务):

typescript 复制代码
/** 入院登记状态:只允许入科 */
public class RegisteredState implements PatientState {
​
    @Override
    public String getStateCode() {
        return PatientStateEnum.REGISTERED.getCode();
    }
​
    @Override
    public void admitToWard(PatientOperations ops, Patient patient, String wardCode) {
        if (wardCode == null || wardCode.trim().isEmpty()) {
            throw new IllegalArgumentException("病区不能为空");
        }
        ops.doAdmitToWard(patient, wardCode);
    }
​
    @Override
    public void assignBed(PatientOperations ops, Patient patient, String bedNo, String operatorId) {
        throw new IllegalStateException("尚未入科,请先执行入科操作");
    }
}
​
/** 已入科/待分床状态:核心业务是分床 → 流转到在床状态 */
public class AdmittedState implements PatientState {
​
    @Override
    public String getStateCode() {
        return PatientStateEnum.ADMITTED.getCode();
    }
​
    @Override
    public void assignBed(PatientOperations ops, Patient patient, String bedNo, String operatorId) {
        if (bedNo == null || bedNo.trim().isEmpty()) {
            throw new IllegalArgumentException("床位号不能为空");
        }
        ops.doAssignBed(patient, bedNo, operatorId);
    }
}
​
/** 在床/住院诊疗中状态:普通医嘱的主要操作在此完成 */
public class InBedState implements PatientState {
​
    @Override
    public String getStateCode() {
        return PatientStateEnum.IN_BED.getCode();
    }
​
    @Override
    public void assignBed(PatientOperations ops, Patient patient, String bedNo, String operatorId) {
        if (bedNo == null || bedNo.trim().isEmpty()) {
            throw new IllegalArgumentException("床位号不能为空");
        }
        if (bedNo.equals(patient.getBedNo())) {
            throw new IllegalStateException("患者已在床位 " + bedNo + ",无需重复分配");
        }
        ops.doChangeBed(patient, bedNo, operatorId);
    }
​
    @Override
    public void createOrder(PatientOperations ops, Patient patient, Order order, String doctorId) {
        if (order == null || order.getOrderText() == null || order.getOrderText().trim().isEmpty()) {
            throw new IllegalArgumentException("医嘱内容不能为空");
        }
        ops.doCreateOrder(patient, order, doctorId);
    }
​
    @Override
    public void decomposeOrder(PatientOperations ops, Patient patient, Order order, String nurseId) {
        // 医嘱由 Service 先行加载传入,守卫只判断不加载
        if (order == null) {
            throw new IllegalArgumentException("医嘱不能为空");
        }
        if (!"CREATED".equals(order.getStatus())) {
            throw new IllegalStateException("医嘱状态为【" + order.getStatus() + "】,无法分解,请确认医嘱已下达");
        }
        ops.doDecomposeOrder(order, nurseId);
    }
​
    @Override
    public void executeOrder(PatientOperations ops, Patient patient, Order order, String nurseId) {
        if (order == null) {
            throw new IllegalArgumentException("医嘱不能为空");
        }
        if (!"DECOMPOSED".equals(order.getStatus())) {
            throw new IllegalStateException("医嘱未分解,请先分解再执行");
        }
        ops.doExecuteOrder(order, nurseId);
    }
​
    @Override
    public void applyDischarge(PatientOperations ops, Patient patient, String advice, String doctorId) {
        if (advice == null || advice.trim().isEmpty()) {
            throw new IllegalArgumentException("出院医嘱/建议不能为空");
        }
        ops.doApplyDischarge(patient, advice, doctorId);
    }
}
​
/** 预出院状态:只允许出院带药的下达/分解/执行,以及结算 */
public class PreDischargeState implements PatientState {
​
    @Override
    public String getStateCode() {
        return PatientStateEnum.PRE_DISCHARGE.getCode();
    }
​
    @Override
    public void createOrder(PatientOperations ops, Patient patient, Order order, String doctorId) {
        if (order == null || order.getOrderText() == null || order.getOrderText().trim().isEmpty()) {
            throw new IllegalArgumentException("医嘱内容不能为空");
        }
        if (!"DISCHARGE_MED".equals(order.getOrderType())) {
            throw new IllegalStateException("预出院状态只能开具出院带药,不能开其他医嘱");
        }
        ops.doCreateOrder(patient, order, doctorId);
    }
​
    @Override
    public void decomposeOrder(PatientOperations ops, Patient patient, Order order, String nurseId) {
        if (order == null) {
            throw new IllegalArgumentException("医嘱不能为空");
        }
        if (!"DISCHARGE_MED".equals(order.getOrderType())) {
            throw new IllegalStateException("预出院状态只能分解出院带药医嘱");
        }
        if (!"CREATED".equals(order.getStatus())) {
            throw new IllegalStateException("只有已下达的医嘱才能分解");
        }
        ops.doDecomposeOrder(order, nurseId);
    }
​
    @Override
    public void executeOrder(PatientOperations ops, Patient patient, Order order, String nurseId) {
        if (order == null) {
            throw new IllegalArgumentException("医嘱不能为空");
        }
        if (!"DISCHARGE_MED".equals(order.getOrderType())) {
            throw new IllegalStateException("预出院状态只能执行出院带药");
        }
        if (!"DECOMPOSED".equals(order.getStatus())) {
            throw new IllegalStateException("出院带药医嘱未分解,请先分解再执行");
        }
        ops.doExecuteOrder(order, nurseId);
    }
​
    @Override
    public void settle(PatientOperations ops, Patient patient, Settlement settlement, String cashierId) {
        if (settlement == null) {
            throw new IllegalArgumentException("结算信息不能为空");
        }
        if (!patient.getPatientId().equals(settlement.getPatientId())) {
            throw new IllegalArgumentException("结算单不属于当前患者");
        }
        ops.doSettle(patient, settlement, cashierId);
    }
​
    @Override
    public void applyDischarge(PatientOperations ops, Patient patient, String advice, String doctorId) {
        throw new IllegalStateException("已申请出院,请勿重复操作");
    }
}
​
​
/** 已出院状态:终态,全部拒绝,自定义业务指引 */
public class DischargedState implements PatientState {
​
    @Override
    public String getStateCode() {
        return PatientStateEnum.DISCHARGED.getCode();
    }
​
    @Override
    public void createOrder(PatientOperations ops, Patient patient, Order order, String doctorId) {
        throw new IllegalStateException("已出院,不能下达医嘱,请办理再次入院或门诊就诊");
    }
​
    @Override
    public void settle(PatientOperations ops, Patient patient, Settlement settlement, String cashierId) {
        throw new IllegalStateException("已结算出院,不能重复结算");
    }
}

到这里,已经完成了所有状态下行为的定义,实现了状态和行为的抽象。

2.6 业务服务实现

下面在 Service 方法中,通过状态模式组织状态守卫、业务行为和状态迁移:

scss 复制代码
/**
 * 住院患者应用服务(贫血路线:Service 作为 Context)
 *
 * 设计要点:
 * 1. 入口方法只做三件事:加载 → 状态分发(this 传入)→ 保存
 * 2. doXxx() 实现 PatientOperations,承载全部业务行为,
 *    可以使用任何 Spring 依赖(BedService、OrderService 等)------这是贫血路线的独特优势
 */
@Service
public class PatientService implements PatientOperations {
​
    @Autowired
    private PatientRepository patientRepository;
​
    @Autowired
    private BedService bedService;          // 床位系统
​
    @Autowired
    private OrderService orderService;      // 医嘱子系统
​
    // ======================== 对外入口:加载 → 状态分发 → 保存 ========================
​
    @Transactional
    public void admitToWard(String patientId, String wardCode) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        PatientStateEnum.of(patient.getStatusCode()).state()
                .admitToWard(this, patient, wardCode);
        patientRepository.save(patient);
    }
​
    @Transactional
    public void assignBed(String patientId, String bedNo, String operatorId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        PatientStateEnum.of(patient.getStatusCode()).state()
                .assignBed(this, patient, bedNo, operatorId);
        patientRepository.save(patient);
    }
​
    @Transactional
    public void createOrder(String patientId, Order order, String doctorId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        PatientStateEnum.of(patient.getStatusCode()).state()
                .createOrder(this, patient, order, doctorId);
        patientRepository.save(patient);
    }
​
    @Transactional
    public void decomposeOrder(String patientId, String orderId, String nurseId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        Order order = orderService.loadForUpdate(orderId);   // 医嘱独立加载
        if (order == null) {
            throw new IllegalArgumentException("医嘱不存在:" + orderId);
        }
        if (!patientId.equals(order.getPatientId())) {
            throw new IllegalArgumentException("医嘱不属于当前患者");
        }
        PatientStateEnum.of(patient.getStatusCode()).state()
                .decomposeOrder(this, patient, order, nurseId);
        patientRepository.save(patient);
    }
​
    @Transactional
    public void executeOrder(String patientId, String orderId, String nurseId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        Order order = orderService.loadForUpdate(orderId);
        if (order == null) {
            throw new IllegalArgumentException("医嘱不存在:" + orderId);
        }
        if (!patientId.equals(order.getPatientId())) {
            throw new IllegalArgumentException("医嘱不属于当前患者");
        }
        PatientStateEnum.of(patient.getStatusCode()).state()
                .executeOrder(this, patient, order, nurseId);
        patientRepository.save(patient);
    }
​
    @Transactional
    public void applyDischarge(String patientId, String advice, String doctorId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        PatientStateEnum.of(patient.getStatusCode()).state()
                .applyDischarge(this, patient, advice, doctorId);
        patientRepository.save(patient);
    }
​
    @Transactional
    public void settle(String patientId, Settlement settlement, String cashierId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        PatientStateEnum.of(patient.getStatusCode()).state()
                .settle(this, patient, settlement, cashierId);
        patientRepository.save(patient);
    }
​
    // ======================== 状态类回调的业务行为 ========================
​
    @Override
    public void doAdmitToWard(Patient patient, String wardCode) {
        patient.setWardCode(wardCode);
        patient.setWardAdmissionTime(LocalDateTime.now());
        patient.setStatusCode(PatientStateEnum.ADMITTED.getCode());
    }
​
    @Override
    public void doAssignBed(Patient patient, String bedNo, String operatorId) {
        bedService.occupy(bedNo, patient.getPatientId());       // 联动床位系统
        patient.setBedNo(bedNo);
        patient.setStatusCode(PatientStateEnum.IN_BED.getCode());
    }
​
    @Override
    public void doChangeBed(Patient patient, String bedNo, String operatorId) {
        bedService.release(patient.getBedNo());
        bedService.occupy(bedNo, patient.getPatientId());
        patient.setBedNo(bedNo);                                 // 换床,状态不变
    }
​
    @Override
    public void doCreateOrder(Patient patient, Order order, String doctorId) {
        order.setPatientId(patient.getPatientId());
        order.setDoctorId(doctorId);
        order.setBedNo(patient.getBedNo());                      // 床位快照,追溯用
        orderService.create(order);                              // 医嘱的创建与持久化归医嘱子系统
    }
​
    @Override
    public void doDecomposeOrder(Order order, String nurseId) {
        order.setStatus("DECOMPOSED");
        order.setNurseId(nurseId);
        orderService.updateStatus(order);
    }
​
    @Override
    public void doExecuteOrder(Order order, String nurseId) {
        order.setStatus("EXECUTED");
        order.setNurseId(nurseId);
        order.setExecuteTime(LocalDateTime.now());
        orderService.updateStatus(order);
    }
​
    @Override
    public void doApplyDischarge(Patient patient, String advice, String doctorId) {
        // 停止所有未完成的长期医嘱:批量操作交给医嘱子系统
        orderService.stopUnfinishedLongTermOrders(patient.getPatientId());
        patient.setStatusCode(PatientStateEnum.PRE_DISCHARGE.getCode());
    }
​
    @Override
    public void doSettle(Patient patient, Settlement settlement, String cashierId) {
        bedService.release(patient.getBedNo());                  // 释放床位
        patient.setStatusCode(PatientStateEnum.DISCHARGED.getCode());
    }
}

调用边界: PatientOperations 中的回调方法受 Java 接口约束,必须是 public,但它们不是对外业务入口。Controller 等调用方只应调用 assignBed()、createOrder() 等统一入口;如果需要在编译期强制隔离,可进一步将回调封装为独立的内部协作对象。

2.7 单元测试

less 复制代码
/**
 * 贫血路线:PatientService 单元测试(Mockito)
 * 重点验证:守卫拦截发生在任何副作用之前;守卫通过后联动正确
 */
@ExtendWith(MockitoExtension.class)
class PatientServiceTest {

    @Mock
    private PatientRepository patientRepository;

    @Mock
    private BedService bedService;

    @Mock
    private OrderService orderService;

    @InjectMocks
    private PatientService patientService;

    @Test
    void 待分床状态分床成功并联动床位系统() {
        Patient patient = patientWithStatus("ADMITTED");
        when(patientRepository.loadForUpdate("P001")).thenReturn(patient);

        patientService.assignBed("P001", "12-03", "N001");

        assertEquals("IN_BED", patient.getStatusCode());
        verify(bedService).occupy("12-03", "P001");       // 床位被占用
        verify(patientRepository).save(patient);
    }

    @Test
    void 登记状态分床被拦截且不产生任何副作用() {
        Patient patient = patientWithStatus("REGISTERED");
        when(patientRepository.loadForUpdate("P001")).thenReturn(patient);

        assertThrows(IllegalStateException.class,
                () -> patientService.assignBed("P001", "12-03", "N001"));

        verifyNoInteractions(bedService);                  // 守卫拦截,床位系统零接触
        verify(patientRepository, never()).save(any());    // 也没有落库
    }

    @Test
    void 在床状态分解医嘱() {
        Patient patient = patientWithStatus("IN_BED");
        Order order = new Order();
        order.setOrderId("O001");
        order.setPatientId("P001");
        order.setStatus("CREATED");
        when(patientRepository.loadForUpdate("P001")).thenReturn(patient);
        when(orderService.loadForUpdate("O001")).thenReturn(order);

        patientService.decomposeOrder("P001", "O001", "N001");

        assertEquals("DECOMPOSED", order.getStatus());
        verify(orderService).updateStatus(order);
    }

    @Test
    void 申请出院时停止未完成长期医嘱() {
        Patient patient = patientWithStatus("IN_BED");
        when(patientRepository.loadForUpdate("P001")).thenReturn(patient);

        patientService.applyDischarge("P001", "病情好转,准予出院", "D001");

        assertEquals("PRE_DISCHARGE", patient.getStatusCode());
        verify(orderService).stopUnfinishedLongTermOrders("P001");
    }

    private Patient patientWithStatus(String statusCode) {
        Patient p = new Patient();
        p.setPatientId("P001");
        p.setStatusCode(statusCode);
        return p;
    }
}

2.8 总结

上面的实现是传统三层架构下基于贫血模型的实现方式。从设计模式的角度看,它按照状态抽象了业务行为,代码结构比较清晰,明确地展现了状态与行为的关系,对延缓代码腐化有一定作用。

下面我们再尝试用充血模型(DDD 四层架构)实现状态模式。


三、充血模型实现状态模式

充血模型示例采用 DDD 四层架构。业务行为放在实体中,Service 层保持轻量,负责事务管理和用例编排,Repository(仓储)负责持久化。

下面给出代码结构:

scss 复制代码
com.example.inpatient
├── domain
│   ├── patient
│   │   ├── Patient.java              // 聚合根
│   │   ├── PatientState.java         // 状态接口
│   │   ├── PatientStateEnum.java     // 状态枚举(持有状态单例)
│   │   ├── Order.java                // 聚合内医嘱实体(自带医嘱状态机)
│   │   ├── Settlement.java           // 结算单
│   │   ├── RegisteredState.java      // 以下五个状态类与 Patient 同包
│   │   ├── AdmittedState.java
│   │   ├── InBedState.java
│   │   ├── PreDischargeState.java
│   │   └── DischargedState.java
│   └── bed
│       └── Bed.java                  // 床位实体(参与聚合内业务协同)
├── repository
│   └── PatientRepository.java        // 聚合的加载、重建与持久化
└── service
    └── PatientService.java           // 薄事务门面

3.1 定义状态枚举

arduino 复制代码
/**
 * 住院患者状态枚举
 * 枚举实例直接持有对应的状态对象(无状态单例),"编码 → 状态"查找由枚举完成
 */
public enum PatientStateEnum {

    REGISTERED("REGISTERED", "入院登记", new RegisteredState()),
    ADMITTED("ADMITTED", "已入科待分床", new AdmittedState()),
    IN_BED("IN_BED", "在床诊疗中", new InBedState()),
    PRE_DISCHARGE("PRE_DISCHARGE", "预出院", new PreDischargeState()),
    DISCHARGED("DISCHARGED", "已出院", new DischargedState());

    private final String code;
    private final String name;
    private final PatientState state;

    PatientStateEnum(String code, String name, PatientState state) {
        this.code = code;
        this.name = name;
        this.state = state;
    }

    public String getCode() {
        return code;
    }

    public String getName() {
        return name;
    }

    public PatientState state() {
        return state;
    }

    public static PatientStateEnum of(String code) {
        for (PatientStateEnum e : values()) {
            if (e.code.equals(code)) {
                return e;
            }
        }
        throw new IllegalArgumentException("未知的患者状态编码:" + code);
    }
}

3.2 定义状态接口

arduino 复制代码
import com.example.inpatient.domain.bed.Bed;

/**
 * 住院患者状态接口
 *
 * 职责边界:
 * 1. 状态类只做守卫------判断"患者当前状态 × 这个操作"在权限矩阵里是否允许,
 *    以及操作参数是否合法(包括读取聚合内对象做判断)
 * 2. 守卫通过后回调 patient.doXxx(),业务协同(对象间交互)发生在聚合根内部
 * 3. 默认实现即"当前状态不允许",具体状态类只重写允许的操作
 */
public interface PatientState {

    String getStateCode();

    /** 入科 */
    default void admitToWard(Patient patient, String wardCode) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许入科");
    }

    /** 分床/换床 */
    default void assignBed(Patient patient, Bed bed, String operatorId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许分配床位");
    }

    /** 下达医嘱 */
    default void createOrder(Patient patient, Order order, String doctorId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许下达医嘱");
    }

    /** 医嘱分解 */
    default void decomposeOrder(Patient patient, String orderId, String nurseId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许分解医嘱");
    }

    /** 执行医嘱 */
    default void executeOrder(Patient patient, String orderId, String nurseId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许执行医嘱");
    }

    /** 申请出院 */
    default void applyDischarge(Patient patient, String advice, String doctorId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许申请出院");
    }

    /** 结算 */
    default void settle(Patient patient, Settlement settlement, String cashierId) {
        throw new IllegalStateException("当前状态【" + getStateName() + "】不允许结算");
    }

    default String getStateName() {
        return PatientStateEnum.of(getStateCode()).getName();
    }
}

3.3 状态模式实现

以下每个 public 状态类分别放在同名的 .java 文件中,并与 Patient 位于同一包。

typescript 复制代码
package com.example.inpatient.domain.patient;

import com.example.inpatient.domain.bed.Bed;

/** 入院登记状态:只允许入科 */
public class RegisteredState implements PatientState {

    @Override
    public String getStateCode() {
        return PatientStateEnum.REGISTERED.getCode();
    }

    @Override
    public void admitToWard(Patient patient, String wardCode) {
        if (wardCode == null || wardCode.trim().isEmpty()) {
            throw new IllegalArgumentException("病区不能为空");
        }
        patient.doAdmitToWard(wardCode);
    }

    @Override
    public void assignBed(Patient patient, Bed bed, String operatorId) {
        throw new IllegalStateException("尚未入科,请先执行入科操作");
    }
}

/** 已入科/待分床状态:核心业务是分床 → 流转到在床状态 */
public class AdmittedState implements PatientState {

    @Override
    public String getStateCode() {
        return PatientStateEnum.ADMITTED.getCode();
    }

    @Override
    public void assignBed(Patient patient, Bed bed, String operatorId) {
        if (bed == null) {
            throw new IllegalArgumentException("床位不能为空");
        }
        // 床位空闲与否的最终守卫在 Bed.occupy() 内部,这里提前拦截给出明确提示
        if (!bed.isFree()) {
            throw new IllegalStateException("床位 " + bed.getBedNo() + " 已被占用");
        }
        patient.doAssignBed(bed, operatorId);
    }
}

/** 在床/住院诊疗中状态:普通医嘱的主要操作在此完成 */
public class InBedState implements PatientState {

    @Override
    public String getStateCode() {
        return PatientStateEnum.IN_BED.getCode();
    }

    @Override
    public void assignBed(Patient patient, Bed bed, String operatorId) {
        // 在床状态的分床即换床:状态不变
        if (bed == null) {
            throw new IllegalArgumentException("床位不能为空");
        }
        if (bed.getBedNo().equals(patient.getCurrentBed().getBedNo())) {
            throw new IllegalStateException("患者已在床位 " + bed.getBedNo() + ",无需重复分配");
        }
        if (!bed.isFree()) {
            throw new IllegalStateException("床位 " + bed.getBedNo() + " 已被占用");
        }
        patient.doChangeBed(bed, operatorId);
    }

    @Override
    public void createOrder(Patient patient, Order order, String doctorId) {
        if (order == null || order.getOrderText() == null || order.getOrderText().trim().isEmpty()) {
            throw new IllegalArgumentException("医嘱内容不能为空");
        }
        patient.doCreateOrder(order, doctorId);
    }

    @Override
    public void decomposeOrder(Patient patient, String orderId, String nurseId) {
        // 存在性守卫在聚合根;医嘱自身状态(CREATED?)的守卫在 Order 实体内部
        patient.doDecomposeOrder(orderId, nurseId);
    }

    @Override
    public void executeOrder(Patient patient, String orderId, String nurseId) {
        patient.doExecuteOrder(orderId, nurseId);
    }

    @Override
    public void applyDischarge(Patient patient, String advice, String doctorId) {
        if (advice == null || advice.trim().isEmpty()) {
            throw new IllegalArgumentException("出院医嘱/建议不能为空");
        }
        patient.doApplyDischarge(advice, doctorId);
    }
}

/** 预出院状态:只允许出院带药的下达/分解/执行,以及结算 */
public class PreDischargeState implements PatientState {

    @Override
    public String getStateCode() {
        return PatientStateEnum.PRE_DISCHARGE.getCode();
    }

    @Override
    public void createOrder(Patient patient, Order order, String doctorId) {
        if (order == null || order.getOrderText() == null || order.getOrderText().trim().isEmpty()) {
            throw new IllegalArgumentException("医嘱内容不能为空");
        }
        if (!order.isDischargeMed()) {
            throw new IllegalStateException("预出院状态只能开具出院带药,不能开其他医嘱");
        }
        patient.doCreateOrder(order, doctorId);
    }

    @Override
    public void decomposeOrder(Patient patient, String orderId, String nurseId) {
        // "仅出院带药"是患者状态×操作的矩阵规则,归状态类守卫
        Order order = patient.findOrder(orderId);
        if (!order.isDischargeMed()) {
            throw new IllegalStateException("预出院状态只能分解出院带药医嘱");
        }
        patient.doDecomposeOrder(orderId, nurseId);
    }

    @Override
    public void executeOrder(Patient patient, String orderId, String nurseId) {
        Order order = patient.findOrder(orderId);
        if (!order.isDischargeMed()) {
            throw new IllegalStateException("预出院状态只能执行出院带药");
        }
        patient.doExecuteOrder(orderId, nurseId);
    }

    @Override
    public void settle(Patient patient, Settlement settlement, String cashierId) {
        if (settlement == null) {
            throw new IllegalArgumentException("结算信息不能为空");
        }
        if (!patient.getPatientId().equals(settlement.getPatientId())) {
            throw new IllegalArgumentException("结算单不属于当前患者");
        }
        patient.doSettle(settlement, cashierId);
    }

    @Override
    public void applyDischarge(Patient patient, String advice, String doctorId) {
        throw new IllegalStateException("已申请出院,请勿重复操作");
    }
}


/** 已出院状态:终态,全部拒绝,自定义业务指引 */
public class DischargedState implements PatientState {

    @Override
    public String getStateCode() {
        return PatientStateEnum.DISCHARGED.getCode();
    }

    @Override
    public void createOrder(Patient patient, Order order, String doctorId) {
        throw new IllegalStateException("已出院,不能下达医嘱,请办理再次入院或门诊就诊");
    }

    @Override
    public void settle(Patient patient, Settlement settlement, String cashierId) {
        throw new IllegalStateException("已结算出院,不能重复结算");
    }
}

3.4 实体定义

3.4.1 床位实体
typescript 复制代码
/**
 * 床位实体
 *
 * 床位的占用规则(空闲才能占、占没占用自己说了算)封装在 Bed 内部,
 * 外部不许 setStatus 绕过规则------床位只暴露 occupy()/release() 两个行为
 */
public class Bed {

    private final String bedNo;
    private final String wardCode;
    private boolean occupied;
    private String occupantPatientId;

    public Bed(String bedNo, String wardCode) {
        this.bedNo = bedNo;
        this.wardCode = wardCode;
        this.occupied = false;
    }

    /** 仓储还原专用:完整恢复床位占用状态,不执行新业务行为 */
    public static Bed reconstitute(String bedNo, String wardCode,
                                   boolean occupied, String occupantPatientId) {
        Bed bed = new Bed(bedNo, wardCode);
        bed.occupied = occupied;
        bed.occupantPatientId = occupantPatientId;
        return bed;
    }

    public boolean isFree() {
        return !occupied;
    }

    /** 占用床位:床位的自身规则------只有空闲床位才能被占用 */
    public void occupy(String patientId) {
        if (occupied) {
            throw new IllegalStateException("床位 " + bedNo + " 已被占用");
        }
        this.occupied = true;
        this.occupantPatientId = patientId;
    }

    /** 释放床位 */
    public void release() {
        this.occupied = false;
        this.occupantPatientId = null;
    }

    public String getBedNo() {
        return bedNo;
    }

    public String getWardCode() {
        return wardCode;
    }

    public String getOccupantPatientId() {
        return occupantPatientId;
    }
}
3.4.2 医嘱实体
ini 复制代码
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 医嘱实体
 *
 * 医嘱自身的状态机:CREATED → DECOMPOSED → EXECUTED,STOPPED 为终止态
 * 患者状态决定"能不能开医嘱"(权限矩阵,归患者状态类守卫),
 * 医嘱状态决定"这条医嘱能不能被分解/执行"(归医嘱自己守卫)
 */
public class Order {

    public static final String TYPE_LONG_TERM = "LONG_TERM";        // 长期医嘱
    public static final String TYPE_TEMPORARY = "TEMPORARY";        // 临时医嘱
    public static final String TYPE_DISCHARGE_MED = "DISCHARGE_MED"; // 出院带药

    private String orderId;
    private String patientId;
    private String orderText;
    private String orderType;
    private String status;           // CREATED / DECOMPOSED / EXECUTED / STOPPED
    private String bedNo;            // 下单时床位快照,追溯用
    private String doctorId;
    private LocalDateTime createTime;
    private String nurseId;
    private LocalDateTime executeTime;

    public Order(String orderText, String orderType) {
        this.orderText = orderText;
        this.orderType = orderType;
    }

    private Order(String orderId, String patientId, String orderText, String orderType,
                  String status, String bedNo, String doctorId, LocalDateTime createTime,
                  String nurseId, LocalDateTime executeTime) {
        this.orderId = orderId;
        this.patientId = patientId;
        this.orderText = orderText;
        this.orderType = orderType;
        this.status = status;
        this.bedNo = bedNo;
        this.doctorId = doctorId;
        this.createTime = createTime;
        this.nurseId = nurseId;
        this.executeTime = executeTime;
    }

    /** 仓储还原专用:只回填已有数据,不重新生成医嘱编号或时间 */
    public static Order reconstitute(String orderId, String patientId, String orderText,
                                     String orderType, String status, String bedNo,
                                     String doctorId, LocalDateTime createTime,
                                     String nurseId, LocalDateTime executeTime) {
        return new Order(orderId, patientId, orderText, orderType, status, bedNo,
                doctorId, createTime, nurseId, executeTime);
    }

    /** 下达:初始化医嘱(由聚合根在开医嘱时调用) */
    void initFor(String patientId, String bedNo, String doctorId) {
        this.orderId = UUID.randomUUID().toString();
        this.patientId = patientId;
        this.bedNo = bedNo;
        this.doctorId = doctorId;
        this.status = "CREATED";
        this.createTime = LocalDateTime.now();
    }

    /** 分解:医嘱自己的守卫------只有已创建的医嘱才能分解 */
    void decompose(String nurseId) {
        if (!"CREATED".equals(status)) {
            throw new IllegalStateException("医嘱状态为【" + status + "】,无法分解,请确认医嘱已下达");
        }
        this.status = "DECOMPOSED";
        this.nurseId = nurseId;
    }

    /** 执行:医嘱自己的守卫------只有已分解的医嘱才能执行 */
    void execute(String nurseId) {
        if (!"DECOMPOSED".equals(status)) {
            throw new IllegalStateException("医嘱未分解,请先分解再执行");
        }
        this.status = "EXECUTED";
        this.nurseId = nurseId;
        this.executeTime = LocalDateTime.now();
    }

    /** 出院停止:医嘱自己决定什么情况下可被停------仅未完成(未执行、未停止)的长期医嘱 */
    void stopForDischarge() {
        if (TYPE_LONG_TERM.equals(orderType)
                && !"EXECUTED".equals(status)
                && !"STOPPED".equals(status)) {
            this.status = "STOPPED";
        }
    }

    boolean isDischargeMed() {
        return TYPE_DISCHARGE_MED.equals(orderType);
    }

    public String getOrderId() {
        return orderId;
    }

    public String getPatientId() {
        return patientId;
    }

    public String getOrderText() {
        return orderText;
    }

    public String getOrderType() {
        return orderType;
    }

    public String getStatus() {
        return status;
    }

    public String getBedNo() {
        return bedNo;
    }

    public String getDoctorId() {
        return doctorId;
    }

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public String getNurseId() {
        return nurseId;
    }

    public LocalDateTime getExecuteTime() {
        return executeTime;
    }
}
3.4.3 结算实体
arduino 复制代码
import java.math.BigDecimal;

/**
 * 结算单
 */
public class Settlement {

    private final String settlementId;
    private final String patientId;
    private final BigDecimal totalAmount;
    private final String cashierId;

    public Settlement(String settlementId, String patientId, BigDecimal totalAmount, String cashierId) {
        this.settlementId = settlementId;
        this.patientId = patientId;
        this.totalAmount = totalAmount;
        this.cashierId = cashierId;
    }

    public String getSettlementId() {
        return settlementId;
    }

    public String getPatientId() {
        return patientId;
    }

    public BigDecimal getTotalAmount() {
        return totalAmount;
    }

    public String getCashierId() {
        return cashierId;
    }
}
3.4.4 住院患者实体(聚合根)
scss 复制代码
import com.example.inpatient.domain.bed.Bed;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 住院患者聚合根(充血模型)
 *
 * 设计要点:
 * 1. 持有聚合内对象:当前床位 currentBed、医嘱集合 orders
 * 2. 状态对外隐形:只存 statusCode,状态对象由私有方法 state() 用时推导
 * 3. 对外只暴露行为入口:每个入口先经状态守卫,守卫通过后回调 doXxx()
 * 4. 业务协同发生在 doXxx() 里------全部表现为聚合内对象之间的交互,
 *    患者从不越俎代庖去改床位/医嘱的内部字段,而是调用它们的行为方法
 */
public class Patient {

    private String patientId;
    private String name;
    private String wardCode;
    private String statusCode;             // 唯一数据源:状态编码
    private LocalDateTime wardAdmissionTime;
    private Bed currentBed;                // 聚合内对象:当前床位(未分床时为 null)
    private List<Order> orders;            // 聚合内对象:医嘱集合
    private long version;                  // 乐观锁版本号

    private Patient() {
    }

    // ======================== 状态推导(私有,外部不可见) ========================

    /** 状态对象只是内部的"守卫分发器",用时推导,用完即弃 */
    private PatientState state() {
        return PatientStateEnum.of(statusCode).state();
    }

    // ======================== 创建与还原 ========================

    /** 新患者建档:初始状态由聚合根自己决定 */
    public static Patient register(String patientId, String name) {
        Patient p = new Patient();
        p.patientId = patientId;
        p.name = name;
        p.statusCode = PatientStateEnum.REGISTERED.getCode();
        p.orders = new ArrayList<>();
        return p;
    }

    /** 仓储还原专用:只回填数据与状态编码,状态对象下次用时自动推导 */
    public static Patient reconstitute(String patientId, String name, String wardCode,
                                       String statusCode, LocalDateTime wardAdmissionTime,
                                       Bed currentBed, List<Order> orders, long version) {
        Patient p = new Patient();
        p.patientId = patientId;
        p.name = name;
        p.wardCode = wardCode;
        p.statusCode = statusCode;
        p.wardAdmissionTime = wardAdmissionTime;
        p.currentBed = currentBed;
        p.orders = orders == null ? new ArrayList<>() : new ArrayList<>(orders);
        p.version = version;
        return p;
    }

    // ======================== 行为入口(对外暴露,守卫在前) ========================

    public void admitToWard(String wardCode) {
        state().admitToWard(this, wardCode);
    }

    public void assignBed(Bed bed, String operatorId) {
        state().assignBed(this, bed, operatorId);
    }

    public void createOrder(Order order, String doctorId) {
        state().createOrder(this, order, doctorId);
    }

    public void decomposeOrder(String orderId, String nurseId) {
        state().decomposeOrder(this, orderId, nurseId);
    }

    public void executeOrder(String orderId, String nurseId) {
        state().executeOrder(this, orderId, nurseId);
    }

    public void applyDischarge(String advice, String doctorId) {
        state().applyDischarge(this, advice, doctorId);
    }

    public void settle(Settlement settlement, String cashierId) {
        state().settle(this, settlement, cashierId);
    }

    // ==================== 业务行为(由状态类回调) ====================
    // 注意这里的每一个方法:业务协同全部表现为对象间的行为调用,
    // 而不是患者去 set 床位/医嘱的内部字段

    /** 入科:登记 → 待分床 */
    void doAdmitToWard(String wardCode) {
        this.wardCode = wardCode;
        this.wardAdmissionTime = LocalDateTime.now();
        this.statusCode = PatientStateEnum.ADMITTED.getCode();
    }

    /** 分床:待分床 → 在床。患者与床位的协作:患者入住,床位占用 */
    void doAssignBed(Bed bed, String operatorId) {
        bed.occupy(this.patientId);                    // ← 对象间交互:床位自己完成占用
        this.currentBed = bed;
        this.statusCode = PatientStateEnum.IN_BED.getCode();
    }

    /** 换床:状态不变。三方协作:旧床释放、新床占用、患者更换指向 */
    void doChangeBed(Bed newBed, String operatorId) {
        this.currentBed.release();                     // ← 旧床位自己完成释放
        newBed.occupy(this.patientId);                 // ← 新床位自己完成占用
        this.currentBed = newBed;
    }

    /** 下达医嘱:状态不变。医嘱的初始化由医嘱自己完成 */
    void doCreateOrder(Order order, String doctorId) {
        order.initFor(this.patientId,
                this.currentBed == null ? null : this.currentBed.getBedNo(),
                doctorId);                             // ← 医嘱自己完成建档
        this.orders.add(order);
    }

    /** 分解医嘱:找到医嘱,让医嘱自己走自己的状态机 */
    void doDecomposeOrder(String orderId, String nurseId) {
        findOrder(orderId).decompose(nurseId);         // ← 医嘱自己守卫并流转
    }

    /** 执行医嘱 */
    void doExecuteOrder(String orderId, String nurseId) {
        findOrder(orderId).execute(nurseId);           // ← 医嘱自己守卫并流转
    }

    /** 申请出院:在床 → 预出院。患者与医嘱集合的协作:每条医嘱自己判断该不该停 */
    void doApplyDischarge(String advice, String doctorId) {
        for (Order order : orders) {
            order.stopForDischarge();                  // ← 每条医嘱自己决定停不停
        }
        this.statusCode = PatientStateEnum.PRE_DISCHARGE.getCode();
    }

    /** 结算:预出院 → 已出院。出院还床:患者把床位还回去 */
    void doSettle(Settlement settlement, String cashierId) {
        if (this.currentBed != null) {
            this.currentBed.release();                 // ← 床位自己完成释放
            this.currentBed = null;
        }
        this.statusCode = PatientStateEnum.DISCHARGED.getCode();
    }

    // ======================== 查询与基础设施 ========================

    /** 在聚合内查找医嘱:医嘱不存在属于业务异常,在这里统一守卫 */
    Order findOrder(String orderId) {
        return orders.stream()
                .filter(o -> o.getOrderId().equals(orderId))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("医嘱不存在:" + orderId));
    }

    public String getPatientId() {
        return patientId;
    }

    public String getName() {
        return name;
    }

    public String getWardCode() {
        return wardCode;
    }

    /** 对外只读状态:供界面展示、查询用 */
    public String getStatusCode() {
        return statusCode;
    }

    public Bed getCurrentBed() {
        return currentBed;
    }

    public List<Order> getOrders() {
        return Collections.unmodifiableList(orders);
    }

    public long getVersion() {
        return version;
    }
}

3.5 仓储设计

仓储的全部职责是对象与数据库记录之间的相互翻译 --- 把记录还原成对象(含聚合内的床位、医嘱),把对象写回记录。

患者仓储:

arduino 复制代码
import com.example.inpatient.domain.bed.Bed;
import com.example.inpatient.domain.patient.Patient;

/**
 * 患者仓储:只做持久化
 * 职责边界:对象 ↔ 数据库记录的翻译,不含任何业务规则
 *
 * 本示例中,床位和医嘱参与患者聚合,因此加载与保存统一由患者仓储收口,
 * 不再通过第二个仓储重复保存同一对象。
 */
public interface PatientRepository {

    /** 加载聚合(含聚合内的床位、医嘱),并锁定患者记录 */
    Patient loadForUpdate(String patientId);

    /** 加载待协作的床位并加行锁,防止并发分床 */
    Bed loadBedForUpdate(String bedNo);

    /** 保存聚合(患者使用乐观锁,床位按新旧床号差异持久化) */
    void save(Patient patient);
}

患者仓储实现:

scss 复制代码
import com.example.inpatient.domain.bed.Bed;
import com.example.inpatient.domain.patient.Order;

/**
 * 患者仓储实现(伪代码)
 *
 * 注意这个类里没有任何业务判断:
 * - loadForUpdate:查患者记录、查床位、查医嘱,翻译成领域对象
 * - loadBedForUpdate:查询并锁定待分配的床位,再翻译成领域对象
 * - save:把领域对象翻译回记录,带乐观锁写库
 * "能不能分床""出院要停医嘱"这类规则不会出现在这里
 */
@Repository
public class PatientRepositoryImpl implements PatientRepository {

    @Autowired
    private PatientMapper patientMapper;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private BedMapper bedMapper;

    @Override
    public Patient loadForUpdate(String patientId) {
        PatientPO po = patientMapper.selectByIdForUpdate(patientId);
        if (po == null) {
            throw new IllegalArgumentException("患者不存在:" + patientId);
        }
        // 还原聚合内的当前床位
        Bed currentBed = null;
        if (po.getBedNo() != null) {
            BedPO bedPO = bedMapper.selectByBedNo(po.getBedNo());
            currentBed = toBed(bedPO);
        }
        // 还原聚合内的医嘱
        List<Order> orders = orderMapper.selectByPatientId(patientId)
                .stream().map(this::toOrder).collect(Collectors.toList());
        // 组装聚合根:状态对象不用管,聚合根用时自己推导
        return Patient.reconstitute(
                po.getPatientId(), po.getName(), po.getWardCode(),
                po.getStatusCode(), po.getWardAdmissionTime(),
                currentBed, orders, po.getVersion());
    }

    @Override
    public Bed loadBedForUpdate(String bedNo) {
        BedPO bedPO = bedMapper.selectByBedNoForUpdate(bedNo);
        if (bedPO == null) {
            throw new IllegalArgumentException("床位不存在:" + bedNo);
        }
        return toBed(bedPO);
    }

    @Override
    public void save(Patient patient) {
        PatientPO old = patientMapper.selectById(patient.getPatientId());
        int rows = patientMapper.updateByIdAndVersion(toPO(patient), patient.getVersion());
        if (rows == 0) {
            throw new OptimisticLockException("患者数据已被并发修改,请重试");
        }
        for (Order order : patient.getOrders()) {
            orderMapper.insertOrUpdate(toPO(order));
        }

        // 床位只在这里保存一次:床号变化时释放旧床,当前床非空时保存当前床。
        // 初次分床、换床和出院还床都由同一套差异映射覆盖。
        Bed currentBed = patient.getCurrentBed();
        String oldBedNo = old.getBedNo();
        if (oldBedNo != null
                && (currentBed == null || !oldBedNo.equals(currentBed.getBedNo()))) {
            bedMapper.release(oldBedNo);
        }
        if (currentBed != null) {
            bedMapper.updateByBedNo(toPO(currentBed));
        }
    }

    // toBed / toOrder 分别调用 Bed.reconstitute、Order.reconstitute;
    // toPO 只做字段映射,均不包含业务规则,具体代码略。
}

仓储位于领域对象与 Mapper 之间,主要负责聚合的重建与持久化,具体表读写仍由 Mapper 完成。这里不详细展开。

3.6 应用服务

DDD 应用服务与传统三层 Service 的主要区别不在于有没有实现,而在于职责:应用服务负责用例编排、事务控制和持久化协调,组织领域对象完成业务并调用仓储保存聚合,但不承载具体领域规则。

typescript 复制代码
import com.example.inpatient.domain.bed.Bed;
import com.example.inpatient.domain.patient.Order;
import com.example.inpatient.domain.patient.Settlement;
import com.example.inpatient.domain.patient.Patient;
import com.example.inpatient.repository.PatientRepository;

/**
 * 住院患者应用服务:薄事务门面
 *
 * 职责:加载协作对象 → 触发聚合根行为 → 保存
 * 守卫在状态类,业务协同在聚合内的对象之间,持久化在仓储
 */
@Service
public class PatientService {

    @Autowired
    private PatientRepository patientRepository;

    @Transactional
    public void admitToWard(String patientId, String wardCode) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        patient.admitToWard(wardCode);
        patientRepository.save(patient);
    }

    @Transactional
    public void assignBed(String patientId, String bedNo, String operatorId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        Bed bed = patientRepository.loadBedForUpdate(bedNo);      // 分床需要床位对象参与协作
        patient.assignBed(bed, operatorId);                // 业务发生在患者与床位之间
        patientRepository.save(patient);
    }

    @Transactional
    public void createOrder(String patientId, Order order, String doctorId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        patient.createOrder(order, doctorId);
        patientRepository.save(patient);
    }

    @Transactional
    public void decomposeOrder(String patientId, String orderId, String nurseId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        patient.decomposeOrder(orderId, nurseId);
        patientRepository.save(patient);
    }

    @Transactional
    public void executeOrder(String patientId, String orderId, String nurseId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        patient.executeOrder(orderId, nurseId);
        patientRepository.save(patient);
    }

    @Transactional
    public void applyDischarge(String patientId, String advice, String doctorId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        patient.applyDischarge(advice, doctorId);
        patientRepository.save(patient);
    }

    @Transactional
    public void settle(String patientId, Settlement settlement, String cashierId) {
        Patient patient = patientRepository.loadForUpdate(patientId);
        // 床位随聚合一并还原(见 PatientRepositoryImpl.loadForUpdate),
        // 出院还床由聚合根内部完成(currentBed.release()),随聚合一并保存
        patient.settle(settlement, cashierId);
        patientRepository.save(patient);
    }
}

3.7 单元测试

scss 复制代码
/**
 * 住院患者状态机单元测试(充血模型)
 * 纯内存测试:不需要 Spring、不需要数据库
 */
class PatientStateTest {

    private Patient patient;
    private Bed bed;

    @BeforeEach
    void setUp() {
        patient = Patient.register("P20260612001", "张三");
        bed = new Bed("12-03", "内科一病区");
    }

    @Test
    void 登记状态下分床被拦截() {
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> patient.assignBed(bed, "N001"));
        assertEquals("尚未入科,请先执行入科操作", e.getMessage());
        assertTrue(bed.isFree());   // 拦截干净:床位未被占用
    }

    @Test
    void 分床成功后患者与床位状态双双变化() {
        patient.admitToWard("内科一病区");
        patient.assignBed(bed, "N001");

        assertEquals("IN_BED", patient.getStatusCode());
        assertFalse(bed.isFree());                          // 床位被占用
        assertEquals("P20260612001", bed.getOccupantPatientId());
        assertSame(bed, patient.getCurrentBed());           // 患者指向该床
    }

    @Test
    void 换床时旧床释放新床占用() {
        patient.admitToWard("内科一病区");
        patient.assignBed(bed, "N001");

        Bed newBed = new Bed("12-05", "内科一病区");
        patient.assignBed(newBed, "N001");                  // 在床状态 = 换床

        assertTrue(bed.isFree());                           // 旧床已释放
        assertFalse(newBed.isFree());                       // 新床被占用
        assertEquals("12-05", patient.getCurrentBed().getBedNo());
        assertEquals("IN_BED", patient.getStatusCode());    // 换床不改变患者状态
    }

    @Test
    void 已占用的床位不能再分配() {
        patient.admitToWard("内科一病区");
        bed.occupy("P_ANOTHER");                            // 床已被别人占了

        assertThrows(IllegalStateException.class,
                () -> patient.assignBed(bed, "N001"));
    }

    @Test
    void 申请出院后未完成的长期医嘱自动停止() {
        toInBed();
        Order done = longTermOrder("0.9%氯化钠注射液 250ml 静滴 bid");
        patient.createOrder(done, "D001");
        patient.decomposeOrder(done.getOrderId(), "N001");
        patient.executeOrder(done.getOrderId(), "N001");
        Order running = longTermOrder("阿司匹林肠溶片 100mg qd");
        patient.createOrder(running, "D001");

        patient.applyDischarge("病情好转,准予出院", "D001");

        assertEquals("PRE_DISCHARGE", patient.getStatusCode());
        assertEquals("EXECUTED", done.getStatus());     // 已执行的不受影响
        assertEquals("STOPPED", running.getStatus());   // 未完成的被医嘱自己停掉
    }

    @Test
    void 预出院状态只能开具出院带药() {
        toPreDischarge();

        assertThrows(IllegalStateException.class,
                () -> patient.createOrder(new Order("血常规", Order.TYPE_TEMPORARY), "D001"));
        assertDoesNotThrow(() ->
                patient.createOrder(new Order("布洛芬缓释胶囊 0.3g bid", Order.TYPE_DISCHARGE_MED), "D001"));
    }

    @Test
    void 医嘱自身状态机由医嘱守护() {
        toInBed();
        Order order = longTermOrder("血常规");
        patient.createOrder(order, "D001");

        // 未分解直接执行 → 医嘱自己的守卫拦截
        assertThrows(IllegalStateException.class,
                () -> patient.executeOrder(order.getOrderId(), "N001"));
    }

    @Test
    void 结算后释放床位并进入终态() {
        toPreDischarge();
        Bed occupiedBed = patient.getCurrentBed();

        patient.settle(new Settlement("S001", patient.getPatientId(),
                new BigDecimal("8530.50"), "C001"), "C001");

        assertEquals("DISCHARGED", patient.getStatusCode());
        assertTrue(occupiedBed.isFree());               // 出院还床
        assertNull(patient.getCurrentBed());

        // 终态全拦截
        assertThrows(IllegalStateException.class,
                () -> patient.createOrder(longTermOrder("血常规"), "D001"));
        assertThrows(IllegalStateException.class,
                () -> patient.assignBed(new Bed("12-04", "内科一病区"), "N001"));
    }

    // ======================== 辅助方法 ========================

    private void toInBed() {
        patient.admitToWard("内科一病区");
        patient.assignBed(bed, "N001");
    }

    private void toPreDischarge() {
        toInBed();
        patient.applyDischarge("病情好转,准予出院", "D001");
    }

    private Order longTermOrder(String text) {
        return new Order(text, Order.TYPE_LONG_TERM);
    }
}

3.8 总结

充血模型的"血",就是数据与行为的内聚,以及对象之间的行为交互。这些调用让业务规则长在数据的家里:谁的数据由谁负责,对象之间通过行为方法协作。状态模式负责在交互发生之前站岗,仓储负责在交互结束之后记账,各安其位。

充血模型和贫血模型的争论已持续多年,这里不评价两者的优劣,仅提供状态模式的两种实现方式。读者可以结合各自项目的技术架构参考这些示例。


四、总结

演进回顾

  1. 裸 if-else 阶段 :状态判断散落在每个 Service 方法中,转换规则不集中,statusCode 可以被任意修改;
  2. 贫血模型实现状态模式:状态接口引入实体参数,守卫通过后回调业务行为,统一入口将"守卫 → 执行"串成固定流程,需要读取实体数据的守卫也收进状态类,Service 变薄;
  3. 充血模型实现状态模式 :聚合根只存状态编码、用时推导状态对象,状态类与聚合根同包,使 doXxx() 可以保持包级可见,状态写入路径全部收拢------状态变更只剩"行为驱动"一条路。

与经典定义的对照

以下角色对照以充血模型实现为例。

经典角色 本文实现 说明
Context Patient 聚合根 只存状态编码,私有方法推导状态对象,对外只暴露行为入口
State PatientState 接口 声明所有依赖状态的行为,默认实现即拦截
ConcreteState RegisteredState、InBedState 等 5 个状态类 守卫 + 回调,无状态、单例共享
状态迁移 doXxx() 内修改 statusCode 迁移是业务行为的最后一步,永远由行为驱动
状态查找 PatientStateEnum 持有状态单例 编码 → 状态对象的映射长在枚举上,无独立工厂

注意事项

  • 状态类必须无状态:所有数据都在聚合根里,状态类只做守卫和回调,这样才能全局单例共享;状态类一旦持有患者数据,共享实例就会串数据;
  • 守卫和业务的分界要守住 :状态类只做"能不能做"的判断(包括需要实体数据的判断),"怎么做"(改哪些数据、联动哪些对象、迁移到哪)全部在 doXxx() 里,状态类一旦开始执行业务修改,就会模糊守卫与业务执行的边界;
  • 状态只留一个数据源 :不要同时存 statusCode 和状态对象两份,用时推导最省事,也天然免疫"改了编码忘了换对象"的 bug;
  • 守卫需要外部数据怎么办:如果某守卫依赖外部服务(如"申请出院前查欠费"),不需要让状态类直接注入外部服务。可以让 Service 先查询,再把结果作为参数传入守卫方法,状态类仍然保持无状态,也仍然可以由枚举持有;
  • 并发控制不能省:状态流转需要结合业务并发和基础设施选择数据库行锁、乐观锁或 Redis 分布式锁。Redis 锁还需要妥善处理过期、续期、锁归属和原子释放,不能简单视为比数据库锁更安全;
  • 其他状态实现方案:Spring Statemachine 这类引擎也能实现状态管理。建议先按设计模式方案实现,当状态流转复杂到需要可视化、持久化或运行时配置时,再考虑引入工具,这也符合架构演进的思路。
相关推荐
卷无止境2 小时前
给AI装上一双眼睛:快速构建代码验证器的思路与落地方案
后端·python
学长毕设2 小时前
基于SpringBoot的餐厅管理系统-附源码
java·spring boot·后端
ZWZhangYu3 小时前
从 Dify 实践理解大模型上下文缓存
java·后端·spring
考虑考虑4 小时前
瀚高数据库
数据库·后端·postgresql
锋行天下5 小时前
DeepSeek写Langgraph就是个大傻逼,一本正经胡说八道
前端·人工智能·后端
小小张说故事7 小时前
Python 的 str 为什么设计成不可变?拆开字符串驻留、字符串池,以及 += 拼接到底慢在哪
后端·python
AI架构师之家7 小时前
我把 Hermes 这匹"马"养了两周,它干了一件让我有点发毛的事
后端
dpharness7 小时前
块协议用了两周,我把九个坑按顺序记下来了
后端
加宝的起点雷达7 小时前
跑了两周的信号日报,我把降级链改了四次
人工智能·后端