C++处方管理系统重构:状态、策略、建造者与观察者模式实战

1. 项目概述:当C++遇见处方管理

最近在复盘一个老项目,一个用C++写的处方管理系统。这听起来可能有点“复古”,毕竟现在很多新项目都奔着Java、Go或者Python去了。但恰恰是这种对性能、资源控制和确定性有严苛要求的遗留系统或特定领域(比如嵌入式医疗设备、本地化高并发服务),C++依然是无可替代的选择。这个项目不是一个简单的增删改查,它需要处理处方的创建、审核、发药、退药、统计查询等一系列复杂且状态严谨的业务流程,同时还要保证高可靠性和数据一致性。最初版本的代码,就像很多急于上线的项目一样,充满了“面条式”的代码和四处散落的if-else,维护和扩展成了噩梦。

重构的核心思路,就是引入设计模式来梳理这团乱麻。但设计模式不是银弹,更不是用来炫技的装饰品。它的价值在于,针对特定场景下的特定设计问题,提供经过验证的优雅解决方案。这次重构,本质上是一次对业务逻辑进行“领域建模”和“职责分离”的过程,而设计模式是我们实现这些设计原则的得力工具。如果你也在用C++处理类似的复杂业务系统,或者对如何将经典的设计模式落地到实际项目中感到困惑,那么这次关于处方管理系统架构的拆解,或许能给你带来一些直接的参考。

2. 核心业务场景与设计挑战分析

在深入代码之前,我们必须先把业务场景和随之而来的设计挑战搞清楚。处方管理系统,虽然领域垂直,但其内部的核心矛盾非常典型。

2.1 处方生命周期的状态复杂性

一张处方从医生开具到最终完成,会经历多个状态:草稿->待审核->已审核->待发药->部分发药->已发药->已退药->作废。每个状态下的可执行操作(如修改、提交审核、发药、退药)和后续状态转移路径都是不同的。最初我们用一个枚举PrescriptionStatus和一堆散落在各个业务函数里的switch-caseif-else来判断,代码迅速膨胀,添加一个新状态或修改一个转移规则都如履薄冰,生怕影响到其他状态。

设计挑战:如何清晰地管理一个对象的状态,使其状态转换逻辑既明确又可扩展,并且将特定状态下的行为封装起来?

2.2 药品条目计算的多样性

处方由多个药品条目组成。计算单个条目的金额时,逻辑就可能很复杂:普通药品直接“单价×数量”;有些药品需要根据患者体重或体表面积计算剂量,再换算成数量和金额;还有的药品存在“打包价”,比如“买三送一”或“大包装优惠”。未来还可能增加更多的计价策略。如果把这些计算逻辑硬编码在PrescriptionItem类的calculateCost()方法里,这个方法很快就会变成一个充斥着条件判断的“怪物”。

设计挑战:如何定义一系列算法(计价策略),使它们可以相互替换,并且算法的变化独立于使用它的客户端(药品条目)?

2.3 处方创建过程的灵活性

创建一张处方,并不是简单new一个对象然后设值。对于普通门诊处方、急诊处方、麻精药品处方,其需要校验的规则、需要填写的附加信息、甚至后续的流程都可能不同。我们是否需要一个统一的接口来创建“产品”(处方),但又允许其子类决定实例化哪个具体的产品类?更进一步,如果创建过程本身很复杂(例如需要按顺序关联患者、医生、药品、医保信息等),我们如何确保这个创建过程稳定,且易于构造不同的“产品”变体?

设计挑战:如何提供一个创建对象的接口,但将实际创建的逻辑延迟到子类或独立的对象中?如何构建一个复杂的对象,使其构建过程与表示分离?

2.4 全局访问点与业务通知

系统中很多地方都需要获取当前登录的用户信息、系统配置(如是否开启审核强制流程)。同时,当处方状态发生关键变化(如审核通过、发药完成)时,需要通知药房窗口、更新库存、记录审计日志,甚至触发短信提醒。如果让处方对象直接依赖这些具体的服务(如InventoryService,SmsService),会导致处方类与众多不相关的子系统紧耦合,难以测试和复用。

设计挑战:如何让一个对象在需要时能方便地访问一个通用的、全局的服务实例?如何让一个对象的状态变更能够自动通知其他多个对象,而又不需要知道这些对象的具体细节?

3. 设计模式选型与架构落地

针对上述挑战,我们系统地引入了相应的设计模式。选择模式的首要原则是“按需索取”,解决具体问题,而不是为了用模式而用模式。

3.1 状态模式:梳理处方生命周期

