Loading... > 开闭原则的英文全称是 Open Closed Principle,简写为 OCP 软件实体(类、模块、函数)**应该对扩展开放,对修改封闭**。 不修改代码,那我怎么实现新的需求呢?答案就是靠扩展 我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。 需求变更的时候,不要想着满街去改,而是想着应该增加一些东西来应对。 表述一下,那就是,**添加一个新的功能应该是**,在已有代码基础上`扩展代码`(新增模块、类、方法等),而`不修改已有代码`(修改模块、类、方法等)。 # 一、不修改代码 前提是要在软件内部留好扩展点,而这正是需要我们去设计的地方。因为每一个扩展点都是一个需要设计的模型。 ```cpp class HotelService { public: double getRoomPrice(const User user, const Room room) { double price = room.getPrice(); if (user.getLevel() == Level.GOLD) { return price * 0.8; } if (user.getLevel() == Level.SILVER) { return price * 0.9; } return price; } } ``` 这时,新的需求来了,要增加白金卡会员,给出 75 折的优惠,如法炮制的写法应该是这样的: ```cpp class HotelService { public: double getRoomPrice(const User user, const Room room) { double price = room.getPrice(); if (user.getLevel() == Level.GOLD) { return price * 0.8; } if (user.getLevel() == Level.SILVER) { return price * 0.9; } if (user.getLevel() == Level.PlATINUM) { return price * 0.75; } return price; } } ``` 问题: 每增加一个用户级别,我们要改的代码就漫山遍野。 解决办法: 既然每次要增加的是用户级别,而且`各种服务的差异都体现在用户级别上,我们就需要一个用户级别的模型,使用策略模式的核心,找出扩展点。` ```cpp class UserLevel { public: double getRoomPrice(Room room); } class GoldUserLevel : public UserLevel { public: double getRoomPrice(const Room room) { return room.getPrice() * 0.8; } } class SilverUserLevel : public UserLevel { public: double getRoomPrice(const Room room) { return room.getPrice() * 0.9; } } ``` 我们原来的代码就可以变成这样: ```cpp class HotelService { public: double getRoomPrice(const User user, const Room room) { return user.getRoomPrice(room); } } class User { public: double getRoomPrice(const Room room) { level = getUserLevel(); return level.getRoomPrice(room); } UserLevel getUserLevel() { if (user.getLevel() == Level.GOLD) { return new GoldUserLevel(); } if (user.getLevel() == Level.SILVER) { return new SilverUserLevel(); } if (user.getLevel() == Level.PlATINUM) { return new PlatinumUserLevel(); } } private: UserLevel level; ... } ``` 再增加白金用户,我们只要写一个新的类就好了: ```cpp class PlatinumUserLevel : public UserLevel { public: double getRoomPrice(const Room room) { return room.getPrice() * 0.75; } } ``` `在代码里留好了扩展点:UserLevel。` **HotelService 的 getRoomPrice 这个方法就稳定了下来,我们就不需要根据用户级别不断地调整这个方法了。** ## 1、构建扩展点 > 封装的要点是行为,数据只是实现细节,而很多人习惯性的写法是面向数据的,这也是导致很多人在设计上缺乏扩展性思考的一个重要原因。 构建模型的难点,首先在于分离关注点,这个之前说过很多次了,不再赘述,其次在于找到共性。 要构建起抽象就要找到事物的共同点,有了这个理解,我们看前面的例子应该还算容易理解。 ```cpp class ReportService { public: void process() { // 获取当天的订单 List<Order> orders = fetchDailyOrders(); // 生成统计信息 OrderStatistics statistics = generateOrderStatistics(orders); // 生成统计报表 generateStatisticsReport(statistics); // 发送统计邮件 sendStatisticsByMail(statistics); } } ``` 在原有的代码里,前面两步分别是获取源数据和生成统计信息,后面两步分别是,生成报表和将统计信息通过邮件发送出去。 新需求: 把统计信息发给另外一个内部系统,这个内部系统可以把统计信息展示出来,供外部合作伙伴查阅。 解决办法: **后两步和即将添加的步骤有一个共同点,都使用了统计信息,这样我们就找到了它们的共性,所以,我们就可以用一个共同的模型去涵盖它们**。 ```cpp class ReportService { public: void process() { // 获取当天的订单 List<Order> orders = fetchDailyOrders(); // 生成统计信息 OrderStatistics statistics = generateOrderStatistics(orders); for (OrderStatisticsConsumer consumer: consumers_) { consumers_.consume(statistics); } } private: List<OrderStatisticsConsumer> consumers_; } ``` 我们的新需求也只要添加一个新的类就可以实现了: ```cpp class StatisticsSender : public OrderStatisticsConsumer { public: void consume(const OrderStatistics statistics) { sendStatisticsToOtherSystem(statistics); } } ``` 在这个例子里,我们第一步做的事情还是分解,就是把一个一个的步骤分开,然后找出步骤之间相似的地方,由此构建出一个新的模型。 在真实的项目中,想要达到开放封闭原则的要求并不是一蹴而就的。这里我们只是因为有了需求的变动,才提取出一个 OrderStatisticsConsumer。 # 二、开闭原则对单元测试的影响 > 只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试 老的单元测试不会被修改 ```cpp class Alert { public: Alert(AlertRule rule, Notification notification) { rule_ = rule; notification_ = notification; } void check(String api, long requestCount, long errorCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) { notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } private: AlertRule rule_; Notification notification_; } ``` AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。 新需求: 如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢? ```cpp class Alert { // ...省略AlertRule/Notification属性和构造函数... // 改动一:添加参数timeoutCount public: void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) { notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } // 改动二:添加接口超时处理逻辑 long timeoutTps = timeoutCount / durationOfSeconds; if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } } } ``` 主要的改动有两处: 第一处是修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数; 第二处是在 check() 函数中添加新的告警逻辑。 ```cpp class Alert { // ...省略AlertRule/Notification属性和构造函数... // 改动一:添加参数timeoutCount public: void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) { notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } // 改动二:添加接口超时处理逻辑 long timeoutTps = timeoutCount / durationOfSeconds; if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } } } ``` 一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 check() 函数,**相应的单元测试都需要修改。** 解决办法: 我们先重构一下之前的 Alert 代码,让它的扩展性更好一些。重构的内容主要包含两部分: 第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类; 第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中。 ```cpp class Alert { public: void addAlertHandler(AlertHandler alertHandler) { alertHandlers_.push_back(alertHandler); } void check(ApiStatInfo apiStatInfo) { for (AlertHandler handler : alertHandlers_) { handler.check(apiStatInfo); } } private: List<AlertHandler> alertHandlers_; } class ApiStatInfo {//省略constructor/getter/setter方法 private: String api; long requestCount; long errorCount; long durationOfSeconds; } class AlertHandler { public: AlertHandler(AlertRule rule, Notification notification) { rule_ = rule; notification_ = notification; } virtual void check(ApiStatInfo apiStatInfo) = 0; protected: AlertRule rule_; Notification notification_; } class TpsAlertHandler : public AlertHandler { public: TpsAlertHandler(AlertRule rule, Notification notification) { super(rule, notification); } @Override void check(ApiStatInfo apiStatInfo) override { long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds(); if (tps > rule_.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) { notification_.notify(NotificationEmergencyLevel.URGENCY, "..."); } } } class ErrorAlertHandler : public AlertHandler { public: ErrorAlertHandler(AlertRule rule, Notification notification){ super(rule, notification); } void check(ApiStatInfo apiStatInfo) override{ if (apiStatInfo.getErrorCount() > rule_.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) { notification_.notify(NotificationEmergencyLevel.SEVERE, "..."); } } } ``` 怎么使用上面的代码? ApplicationContext 是一个单例类,负责 Alert 的创建、组装(alertRule 和 notification 的依赖注入)、初始化(添加 handlers)工作 ```cpp class ApplicationContext { public: void initializeBeans() { alertRule_ = new AlertRule(/*.省略参数.*/); //省略一些初始化代码 notification_ = new Notification(/*.省略参数.*/); //省略一些初始化代码 alert_ = new Alert(); alert_.addAlertHandler(new TpsAlertHandler(alertRule, notification)); alert_.addAlertHandler(new ErrorAlertHandler(alertRule, notification)); } Alert getAlert() { return alert_; } static ApplicationContext getInstance() { return instance; } private: AlertRule alertRule_; Notification notification_; Alert alert_; // 饿汉式单例 static final ApplicationContext instance = new ApplicationContext(); ApplicationContext() { initializeBeans(); } } class Demo { public: static void main(String[] args) { ApiStatInfo apiStatInfo = new ApiStatInfo(); // ...省略设置apiStatInfo数据值的代码 ApplicationContext.getInstance().getAlert().check(apiStatInfo); } } ``` 总结下所有的改动点: 主要的改动有下面四处。 第一处改动是:在 ApiStatInfo 类中添加新的属性 timeoutCount。 第二处改动是:添加新的 TimeoutAlertHander 类。 第三处改动是:在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler。 第四处改动是:在使用 Alert 类的时候,需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。 ```cpp class Alert { // 代码未改动... }; class ApiStatInfo { //省略constructor/getter/setter方法 private: String api; long requestCount; long errorCount; long durationOfSeconds; long timeoutCount; // 改动一:添加新字段 }; class AlertHandler { // abstract 代码未改动... }; class TpsAlertHandler : public AlertHandler { //代码未改动... }; class ErrorAlertHandler : public AlertHandler { //代码未改动... }; // 改动二:添加新的handler class TimeoutAlertHandler : public AlertHandler { //省略代码... }; class ApplicationContext { private: AlertRule alertRule_; Notification notification_; Alert alert_; public: void initializeBeans() { alertRule_ = new AlertRule(/*.省略参数.*/); //省略一些初始化代码 notification_ = new Notification(/*.省略参数.*/); //省略一些初始化代码 alert_ = new Alert(); alert_.addAlertHandler(new TpsAlertHandler(alertRule_, notification_)); alert_.addAlertHandler(new ErrorAlertHandler(alertRule_, notification_)); // 改动三:注册handler alert_.addAlertHandler(new TimeoutAlertHandler(alertRule_, notification_)); } //...省略其他未改动代码... }; class Demo { public: static void main(String[] args) { ApiStatInfo apiStatInfo = new ApiStatInfo(); // ...省略apiStatInfo的set字段代码 apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值 ApplicationContext.getInstance().getAlert().check(apiStatInfo); } }; ``` 重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。 # 三、如何在项目中灵活应用开闭原则? > 个人理解,选择有if存在的地方 如果你开发的是一个业务导向的系统: 比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。 如果你开发的是跟业务无关的、通用的、偏底层的系统: 比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。 不过,有一句话说得好,“唯一不变的只有变化本身”。即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。`我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计`。 `开放封闭原则还可以帮助我们改进自己的系统,我们可以通过查看自己的源码控制系统,找出那些最经常变动的文件,它们通常都是没有满足开放封闭原则的,而这可以成为我们改进系统的起点`。 **构建模型的难点,首先就在于分离关注点,其次是找到共性**。 **设计扩展点,迈向开放封闭原则。** 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