C++处方管理系统重构:策略、工厂、观察者与装饰器模式实战解析

发布时间:2026/8/1 17:07:49
C++处方管理系统重构:策略、工厂、观察者与装饰器模式实战解析 1. 项目概述从业务痛点出发的架构思考最近在复盘一个之前参与的医疗信息化项目核心是重构一个老旧的处方管理系统。这个系统最初是“业务驱动”的产物功能堆砌严重代码里充满了if-else的“面条式”逻辑。每当医保政策调整、药品目录更新或者需要对接新的药房打印机时改动一处就牵一发而动全身测试和上线都让人提心吊胆。这让我深刻意识到对于C这类追求性能与稳定的系统级语言项目尤其是在医疗这种高可靠性要求的领域一个好的架构设计不是“锦上添花”而是“雪中送炭”的必需品。这次重构的核心目标就是运用经典的设计模式将系统从“能跑就行”的状态升级为“清晰、灵活、易维护”的工程化作品。处方管理系统看似核心流程简单——医生开方、药师审核、患者取药——但其背后涉及复杂的业务规则校验如药品配伍禁忌、医保限用、多变的数据源药品库、患者信息、医保接口以及多样的输出形式打印处方、电子签名、数据上报。如果没有一个清晰的架构将这些关注点分离代码很快就会变成一团乱麻。接下来我将结合这个具体的C处方管理系统项目拆解几个最关键的设计模式应用场景分享如何将它们落地以及我在实操中踩过的坑和总结的经验。2. 核心架构与设计模式选型逻辑面对一个遗留系统的重构首要任务不是直接写代码而是梳理出系统的稳定点和变化点。在处方管理系统中业务流程创建、审核、发药是相对稳定的核心骨架而业务规则如何验证处方、数据来源从哪里获取药品信息、输出行为如何展示或打印处方则是可能频繁变化的部分。我们的架构设计就是要用抽象和接口隔离这些变化让核心流程不受其干扰。2.1 为什么是这些模式在众多设计模式中我重点选择了策略模式、工厂方法模式、观察者模式和装饰器模式。这个选择不是拍脑袋决定的而是基于处方系统的具体痛点多变的校验规则药品配伍禁忌、医保政策、科室权限等校验逻辑经常变动且可能组合使用。策略模式能将每种校验算法封装成独立对象使它们可以相互替换完美匹配这个场景。复杂对象的创建一张处方单Prescription对象的构建需要组合患者信息、药品明细、医生信息、审核记录等多个部分。这些部分的创建逻辑可能依赖于数据库、缓存或外部服务。工厂方法模式可以将对象的创建延迟到子类让核心代码不依赖于具体的对象创建细节。松耦合的事件通知当处方状态发生变化如从“已开立”变为“已审核”时需要通知多个模块更新界面、写入审计日志、触发短信提醒等。观察者模式定义了对象间的一对多依赖关系让状态变更者无需关心是谁接收了通知。动态扩展对象功能处方在打印时可能需要添加不同的页眉页脚、二维码、或者按照特定模板格式化。装饰器模式允许我们在运行时动态地给一个对象添加额外的职责避免了通过继承导致的“类爆炸”。注意设计模式不是银弹滥用会导致过度设计。我们的原则是“识别变化封装变化”。如果某个逻辑在可预见的未来根本不会变那么直接用简单的实现反而更清晰。2.2 整体架构视图基于以上分析我们构建了一个分层架构表示层负责UI交互它不包含业务逻辑只负责调用业务层的接口并展示结果。业务逻辑层这是核心包含了处方管理的核心流程PrescriptionService以及通过策略模式组织的各种校验规则ValidationStrategy。数据访问层抽象了数据操作接口IPrescriptionRepository,IDrugInfoProvider底层可以是数据库、Web Service或本地文件业务层不关心具体实现。基础设施层提供如日志、配置、网络通信等跨领域服务。设计模式像胶水一样将这些层有机地连接起来。例如业务逻辑层通过工厂创建数据访问对象通过策略调用校验规则通过观察者发布状态变更事件。这样每一层的职责都变得单一而清晰。3. 核心设计模式应用场景深度解析3.1 策略模式应对纷繁复杂的处方校验规则处方校验是业务规则最密集的区域。最初的老系统代码是这样的bool validatePrescription(const Prescription rx) { // 校验1药品配伍禁忌 if (!checkDrugInteraction(rx.getDrugs())) { return false; } // 校验2医保合规性 if (!checkInsuranceCompliance(rx, currentPatient)) { return false; } // 校验3医生权限 if (!checkDoctorAuthority(rx.getDoctorId(), rx.getDrugs())) { return false; } // ... 更多校验 return true; }这种写法的弊端显而易见每增加一种校验比如“特殊药品用量限制”就要修改这个核心函数违反开闭原则。同时很难动态启用或禁用某些校验比如夜间急诊模式可能放宽某些规则。重构后我们引入策略模式首先定义一个抽象的校验策略接口。class IValidationStrategy { public: virtual ~IValidationStrategy() default; virtual ValidationResult validate(const Prescription prescription) const 0; };然后将每种校验规则实现为一个具体的策略类。class DrugInteractionStrategy : public IValidationStrategy { public: ValidationResult validate(const Prescription rx) const override { // 具体的药品相互作用校验逻辑 // ... return result; } }; class InsuranceComplianceStrategy : public IValidationStrategy { // ... 实现医保合规校验 };最后在处方服务中我们持有一个校验策略的集合。class PrescriptionService { private: std::vectorstd::unique_ptrIValidationStrategy validators; public: void addValidator(std::unique_ptrIValidationStrategy validator) { validators.push_back(std::move(validator)); } bool submitPrescription(const Prescription rx) { for (const auto validator : validators) { auto result validator-validate(rx); if (!result.isPassed()) { logError(result.getMessage()); return false; } } // 所有校验通过执行提交逻辑... return true; } };实操心得与避坑指南策略的无状态性确保每个IValidationStrategy的实现类是无状态的或仅包含配置状态。它们的validate方法应只依赖于输入参数。这样策略对象就可以安全地被多个线程共享或者在不同的上下文中复用。策略的组合与顺序有时校验有先后依赖。例如必须先通过基础格式校验才能进行复杂的医保计算。我们可以在PrescriptionService中维护一个有序列表或者引入一个“校验链”模式Chain of Responsibility来管理顺序。在我们的实现中因为依赖关系不强简单的顺序执行足够但需要在设计文档中明确说明。性能考量如果校验策略非常多且执行成本高可以考虑引入“短路”逻辑。我们在ValidationResult中增加了严重级别字段如ERROR, WARNING。一旦出现ERROR级别的失败立即终止后续校验。对于WARNING则收集起来最后统一提示给用户。3.2 工厂方法模式解耦处方对象的复杂创建过程一张处方对象的创建并非简单的new Prescription()。它需要从数据库加载患者基本信息。根据处方ID或医生输入获取药品明细列表可能涉及库存查询。关联当前操作医生和科室信息。初始化状态为“草稿”。 如果这些创建逻辑散落在业务代码各处一旦数据源变化比如患者信息改从ESB服务总线获取修改点将非常多。我们使用工厂方法模式进行封装class PrescriptionFactory { public: virtual ~PrescriptionFactory() default; virtual std::unique_ptrPrescription createPrescription(int patientId, const std::vectorDrugItem drugs) 0; }; class DatabasePrescriptionFactory : public PrescriptionFactory { private: PatientRepository patientRepo; DrugInfoProvider drugProvider; public: DatabasePrescriptionFactory(PatientRepository repo, DrugInfoProvider provider) : patientRepo(repo), drugProvider(provider) {} std::unique_ptrPrescription createPrescription(int patientId, const std::vectorDrugItem drugs) override { auto prescription std::make_uniquePrescription(); // 1. 设置患者信息来自数据库 auto patient patientRepo.findById(patientId); if (!patient) throw std::runtime_error(Patient not found); prescription-setPatient(*patient); // 2. 填充药品详情查询药品库获取单价、规格等 for (auto item : drugs) { auto drugDetail drugProvider.fetchDrugDetail(item.drugCode); item.fillDetailsFrom(drugDetail); // 填充信息 } prescription-setDrugItems(drugs); // 3. 设置初始状态和创建时间 prescription-setStatus(PrescriptionStatus::DRAFT); prescription-setCreateTime(std::chrono::system_clock::now()); return prescription; } };在业务代码中我们只需要依赖PrescriptionFactory抽象接口。class PrescriptionService { private: std::unique_ptrPrescriptionFactory factory; public: void setFactory(std::unique_ptrPrescriptionFactory factory) { this-factory std::move(factory); } void createNewPrescription(int patientId) { // 获取药品列表来自UI auto drugs fetchDrugsFromUI(); // 使用工厂创建处方对象 auto rx factory-createPrescription(patientId, drugs); // ... 后续操作 } };这样做的好处是将创建逻辑集中化所有关于如何组装一个合法Prescription对象的知识都在工厂里。便于测试在单元测试中我们可以轻松注入一个MockPrescriptionFactory返回预设的处方对象从而隔离对数据库和外部服务的依赖。支持多数据源未来如果需要从缓存或测试文件创建处方只需实现一个新的工厂类如CachePrescriptionFactory业务代码无需改动。3.3 观察者模式实现处方状态变更的松耦合通知处方状态草稿、已提交、已审核、已发药、已作废的每一次变更都可能触发一系列后续动作UI更新刷新界面上的处方列表。审计日志记录“谁在什么时间将处方从状态A改为状态B”。消息推送向患者手机发送审核通过提醒。库存预扣处方审核通过后预先扣除药房库存。如果让负责修改状态的Prescription对象直接调用这些模块耦合度会非常高Prescription类将变得臃肿且难以维护。观察者模式是解决此问题的标准方案首先定义观察者接口和主题被观察者接口。// 观察者接口 class IPrescriptionObserver { public: virtual ~IPrescriptionObserver() default; virtual void onPrescriptionStatusChanged(const Prescription rx, PrescriptionStatus oldStatus, PrescriptionStatus newStatus) 0; }; // 主题接口 class PrescriptionSubject { private: std::vectorIPrescriptionObserver* observers; public: void attach(IPrescriptionObserver* observer) { observers.push_back(observer); } void detach(IPrescriptionObserver* observer) { observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end()); } void notifyStatusChanged(const Prescription rx, PrescriptionStatus oldStatus, PrescriptionStatus newStatus) { for (auto obs : observers) { obs-onPrescriptionStatusChanged(rx, oldStatus, newStatus); } } };然后让Prescription类继承PrescriptionSubject并在其状态改变方法中触发通知。class Prescription : public PrescriptionSubject { private: PrescriptionStatus status; public: void setStatus(PrescriptionStatus newStatus) { if (newStatus ! status) { auto oldStatus status; status newStatus; // 关键通知所有观察者 notifyStatusChanged(*this, oldStatus, newStatus); } } // ... 其他属性和方法 };最后实现具体的观察者。class AuditLogObserver : public IPrescriptionObserver { public: void onPrescriptionStatusChanged(const Prescription rx, PrescriptionStatus oldStatus, PrescriptionStatus newStatus) override { AuditLogEntry entry; entry.prescriptionId rx.getId(); entry.oldStatus oldStatus; entry.newStatus newStatus; entry.timestamp std::chrono::system_clock::now(); entry.operatorId getCurrentUserId(); // 获取当前操作员 AuditLogService::getInstance().log(entry); } }; class NotificationObserver : public IPrescriptionObserver { public: void onPrescriptionStatusChanged(const Prescription rx, PrescriptionStatus oldStatus, PrescriptionStatus newStatus) override { if (newStatus PrescriptionStatus::APPROVED) { // 发送短信或App推送 auto patientPhone rx.getPatient().getPhone(); SmsService::send(patientPhone, 您的处方已审核通过请前往药房取药。); } } };在系统启动时将这些观察者注册到处方对象或一个全局的处方管理器中。注意事项通知的线程安全如果Prescription对象可能在多线程环境下被修改那么notifyStatusChanged的调用和观察者列表的维护attach/detach需要考虑线程安全。我们使用了简单的互斥锁std::mutex来保护观察者列表。观察者的执行耗时通知是同步的如果某个观察者比如一个执行复杂统计的观察者执行非常慢会阻塞状态变更线程。对于耗时操作可以考虑让观察者将任务抛到线程池中异步执行或者使用事件队列Event Queue模式进行解耦。内存管理要小心观察者的生命周期。如果观察者对象被提前销毁而主题还在试图通知它会导致悬空指针和崩溃。我们采用了弱引用std::weak_ptr和智能指针结合的方式来管理观察者确保安全。一个更现代和安全的C做法是使用信号槽库如Qt的信号槽或独立的库如boost::signals2它们内置了生命周期管理。3.4 装饰器模式动态扩展处方的打印与展示功能处方打印的需求多样有的需要标准格式有的需要带医院LOGO的公文格式有的需要为医保报销提供特定签章区域还有的只需要打印药品清单。如果通过继承为每一种组合创建一个子类如StandardPrescription,LogoPrescription,InsurancePrescription,LogoAndInsurancePrescription...类的数量会呈指数级增长这就是“类爆炸”问题。装饰器模式允许我们在运行时动态地添加功能。我们将其应用于处方的打印渲染器。 首先定义一个抽象的打印组件接口。class IPrescriptionPrinter { public: virtual ~IPrescriptionPrinter() default; virtual void print(const Prescription rx) const 0; virtual std::string generateHtml(const Prescription rx) const 0; };实现一个最基础的、只输出核心内容的具体组件。class BasicPrescriptionPrinter : public IPrescriptionPrinter { public: void print(const Prescription rx) const override { // 简单打印到标准输出或基础格式 std::cout Prescription ID: rx.getId() std::endl; std::cout Patient: rx.getPatient().getName() std::endl; for (const auto drug : rx.getDrugItems()) { std::cout - drug.name , Qty: drug.quantity std::endl; } } std::string generateHtml(const Prescription rx) const override { // 生成基础HTML std::string html div classprescription...基础内容.../div; return html; } };然后定义一个装饰器基类它也继承自IPrescriptionPrinter并持有一个IPrescriptionPrinter的指针。这是装饰器模式的关键。class PrescriptionPrinterDecorator : public IPrescriptionPrinter { protected: std::unique_ptrIPrescriptionPrinter wrappedPrinter; public: explicit PrescriptionPrinterDecorator(std::unique_ptrIPrescriptionPrinter printer) : wrappedPrinter(std::move(printer)) {} // 默认实现是直接转发给被装饰的打印机 void print(const Prescription rx) const override { if (wrappedPrinter) { wrappedPrinter-print(rx); } } std::string generateHtml(const Prescription rx) const override { if (wrappedPrinter) { return wrappedPrinter-generateHtml(rx); } return ; } };现在我们可以创建各种具体的装饰器来添加功能。class LogoHeaderDecorator : public PrescriptionPrinterDecorator { private: std::string logoPath; public: LogoHeaderDecorator(std::unique_ptrIPrescriptionPrinter printer, std::string logo) : PrescriptionPrinterDecorator(std::move(printer)), logoPath(std::move(logo)) {} std::string generateHtml(const Prescription rx) const override { std::string html headerimg src logoPath altHospital Logo//header\n; html PrescriptionPrinterDecorator::generateHtml(rx); // 调用被装饰对象的功能 return html; } }; class InsuranceFooterDecorator : public PrescriptionPrinterDecorator { public: using PrescriptionPrinterDecorator::PrescriptionPrinterDecorator; // 继承构造函数 std::string generateHtml(const Prescription rx) const override { std::string html PrescriptionPrinterDecorator::generateHtml(rx); html footer classinsurance-note*此联为医保报销凭证请妥善保管。/footer; return html; } };客户端可以像组装乐高积木一样组合出需要的打印机。// 创建一个带医院LOGO和医保脚注的处方打印机 std::unique_ptrIPrescriptionPrinter printer std::make_uniqueInsuranceFooterDecorator( std::make_uniqueLogoHeaderDecorator( std::make_uniqueBasicPrescriptionPrinter(), /assets/hospital_logo.png ) ); // 使用这个装饰好的打印机 auto html printer-generateHtml(somePrescription);实操中的技巧装饰器与代理模式的区别初学者容易混淆。装饰器模式重在增强功能它和被装饰对象实现同一接口调用会穿透多层装饰。代理模式重在控制访问它可能限制或延迟对被代理对象的访问。在我们的例子里装饰器是透明的它最终一定会调用核心的BasicPrescriptionPrinter。顺序敏感性某些装饰器的顺序可能影响结果。比如先加页眉还是先加页脚在定义装饰器时需要明确其职责和顺序约定。通常与核心内容越相关的装饰越靠近内层。性能影响多层装饰会导致多次函数调用和可能的临时对象创建。在性能敏感的路径如每秒生成数千张处方需要评估开销。不过对于打印这种I/O密集型操作装饰器带来的开销通常可以忽略不计。4. 模式组合与架构集成实践在实际项目中设计模式很少单独使用它们需要协同工作并与整个应用程序架构如依赖注入、模块化集成。4.1 策略模式与工厂模式的联用在我们的系统中校验策略的创建本身也可能比较复杂。例如DrugInteractionStrategy可能需要加载一个庞大的药品相互作用知识库。我们可以为策略的创建也使用工厂模式或抽象工厂或者更简单地在程序启动时由一个配置模块读取配置文件动态创建并组装所需的策略集合然后注入到PrescriptionService中。这进一步将“使用策略”和“创建策略”的职责分离开。4.2 依赖注入容器管理模式对象手动管理这些模式对象工厂、策略、观察者的创建和生命周期会变得繁琐。我们引入了一个轻量级的依赖注入DI容器。在应用启动时将所有服务、工厂、策略注册到容器中并配置它们的依赖关系。例如// 伪代码示意概念 Container container; container.registerSingletonIPrescriptionRepository, DatabasePrescriptionRepository(); container.registerSingletonIDrugInfoProvider, WebServiceDrugProvider(); container.registerSingletonPrescriptionFactory, DatabasePrescriptionFactory(); // 注册校验策略 container.registerTransientIValidationStrategy, DrugInteractionStrategy(interaction); container.registerTransientIValidationStrategy, InsuranceComplianceStrategy(insurance); // ... // PrescriptionService 的依赖由容器自动解析和注入 auto prescriptionService container.resolvePrescriptionService();这样PrescriptionService不需要知道具体的策略类或工厂类它只依赖于抽象接口。对象的创建和组装由容器负责大大降低了模块间的耦合度也使得单元测试更加方便可以注册Mock对象。4.3 应对业务扩展当新需求来临项目上线后果然来了新需求支持处方模板功能快速开具常用药组合并且需要为第三方平台提供处方数据接口。对于处方模板我们发现模板本质上是一个预设了药品列表和用法的“处方蓝图”。我们并没有修改核心的Prescription类而是创建了一个PrescriptionTemplate类它内部使用和Prescription相同的药品项列表。开具处方时PrescriptionFactory增加了一个createFromTemplate方法将模板内容复制到新的处方对象中。这符合“组合优于继承”的原则没有破坏原有处方对象的稳定性。对于第三方接口我们定义了一个新的IPrescriptionExporter接口并实现了JsonExporter和XmlExporter等策略。在需要对外提供数据时处方服务根据对方要求选择合适的导出策略。观察者模式也派上了用场我们增加了一个ApiSyncObserver当处方审核通过后自动将数据同步到第三方平台。原有的核心业务流程完全没有被触动只是增加了新的策略和观察者并通过配置集成进来。5. 性能考量、调试与常见问题排查在C项目中应用设计模式必须关注其运行时开销和调试复杂性。5.1 性能开销分析虚函数调用策略模式、观察者模式等大量依赖虚函数。现代C编译器对虚函数调用的优化已经很好单次调用开销很小通常是一次指针间接寻址加一次函数调用。但在每秒需要处理数万次校验的超高性能场景这个开销可能需要评估。我们的处方系统TPS每秒事务数在百级左右虚函数开销可忽略不计。如果真有性能瓶颈可以考虑使用std::function与函数对象或在编译期使用策略模式通过模板但这会损失一些运行时灵活性。对象创建与销毁工厂模式、装饰器模式会创建更多对象。我们广泛使用智能指针std::unique_ptr,std::shared_ptr来管理生命周期避免内存泄漏。对于频繁创建销毁的小对象如某些装饰器可以考虑使用对象池进行优化。观察者通知的代价如果观察者数量很多notify方法中的循环遍历会成为瓶颈。我们曾遇到一个性能问题一个处方状态变更意外触发了一个执行复杂数据库查询的观察者导致界面卡顿。解决方案是第一审查观察者逻辑确保其高效第二将耗时操作异步化第三对于非关键通知可以考虑批量或延迟处理。5.2 调试与日志增强模式增加了间接层调试时堆栈可能更深。为了快速定位问题我们强化了日志在每个策略的validate方法入口和出口打日志记录策略名称和校验结果。在工厂的create方法中记录创建过程中的关键步骤和可能出现的异常。在观察者的onPrescriptionStatusChanged中记录观察者类型和其执行耗时。 我们为这些日志点定义了不同的级别DEBUG, INFO, WARN在生产环境中关闭DEBUG日志在测试和预发环境则全部打开便于追踪数据流。5.3 常见问题速查表问题现象可能原因排查思路与解决方案程序崩溃错误指向虚函数表观察者或策略对象已被销毁但主题仍持有其指针悬空指针。1. 检查对象生命周期确保观察者存活期长于或等于主题。2. 将原始指针改为std::weak_ptr在通知前尝试lock()获取shared_ptr失败则跳过。3. 使用boost::signals2等库它们自动管理连接生命周期。某个校验规则始终不生效策略未被正确添加到PrescriptionService的校验链中。1. 检查依赖注入配置或初始化代码确认策略对象被实例化并添加。2. 在服务初始化后打印当前已加载的策略列表进行确认。装饰器添加的页眉/页脚顺序不对装饰器的包装顺序与预期不符。1. 审查创建装饰器对象的代码确认包装层次。内层装饰器先执行其核心功能然后外层装饰器添加额外内容。2. 明确约定装饰器的职责和推荐顺序并在文档中说明。内存使用量持续增长工厂创建的对象未正确释放或观察者列表中存在循环引用。1. 使用Valgrind或AddressSanitizer等工具检测内存泄漏。2. 检查智能指针的使用确保没有用shared_ptr造成循环引用特别是观察者与主题相互持有时。必要时使用weak_ptr打破循环。多线程下状态通知丢失或重复notifyStatusChanged方法非线程安全在遍历观察者列表时列表被另一个线程修改。1. 使用互斥锁std::mutex保护观察者列表的修改和遍历操作。2. 考虑使用线程安全的容器如std::vector配合锁或并发库中的数据结构。注意锁的粒度避免性能问题。6. 总结与个人体会回顾整个处方管理系统的重构过程设计模式的价值不在于生搬硬套那23种模板而在于它们提供了一套经过验证的、用于管理代码复杂性的词汇表和思维工具。当我在代码中看到“if-else”链或感受到添加新功能时的恐惧时我就会下意识地去寻找其中隐藏的“变化点”然后思考哪个模式可以帮助我封装它。对于C开发者而言使用设计模式需要格外注意资源管理和性能。RAII资源获取即初始化原则和智能指针是我们的利器能确保模式带来的对象动态创建不会导致内存泄漏。同时要时刻用性能剖析工具如gprof, perf观察模式引入的间接调用是否成了热点避免为了设计而设计牺牲了C的核心优势。最后一点体会是模式的引入应该是渐进式的。不要试图在项目第一天就设计出一个完美包含所有模式的架构。更好的做法是先让代码工作然后在重构中识别坏味道Code Smell再引入合适的模式进行改善。例如当你发现需要添加一个新的处方校验规则却不得不修改一个庞大的validate函数时就是引入策略模式的最佳时机。这种由需求驱动、小步快跑的重构能让系统架构在保持灵活性的同时也不至于过早陷入过度设计的泥潭。这个处方管理系统的代码在经过模式化重构后不仅顺利支撑了后续两年的业务扩展而且新同事接手时通过阅读类名和接口就能很快理解系统的核心结构和数据流向这大概就是对这次架构工作最好的肯定。