这是解决状态复杂性的利器。我们不再使用简单的枚举和分散的条件判断。

实现方式

  1. 定义一个抽象状态接口PrescriptionState,声明所有可能的状态行为方法,如submitForReview(),approve(),dispense()等。
  2. 为每个具体状态创建一个类,实现PrescriptionState接口。例如DraftState,PendingReviewState,ApprovedState。每个状态类只负责处理在该状态下允许的操作,并决定操作后处方应转移到哪个新状态。
  3. 在处方类Prescription中,持有一个指向当前状态对象的指针(std::unique_ptr<PrescriptionState>)。处方将所有状态相关的行为委托给当前状态对象去执行。
// 状态接口 class PrescriptionState { public: virtual ~PrescriptionState() = default; virtual void submit(Prescription* context) = 0; virtual void approve(Prescription* context) = 0; virtual void reject(Prescription* context, const std::string& reason) = 0; virtual void dispense(Prescription* context) = 0; virtual std::string getName() const = 0; }; // 具体状态:“待审核” class PendingReviewState : public PrescriptionState { public: void submit(Prescription* context) override { // 在待审核状态下,提交操作无效或抛出异常 throw std::logic_error("处方已在审核中,无法重复提交。"); } void approve(Prescription* context) override { // 执行审核通过的业务逻辑... // 然后改变上下文(处方)的状态 context->changeState(std::make_unique<ApprovedState>()); // 触发相关事件,如通知药房... } void reject(Prescription* context, const std::string& reason) override { // 执行审核拒绝的逻辑... context->changeState(std::make_unique<DraftState>()); context->setRejectionReason(reason); } void dispense(Prescription* context) override { throw std::logic_error("处方未审核通过,不能发药。"); } std::string getName() const override { return "PendingReview"; } }; // 处方类(上下文) class Prescription { private: std::unique_ptr<PrescriptionState> state_; // ... 其他成员,如ID、患者信息、药品列表等 public: Prescription() : state_(std::make_unique<DraftState>()) {} void changeState(std::unique_ptr<PrescriptionState> newState) { state_ = std::move(newState); // 状态变更时,可以在这里记录日志或触发事件 } // 将行为委托给当前状态对象 void submit() { state_->submit(this); } void approve() { state_->approve(this); } void reject(const std::string& reason) { state_->reject(this, reason); } void dispense() { state_->dispense(this); } std::string getCurrentStateName() const { return state_->getName(); } };

实操心得与避坑指南

  • 状态转移的维护:所有状态转移逻辑现在都封装在各个具体的状态类中。新增一个状态,只需新增一个类并实现其行为。修改某个状态下的转移规则,也只需修改对应类的代码,符合“开闭原则”。
  • 上下文(Context)的传递:状态对象通常需要回调上下文(处方对象)来修改其状态或访问其数据。我们通过将Prescription*指针传递给状态类的方法来实现。要小心处理指针的生命周期和空指针问题。
  • 与持久化的结合:将处方保存到数据库时,我们只需保存一个代表状态的字符串(如“Approved”)。从数据库加载时,根据这个字符串创建对应的具体状态对象。可以配合简单工厂模式来根据状态名创建状态对象。
  • 避免“上帝状态”:不要让一个状态类知道太多其他状态类的细节。状态转移时,应通过上下文进行,或者依赖一个集中的状态管理器(但通常不需要这么复杂)。

3.2 策略模式:封装多变的计价算法

针对药品计价规则的多样性,策略模式是自然的选择。

实现方式

  1. 定义策略接口PricingStrategy,声明一个calculateCost(double unitPrice, int quantity, const PatientInfo& patient)之类的计算方法。
  2. 实现不同的具体策略类,如StandardPricingStrategy(标准计价)、WeightBasedPricingStrategy(按体重计价)、BundlePricingStrategy(打包计价)。
  3. 在药品条目类PrescriptionItem中,持有一个PricingStrategy的指针(或std::function)。计价时,委托给策略对象。
