C++处方管理系统重构:状态、策略、建造者与观察者模式实战 1. 项目概述当C遇见处方管理最近在复盘一个老项目一个用C写的处方管理系统。这听起来可能有点“复古”毕竟现在很多新项目都奔着Java、Go或者Python去了。但恰恰是这种对性能、资源控制和确定性有严苛要求的遗留系统或特定领域比如嵌入式医疗设备、本地化高并发服务C依然是无可替代的选择。这个项目不是一个简单的增删改查它需要处理处方的创建、审核、发药、退药、统计查询等一系列复杂且状态严谨的业务流程同时还要保证高可靠性和数据一致性。最初版本的代码就像很多急于上线的项目一样充满了“面条式”的代码和四处散落的if-else维护和扩展成了噩梦。重构的核心思路就是引入设计模式来梳理这团乱麻。但设计模式不是银弹更不是用来炫技的装饰品。它的价值在于针对特定场景下的特定设计问题提供经过验证的优雅解决方案。这次重构本质上是一次对业务逻辑进行“领域建模”和“职责分离”的过程而设计模式是我们实现这些设计原则的得力工具。如果你也在用C处理类似的复杂业务系统或者对如何将经典的设计模式落地到实际项目中感到困惑那么这次关于处方管理系统架构的拆解或许能给你带来一些直接的参考。2. 核心业务场景与设计挑战分析在深入代码之前我们必须先把业务场景和随之而来的设计挑战搞清楚。处方管理系统虽然领域垂直但其内部的核心矛盾非常典型。2.1 处方生命周期的状态复杂性一张处方从医生开具到最终完成会经历多个状态草稿-待审核-已审核-待发药-部分发药-已发药-已退药-作废。每个状态下的可执行操作如修改、提交审核、发药、退药和后续状态转移路径都是不同的。最初我们用一个枚举PrescriptionStatus和一堆散落在各个业务函数里的switch-case或if-else来判断代码迅速膨胀添加一个新状态或修改一个转移规则都如履薄冰生怕影响到其他状态。设计挑战如何清晰地管理一个对象的状态使其状态转换逻辑既明确又可扩展并且将特定状态下的行为封装起来2.2 药品条目计算的多样性处方由多个药品条目组成。计算单个条目的金额时逻辑就可能很复杂普通药品直接“单价×数量”有些药品需要根据患者体重或体表面积计算剂量再换算成数量和金额还有的药品存在“打包价”比如“买三送一”或“大包装优惠”。未来还可能增加更多的计价策略。如果把这些计算逻辑硬编码在PrescriptionItem类的calculateCost()方法里这个方法很快就会变成一个充斥着条件判断的“怪物”。设计挑战如何定义一系列算法计价策略使它们可以相互替换并且算法的变化独立于使用它的客户端药品条目2.3 处方创建过程的灵活性创建一张处方并不是简单new一个对象然后设值。对于普通门诊处方、急诊处方、麻精药品处方其需要校验的规则、需要填写的附加信息、甚至后续的流程都可能不同。我们是否需要一个统一的接口来创建“产品”处方但又允许其子类决定实例化哪个具体的产品类更进一步如果创建过程本身很复杂例如需要按顺序关联患者、医生、药品、医保信息等我们如何确保这个创建过程稳定且易于构造不同的“产品”变体设计挑战如何提供一个创建对象的接口但将实际创建的逻辑延迟到子类或独立的对象中如何构建一个复杂的对象使其构建过程与表示分离2.4 全局访问点与业务通知系统中很多地方都需要获取当前登录的用户信息、系统配置如是否开启审核强制流程。同时当处方状态发生关键变化如审核通过、发药完成时需要通知药房窗口、更新库存、记录审计日志甚至触发短信提醒。如果让处方对象直接依赖这些具体的服务如InventoryService,SmsService会导致处方类与众多不相关的子系统紧耦合难以测试和复用。设计挑战如何让一个对象在需要时能方便地访问一个通用的、全局的服务实例如何让一个对象的状态变更能够自动通知其他多个对象而又不需要知道这些对象的具体细节3. 设计模式选型与架构落地针对上述挑战我们系统地引入了相应的设计模式。选择模式的首要原则是“按需索取”解决具体问题而不是为了用模式而用模式。3.1 状态模式梳理处方生命周期这是解决状态复杂性的利器。我们不再使用简单的枚举和分散的条件判断。实现方式定义一个抽象状态接口PrescriptionState声明所有可能的状态行为方法如submitForReview(),approve(),dispense()等。为每个具体状态创建一个类实现PrescriptionState接口。例如DraftState,PendingReviewState,ApprovedState。每个状态类只负责处理在该状态下允许的操作并决定操作后处方应转移到哪个新状态。在处方类Prescription中持有一个指向当前状态对象的指针std::unique_ptrPrescriptionState。处方将所有状态相关的行为委托给当前状态对象去执行。// 状态接口 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_uniqueApprovedState()); // 触发相关事件如通知药房... } void reject(Prescription* context, const std::string reason) override { // 执行审核拒绝的逻辑... context-changeState(std::make_uniqueDraftState()); context-setRejectionReason(reason); } void dispense(Prescription* context) override { throw std::logic_error(处方未审核通过不能发药。); } std::string getName() const override { return PendingReview; } }; // 处方类上下文 class Prescription { private: std::unique_ptrPrescriptionState state_; // ... 其他成员如ID、患者信息、药品列表等 public: Prescription() : state_(std::make_uniqueDraftState()) {} void changeState(std::unique_ptrPrescriptionState 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 策略模式封装多变的计价算法针对药品计价规则的多样性策略模式是自然的选择。实现方式定义策略接口PricingStrategy声明一个calculateCost(double unitPrice, int quantity, const PatientInfo patient)之类的计算方法。实现不同的具体策略类如StandardPricingStrategy标准计价、WeightBasedPricingStrategy按体重计价、BundlePricingStrategy打包计价。在药品条目类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_ptrPricingStrategy pricingStrategy_; double unitPrice_; double quantity_; public: PrescriptionItem(std::unique_ptrPricingStrategy strategy, double price, double qty) : pricingStrategy_(std::move(strategy)), unitPrice_(price), quantity_(qty) {} void setPricingStrategy(std::unique_ptrPricingStrategy 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 建造者模式构造复杂处方对象对于需要多步骤、按顺序构建的复杂处方对象尤其是麻精药品处方需要大量附加信息和校验我们采用了建造者模式。实现方式定义一个PrescriptionBuilder抽象类或接口声明构建处方各个部分的步骤方法如setPatientInfo(),addMedicineItem(),setPriority(),validate()等以及一个getResult()方法返回构建好的处方。实现具体的建造者如OutpatientPrescriptionBuilder,EmergencyPrescriptionBuilder,ControlledDrugPrescriptionBuilder。每个建造者知道如何构建和校验特定类型的处方。可选引入一个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_ptrPrescription getResult() 0; }; // 具体建造者麻精药品处方建造者 class ControlledDrugPrescriptionBuilder : public PrescriptionBuilder { private: std::unique_ptrPrescription prescription_; public: ControlledDrugPrescriptionBuilder() { reset(); } void reset() override { prescription_ std::make_uniquePrescription(); // 初始化一些麻精药处方特有的默认值 } 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_ptrPrescription 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_ptrPrescription 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_mapstd::string, std::string configs_; public: static ConfigManager getInstance() { static ConfigManager instance; // C11保证静态局部变量初始化线程安全 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。实现要点定义Observer接口通常包含一个update(const Prescription rx)方法。处方类维护一个Observer的列表如std::vectorstd::weak_ptrObserver。当处方状态发生重要变化时如在changeState方法内遍历观察者列表调用每个观察者的update方法。使用weak_ptr避免循环引用导致的内存泄漏。// 观察者接口 class PrescriptionObserver { public: virtual ~PrescriptionObserver() default; virtual void onPrescriptionStatusChanged(const Prescription rx) 0; }; // 被观察者处方需要添加管理观察者的能力 class Prescription { // ... 其他成员 private: std::vectorstd::weak_ptrPrescriptionObserver observers_; std::mutex observersMutex_; // 多线程环境下需要保护 public: void attach(std::weak_ptrPrescriptionObserver observer) { std::lock_guardstd::mutex lock(observersMutex_); observers_.push_back(observer); } void detach(const std::shared_ptrPrescriptionObserver observer) { std::lock_guardstd::mutex lock(observersMutex_); observers_.erase( std::remove_if(observers_.begin(), observers_.end(), [observer](const std::weak_ptrPrescriptionObserver wp) { auto sp wp.lock(); return sp sp observer; }), observers_.end()); } protected: void notifyStatusChanged() { std::vectorstd::weak_ptrPrescriptionObserver observersCopy; { std::lock_guardstd::mutex lock(observersMutex_); observersCopy observers_; } for (auto weakObs : observersCopy) { if (auto obs weakObs.lock()) { obs-onPrescriptionStatusChanged(*this); } } } // 在 changeState 方法末尾调用 notifyStatusChanged void changeState(std::unique_ptrPrescriptionState newState) { state_ std::move(newState); notifyStatusChanged(); // 关键状态改变后通知所有观察者 } }; // 具体观察者库存服务 class InventoryService : public PrescriptionObserver, public std::enable_shared_from_thisInventoryService { 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实现的性能考量内存管理大量使用std::unique_ptr和std::shared_ptr/std::weak_ptr来明确所有权和生命周期避免内存泄漏。对于有性能要求的场景可以考虑使用对象池或自定义分配器来管理状态对象、策略对象等小对象。多线程安全处方对象可能被多个线程操作如同时发药和退药。我们在状态模式的关键方法如submit,approve和观察者列表操作中加入了轻量级锁如std::mutex。更精细的并发控制可以考虑读写锁或无锁数据结构但复杂度会显著增加。虚函数开销状态模式和策略模式都依赖虚函数调用。这在大多数业务场景下开销可以忽略不计。如果经过性能分析发现这是热点可以考虑使用std::variant和访问者模式等基于类型擦除的替代方案但这会牺牲一部分清晰度。序列化将包含多态对象如状态、策略的处方序列化到数据库或网络时需要处理类型信息。我们为每个具体状态/策略类分配一个唯一的类型ID序列化时保存ID和必要数据反序列化时根据ID工厂创建对应的对象。5. 调试与问题排查实录在重构和后续维护中我们遇到并解决了一些典型问题。问题1状态转移出现死循环或意外状态。现象处方从“已发药”状态又回到了“待审核”状态。排查检查DispensedState的approve()或submit()方法实现。发现DispensedState::approve()方法错误地调用了context-changeState(std::make_uniquePendingReviewState())。解决在DispensedState中所有可能导致状态回退的方法如approve,submit都应抛出异常或设置为空操作因为“已发药”是终态之一。教训为每个状态类编写单元测试覆盖所有可能的方法调用确保其行为符合业务规则。问题2观察者通知导致性能瓶颈。现象批量审核1000张处方时系统响应变慢。排查使用性能分析工具发现大量时间花在notifyStatusChanged上某个观察者如一个写远程日志的服务的update方法同步阻塞且耗时很长。解决异步化将通知改为异步任务放入线程池队列中执行不阻塞主业务线程。去重在短时间内同一处方的多次状态变化可以合并为一次通知。选择性通知并非所有状态变化都需要通知所有观察者。可以为观察者订阅特定的事件类型。使用更高效的消息队列对于跨进程或系统的通知考虑集成专业的消息中间件。问题3建造者模式中校验逻辑分散且重复。现象EmergencyPrescriptionBuilder和ControlledDrugPrescriptionBuilder中有部分相同的校验逻辑如患者ID有效性。解决提取公共的校验步骤到父类BasePrescriptionBuilder中或者使用模板方法模式定义构建的骨架将公共校验作为骨架中的一个步骤子类重写特定的校验步骤。问题4策略对象创建依赖外部参数导致客户端代码复杂。现象创建WeightBasedPricingStrategy需要患者体重而体重信息在创建处方条目时可能不易获取。解决引入抽象工厂模式或依赖注入框架来负责策略对象的组装。或者将策略设计为无状态的所需参数通过calculate方法的参数传入而不是在构造函数中传入。这样策略对象就可以被复用。重构后的系统代码结构清晰职责分明极大地提升了可维护性和可扩展性。当需要新增一种处方类型或计价规则时基本上只需要添加新的类而无需修改大量现有代码。这正体现了运用设计模式的最终目的管理复杂度应对变化。