状态模式在住院患者管理中的实战应用
今天聊一聊状态模式。之前我们聊过策略模式,解决的是"同一行为有多种算法实现"的问题;状态模式解决的是另一个高频问题------对象的行为依赖于它的状态,状态一变,行为跟着变。
状态模式(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 总结
充血模型的"血",就是数据与行为的内聚,以及对象之间的行为交互。这些调用让业务规则长在数据的家里:谁的数据由谁负责,对象之间通过行为方法协作。状态模式负责在交互发生之前站岗,仓储负责在交互结束之后记账,各安其位。
充血模型和贫血模型的争论已持续多年,这里不评价两者的优劣,仅提供状态模式的两种实现方式。读者可以结合各自项目的技术架构参考这些示例。
四、总结
演进回顾
- 裸 if-else 阶段 :状态判断散落在每个 Service 方法中,转换规则不集中,
statusCode可以被任意修改; - 贫血模型实现状态模式:状态接口引入实体参数,守卫通过后回调业务行为,统一入口将"守卫 → 执行"串成固定流程,需要读取实体数据的守卫也收进状态类,Service 变薄;
- 充血模型实现状态模式 :聚合根只存状态编码、用时推导状态对象,状态类与聚合根同包,使
doXxx()可以保持包级可见,状态写入路径全部收拢------状态变更只剩"行为驱动"一条路。
与经典定义的对照
以下角色对照以充血模型实现为例。
| 经典角色 | 本文实现 | 说明 |
|---|---|---|
| Context | Patient 聚合根 | 只存状态编码,私有方法推导状态对象,对外只暴露行为入口 |
| State | PatientState 接口 | 声明所有依赖状态的行为,默认实现即拦截 |
| ConcreteState | RegisteredState、InBedState 等 5 个状态类 | 守卫 + 回调,无状态、单例共享 |
| 状态迁移 | doXxx() 内修改 statusCode |
迁移是业务行为的最后一步,永远由行为驱动 |
| 状态查找 | PatientStateEnum 持有状态单例 | 编码 → 状态对象的映射长在枚举上,无独立工厂 |
注意事项
- 状态类必须无状态:所有数据都在聚合根里,状态类只做守卫和回调,这样才能全局单例共享;状态类一旦持有患者数据,共享实例就会串数据;
- 守卫和业务的分界要守住 :状态类只做"能不能做"的判断(包括需要实体数据的判断),"怎么做"(改哪些数据、联动哪些对象、迁移到哪)全部在
doXxx()里,状态类一旦开始执行业务修改,就会模糊守卫与业务执行的边界; - 状态只留一个数据源 :不要同时存
statusCode和状态对象两份,用时推导最省事,也天然免疫"改了编码忘了换对象"的 bug; - 守卫需要外部数据怎么办:如果某守卫依赖外部服务(如"申请出院前查欠费"),不需要让状态类直接注入外部服务。可以让 Service 先查询,再把结果作为参数传入守卫方法,状态类仍然保持无状态,也仍然可以由枚举持有;
- 并发控制不能省:状态流转需要结合业务并发和基础设施选择数据库行锁、乐观锁或 Redis 分布式锁。Redis 锁还需要妥善处理过期、续期、锁归属和原子释放,不能简单视为比数据库锁更安全;
- 其他状态实现方案:Spring Statemachine 这类引擎也能实现状态管理。建议先按设计模式方案实现,当状态流转复杂到需要可视化、持久化或运行时配置时,再考虑引入工具,这也符合架构演进的思路。