// 策略接口 class PricingStrategy { public: virtual ~PricingStrategy() = default; virtual double calculate(double unitPrice, double quantity, const PatientInfo* patient = nullptr) const = 0; }; // 具体策略:标准计价 class StandardPricingStrategy : public PricingStrategy { public: double calculate(double unitPrice, double quantity, const PatientInfo*) const override { return unitPrice * quantity; } }; // 具体策略:按体重计价 class WeightBasedPricingStrategy : public PricingStrategy { private: double dosePerKg_; // 每公斤体重剂量(mg/kg) double weightInKg_; // 患者体重(kg) public: WeightBasedPricingStrategy(double dosePerKg, double weightInKg) : dosePerKg_(dosePerKg), weightInKg_(weightInKg) {} double calculate(double unitPrice, double, const PatientInfo*) const override { double totalDoseMg = dosePerKg_ * weightInKg_; // 假设药品规格是每片X mg,计算需要多少片 double tabletsNeeded = totalDoseMg / 500.0; // 假设每片500mg return unitPrice * tabletsNeeded; } }; // 药品条目类(上下文) class PrescriptionItem { private: std::unique_ptr<PricingStrategy> pricingStrategy_; double unitPrice_; double quantity_; public: PrescriptionItem(std::unique_ptr<PricingStrategy> strategy, double price, double qty) : pricingStrategy_(std::move(strategy)), unitPrice_(price), quantity_(qty) {} void setPricingStrategy(std::unique_ptr<PricingStrategy> strategy) { pricingStrategy_ = std::move(strategy); } double calculateCost(const PatientInfo* patient = nullptr) const { if (!pricingStrategy_) { throw std::runtime_error("计价策略未设置。"); } return pricingStrategy_->calculate(unitPrice_, quantity_, patient); } };

注意事项

  • 策略的无状态与有状态:像StandardPricingStrategy可以是无状态的,实现为单例即可。而WeightBasedPricingStrategy需要患者体重等参数,是有状态的。需要根据情况设计策略对象的构造和持有方式。
  • 策略的创建:策略对象可以在创建药品条目时根据药品类型动态创建并注入。这可以结合工厂模式依赖注入容器来完成,使得业务代码不依赖具体的策略类。
  • 与配置的结合:计价策略的类型和参数(如dosePerKg)可以存储在数据库或配置文件中,实现运行时动态配置,极大提升了系统的灵活性。

3.3 建造者模式:构造复杂处方对象

对于需要多步骤、按顺序构建的复杂处方对象(尤其是麻精药品处方需要大量附加信息和校验),我们采用了建造者模式。

实现方式

  1. 定义一个PrescriptionBuilder抽象类或接口,声明构建处方各个部分的步骤方法,如setPatientInfo(),addMedicineItem(),setPriority(),validate()等,以及一个getResult()方法返回构建好的处方。
  2. 实现具体的建造者,如OutpatientPrescriptionBuilder,EmergencyPrescriptionBuilder,ControlledDrugPrescriptionBuilder。每个建造者知道如何构建和校验特定类型的处方。
  3. (可选)引入一个Director(指导者)类,它定义构建的固定流程(例如,必须先设置患者,再添加药品,最后进行医保核算)。Director接收一个Builder,并按顺序调用其方法。
// 产品:处方 class Prescription { // ... 复杂的内部结构 public: // 可能有很多setter,但构造函数很复杂 }; // 建造者接口 class PrescriptionBuilder { public: virtual ~PrescriptionBuilder() = default; virtual void reset() = 0; virtual void setBasicInfo(int patientId, int doctorId) = 0; virtual void addItem(int medicineId, double quantity) = 0; virtual void setEmergencyFlag(bool isEmergency) = 0; virtual void setControlledDrugInfo(const std::string& license) = 0; // 麻精药特有 virtual void validate() = 0; // 执行类型特定的校验 virtual std::unique_ptr<Prescription> getResult() = 0; }; // 具体建造者:麻精药品处方建造者 class ControlledDrugPrescriptionBuilder : public PrescriptionBuilder { private: std::unique_ptr<Prescription> prescription_; public: ControlledDrugPrescriptionBuilder() { reset(); } void reset() override { prescription_ = std::make_unique<Prescription>(); // 初始化一些麻精药处方特有的默认值 } void setBasicInfo(int patientId, int doctorId) override { prescription_->setPatientId(patientId); prescription_->setDoctorId(doctorId); // 可能需要额外检查医生是否有麻精药处方权 } void addItem(int medicineId, double quantity) override { // 1. 检查药品ID是否为麻精药品 // 2. 检查库存是否在特殊管制库位 // 3. 调用处方对象的内部方法添加条目 prescription_->internalAddItem(medicineId, quantity); } void setEmergencyFlag(bool) override { // 麻精药处方可能不允许设置为急诊,或忽略此标志 } void setControlledDrugInfo(const std::string& license) override { // 设置麻精药处方特有的信息,如处方编号、医师授权号等 prescription_->setControlledDrugLicense(license); } void validate() override { // 执行麻精药品处方的严格校验 // 例如:总量限制、医师资质、患者历史用药核查等 if (prescription_->getControlledDrugLicense().empty()) { throw std::invalid_argument("麻精药品处方必须提供授权许可证号。"); } // ... 更多校验 prescription_->internalFinalize(); // 处方内部最终处理 } std::unique_ptr<Prescription> getResult() override { auto result = std::move(prescription_); reset(); // 为下一次构建准备 return result; } }; // 使用示例 void createControlledDrugPrescription() { ControlledDrugPrescriptionBuilder builder; // 可以有一个Director来指导流程,也可以客户端直接控制 builder.setBasicInfo(1001, 2001); builder.setControlledDrugInfo("CD_LICENSE_2023_001"); builder.addItem(3001, 1.0); // 麻精药品ID builder.addItem(3002, 2.0); try { builder.validate(); std::unique_ptr<Prescription> rx = builder.getResult(); // 保存或处理处方... } catch (const std::exception& e) { std::cerr << "创建处方失败: " << e.what() << std::endl; } }

核心优势

  • 分离构建与表示:客户端代码不再需要知道处方对象复杂的内部结构和构建顺序。构建逻辑被封装在建造者中。
  • 精细控制构建过程:可以分步骤构建,允许在最终获取产品前进行复杂的校验和初始化。
  • 复用相同的构建代码:不同的Director可以复用相同的Builder来创建不同流程变体的产品。

3.4 单例与观察者模式:管理全局服务与解耦通知

对于全局的配置管理、日志服务等,我们使用了单例模式,但采用了“Meyers‘ Singleton”这种线程安全的懒汉式实现,避免双重检查锁定等复杂问题。

class ConfigManager { private: ConfigManager() = default; // 私有构造函数 ~ConfigManager() = default; // 禁止拷贝和赋值 ConfigManager(const ConfigManager&) = delete; ConfigManager& operator=(const ConfigManager&) = delete; std::unordered_map<std::string, std::string> configs_; public: static ConfigManager& getInstance() { static ConfigManager instance; // C++11保证静态局部变量初始化线程安全 return instance; } void setConfig(const std::string& key, const std::string& value) { configs_[key] = value; } std::string getConfig(const std::string& key, const std::string& defaultValue = "") const { auto it = configs_.find(key); return it != configs_.end() ? it->second : defaultValue; } };

对于处方状态变更需要通知多个子系统的问题,我们采用了观察者模式。处方作为被观察者(Subject),药房库存服务、审计日志服务、消息推送服务等作为观察者(Observer)。

实现要点

  1. 定义Observer接口,通常包含一个update(const Prescription& rx)方法。
  2. 处方类维护一个Observer的列表(如std::vector<std::weak_ptr<Observer>>)。
  3. 当处方状态发生重要变化时(如在changeState方法内),遍历观察者列表,调用每个观察者的update方法。
  4. 使用weak_ptr避免循环引用导致的内存泄漏。
// 观察者接口 class PrescriptionObserver { public: virtual ~PrescriptionObserver() = default; virtual void onPrescriptionStatusChanged(const Prescription& rx) = 0; }; // 被观察者(处方)需要添加管理观察者的能力 class Prescription { // ... 其他成员 private: std::vector<std::weak_ptr<PrescriptionObserver>> observers_; std::mutex observersMutex_; // 多线程环境下需要保护 public: void attach(std::weak_ptr<PrescriptionObserver> observer) { std::lock_guard<std::mutex> lock(observersMutex_); observers_.push_back(observer); } void detach(const std::shared_ptr<PrescriptionObserver>& observer) { std::lock_guard<std::mutex> lock(observersMutex_); observers_.erase( std::remove_if(observers_.begin(), observers_.end(), [&observer](const std::weak_ptr<PrescriptionObserver>& wp) { auto sp = wp.lock(); return sp && sp == observer; }), observers_.end()); } protected: void notifyStatusChanged() { std::vector<std::weak_ptr<PrescriptionObserver>> observersCopy; { std::lock_guard<std::mutex> lock(observersMutex_); observersCopy = observers_; } for (auto& weakObs : observersCopy) { if (auto obs = weakObs.lock()) { obs->onPrescriptionStatusChanged(*this); } } } // 在 changeState 方法末尾调用 notifyStatusChanged void changeState(std::unique_ptr<PrescriptionState> newState) { state_ = std::move(newState); notifyStatusChanged(); // 关键:状态改变后通知所有观察者 } }; // 具体观察者:库存服务 class InventoryService : public PrescriptionObserver, public std::enable_shared_from_this<InventoryService> { public: void onPrescriptionStatusChanged(const Prescription& rx) override { if (rx.getCurrentStateName() == "Approved") { // 处方审核通过,预扣库存 deductStockForPrescription(rx); } else if (rx.getCurrentStateName() == "Cancelled") { // 处方作废,恢复库存 restoreStockForPrescription(rx); } } private: void deductStockForPrescription(const Prescription& rx) { /* ... */ } void restoreStockForPrescription(const Prescription& rx) { /* ... */ } };

重要提醒:单例模式要慎用,它本质上是一种全局变量,会带来隐藏的耦合和测试困难。仅在确有必要(如真正的全局唯一资源管理器)时才使用。观察者模式要注意性能,如果观察者很多或update操作很重,可能需要异步通知。

4. 架构整合与性能考量

将上述模式组合起来,就形成了我们处方管理系统的核心业务层架构。Prescription作为聚合根,内部通过状态模式管理生命周期,通过组合策略模式处理药品计价,在构建时可以通过建造者模式灵活创建。外部的全局服务和业务通知,则通过单例(谨慎使用)和观察者模式进行解耦。

关于C++实现的性能考量

  1. 内存管理:大量使用std::unique_ptrstd::shared_ptr/std::weak_ptr来明确所有权和生命周期,避免内存泄漏。对于有性能要求的场景,可以考虑使用对象池或自定义分配器来管理状态对象、策略对象等小对象。
  2. 多线程安全:处方对象可能被多个线程操作(如同时发药和退药)。我们在状态模式的关键方法(如submit,approve)和观察者列表操作中加入了轻量级锁(如std::mutex)。更精细的并发控制可以考虑读写锁或无锁数据结构,但复杂度会显著增加。
  3. 虚函数开销:状态模式和策略模式都依赖虚函数调用。这在大多数业务场景下开销可以忽略不计。如果经过性能分析发现这是热点,可以考虑使用std::variant和访问者模式等基于类型擦除的替代方案,但这会牺牲一部分清晰度。
  4. 序列化:将包含多态对象(如状态、策略)的处方序列化到数据库或网络时,需要处理类型信息。我们为每个具体状态/策略类分配一个唯一的类型ID,序列化时保存ID和必要数据;反序列化时,根据ID工厂创建对应的对象。

5. 调试与问题排查实录

在重构和后续维护中,我们遇到并解决了一些典型问题。

问题1:状态转移出现死循环或意外状态。

  • 现象:处方从“已发药”状态又回到了“待审核”状态。
  • 排查:检查DispensedStateapprove()submit()方法实现。发现DispensedState::approve()方法错误地调用了context->changeState(std::make_unique<PendingReviewState>())
  • 解决:在DispensedState中,所有可能导致状态回退的方法(如approve,submit)都应抛出异常或设置为空操作,因为“已发药”是终态之一。教训:为每个状态类编写单元测试,覆盖所有可能的方法调用,确保其行为符合业务规则。

问题2:观察者通知导致性能瓶颈。

  • 现象:批量审核1000张处方时,系统响应变慢。
  • 排查:使用性能分析工具,发现大量时间花在notifyStatusChanged上,某个观察者(如一个写远程日志的服务)的update方法同步阻塞且耗时很长。
  • 解决
    • 异步化:将通知改为异步任务,放入线程池队列中执行,不阻塞主业务线程。
    • 去重:在短时间内同一处方的多次状态变化,可以合并为一次通知。
    • 选择性通知:并非所有状态变化都需要通知所有观察者。可以为观察者订阅特定的事件类型。
    • 使用更高效的消息队列:对于跨进程或系统的通知,考虑集成专业的消息中间件。

问题3:建造者模式中,校验逻辑分散且重复。

  • 现象EmergencyPrescriptionBuilderControlledDrugPrescriptionBuilder中有部分相同的校验逻辑(如患者ID有效性)。
  • 解决:提取公共的校验步骤到父类BasePrescriptionBuilder中,或者使用模板方法模式定义构建的骨架,将公共校验作为骨架中的一个步骤,子类重写特定的校验步骤。

问题4:策略对象创建依赖外部参数,导致客户端代码复杂。

  • 现象:创建WeightBasedPricingStrategy需要患者体重,而体重信息在创建处方条目时可能不易获取。
  • 解决:引入抽象工厂模式依赖注入框架来负责策略对象的组装。或者,将策略设计为无状态的,所需参数通过calculate方法的参数传入,而不是在构造函数中传入。这样策略对象就可以被复用。

重构后的系统,代码结构清晰,职责分明,极大地提升了可维护性和可扩展性。当需要新增一种处方类型或计价规则时,基本上只需要添加新的类,而无需修改大量现有代码。这正体现了运用设计模式的最终目的:管理复杂度,应对变化。