范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

软件设计中最关键的开闭原则,到底指什么?

  前言
  软件设计原则中有一条很关键的原则是开闭原则,就是所谓的对扩展开放,对修改关闭。个人觉得这条原则是非常重要的,直接关系到你的设计是否具备良好的扩展性,但也是相对比较难以理解和掌握的,究竟怎样的代码改动才被定义为"扩展"?怎样的代码改动才被定义为"修改"?怎么才算满足或违反"开闭原则"?别急,本文将展开详细阐述。举个例子好理解
  为了更好的解释清楚,直接上例子,这是监控告警的类,Alert是监控告警类,AlertRule存储告警规则信息,Notification是告警通知类。public class Alert {      // 存储告警规则     private AlertRule rule;      // 告警通知类, 支持邮件、短信、微信、手机等多种通知渠道。     private Notification notification;  	public Alert(AlertRule rule, Notification notification) {          this.rule = rule;          this.notification = notification;      }      // 校验是否进行告警     public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {         // 计算请求的tps         long tps = requestCount / durationOfSeconds;          // 如果tps大于阈值进行告警         if (tps > rule.getMatchedRule(api).getMaxTps()) {               notification.notify(NotificationEmergencyLevel.URGENCY, "...");          }          // 如果错误次数大于规则阈值进行告警         if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {              notification.notify(NotificationEmergencyLevel.SEVERE, "...");          }      }  }
  这个告警Alert的核心业务逻辑主要集中在check()函数中当接口的 TPS 超过某个预先设置的最大值时,触发告警,发送通知。当接口请求出错数大于某个最大允许值时,就会触发告警,通知接口的相关负责人或者团队。
  现在来了个新的需求,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢?做法一
  这简单,你可能直接开工就写出下面的代码了。public class Alert {      // ... 省略 AlertRule/Notification 属性和构造函数...      // 改动一:添加参数 timeoutCount      public void check(String api, long requestCount, long errorCount, long timeoutCount) {     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()方法逻辑中添加了接口超时处理逻辑。
  这个做法有啥问题呢?你竟然调整了check()方法的参数,所有原来调用的地方都要修改,如果很多,这不得恨死你呀。修改了 check()函数,相应的单元测试都需要修改。
  像这种情况,我们就是完全对原来的代码进行修改,不符合开闭原则。做法二
  这时候,你开动脑瓜,大刀阔斧的进行了重构。引入了ApiStatInfo类,封装了check的入参信息。public class ApiStatInfo {// 省略 constructor/getter/setter 方法 	private String api;      private long requestCount;      private long errorCount;     private long durationOfSeconds;   } 引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中 public abstract class AlertHandler {      protected AlertRule rule;     protected Notification notification;           public AlertHandler(AlertRule rule, Notification notification) { 		this.rule = rule;          this.notification = notification;      }      public abstract void check(ApiStatInfo apiStatInfo);  }  // TPS的告警处理器 public class TpsAlertHandler extends AlertHandler {     public TpsAlertHandler(AlertRule rule, Notification notification) {          super(rule, notification);      }           @Override     public void check(ApiStatInfo apiStatInfo) {          long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds;         if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {              notification.notify(NotificationEmergencyLevel.URGENCY, "...");          }     }  }  // 错误次数告警处理器 public class ErrorAlertHandler extends AlertHandler {      public ErrorAlertHandler(AlertRule rule, Notification notification){          super(rule, notification);      }      @Override     public void check(ApiStatInfo apiStatInfo) {          if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi())              notification.notify(NotificationEmergencyLevel.SEVERE, "...");      }  } 修改Alert类,添加各种告警处理器。public class Alert {      private List alertHandlers = new ArrayList<>();      public void addAlertHandler(AlertHandler alertHandler) {          this.alertHandlers.add(alertHandler);      }           public void check(ApiStatInfo apiStatInfo) {          // 遍历各种告警处理器         for (AlertHandler handler : alertHandlers) {              handler.check(apiStatInfo);          }      }  }上层单例类ApplicationContext创建、组装、使用Alert类public class ApplicationContext {      private AlertRule alertRule;      private Notification notification;      private 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));      }       // 返回告警器Alert     public Alert getAlert() { return alert; }  	// 饿汉式单例     private static final ApplicationContext instance = new ApplicationContext();          private ApplicationContext() {          instance.initializeBeans();      }           public static ApplicationContext getInstance() {          return instance;      } }  public class Demo {      public static void main(String[] args) {          ApiStatInfo apiStatInfo = new ApiStatInfo();  // ... 省略设置 apiStatInfo 数据值的代码 		// 进行告警操作         ApplicationContext.getInstance().getAlert().check(apiStatInfo);      }  }
  终于你重构完一开始的逻辑了, 在这个基础上,针对每秒钟接口超时请求个数超过某个最大阈值就告警这个需求,我们又该如何改动代码呢?ApiStatInfo类添加新字段public class ApiStatInfo {// 省略 constructor/getter/setter 方法 	private String api;      private long requestCount;      private long errorCount;      private long durationOfSeconds;      private long timeoutCount; // 改动一:添加新字段  } 添加新的处理器类TimeoutAlertHandlerpublic class TimeoutAlertHandler extends AlertHandler {// 省略代码...}修改ApplicationContext类添加注册TimeoutAlertHandlerpublic 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));          // 改动三:注册 handler          alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));      }      //... 省略其他未改动代码 }调用告警处理的地方设置参数public 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逻辑,也只需要为新增的类写单元测试。这种情况就是很符合开闭原则的。
  可能你会纠结我也明明修改代码了,怎么就是对修改关闭了呢?第一个修改的地方是向 ApiStatInfo 类中添加新的属性 timeoutCount。实际上,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为"修改",在细代码粒度下,又可以被认定为"扩展"。比如这里的添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为"修改";但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为"扩展"。另外一个修改的地方是在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler;在使用 Alert 类的时候,需要给check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。首先说明添加一个新功能,不可能任何模块、类、方法的代码都不"修改",这个是不可能的。主要看修改的是什么内容,这里的修改是上层的代码,而非核心下层的代码,所以是可以接受的。如何理解开闭原则?
  前面通过一个例子详细阐述了开闭原则的核心思想,对修改关闭,对扩张开放,这里再次做一个总结,让大家进一步理解开闭原则。
  添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发,而且尽量修改的是上层的代码,而非底层或者和核心逻辑的代码。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为"修改";在细代码粒度下,可能又被认定为"扩展",比如对于一个类添加一个字段或者方法,在某些情况下我们也可以认为是扩展。开闭原则一定是好的吗?
  开闭原则并不是没有条件的。有些情况下,代码的扩展性会跟可读性相冲突。比如,我们之前举的 Alert 告警的例子。为了更好地支持扩展性,我们对代码进行了重构,重构之后的代码要比之前的代码复杂很多,理解起来也更加有难度。很多时候,我们都需要在扩展性和可读性之间做权衡。在某些场景下,代码的扩展性很重要,我们就可以适当地牺牲一些代码的可读性;在另一些场景下,代码的可读性更加重要,那我们就适当地牺牲一些代码的可
  扩展性。
  在我们之前举的 Alert 告警的例子中,如果告警规则并不是很多、也不复杂,那 check() 函数中的 if 语句就不会很多,代码逻辑也不复杂,代码行数也不多,那最初的第一种代码实现思路简单易读,就是比较合理的选择。相反,如果告警规则很多、很复杂,check()函数的 if 语句、代码逻辑就会很多、很复杂,相应的代码行数也会很多,可读性、可维护性就会变差,那重构之后的第二种代码实现思路就是更加合理的选择了。总之,这里没有一个放之四海而皆准的参考标准,全凭实际的应用场景来决定。怎么做到"对扩展开放、修改关闭"?
  开闭原则,本质上就是让你写的程序扩展性好,这需要你平时慢慢的积累和学习,需要时刻具备扩展意识、抽象意识、封装意识。这些"潜意识"可能比任何开发技巧都重要。平时需要多多思考,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到"对扩展开放、对修改关闭"。但是切记不要过度设计,不然维护十分困难,也会造成灾难性后果。
  至于具体的方法论层面,我十分推荐大家要面向接口编程,怎么理解呢?
  比如现在有个业务需求是将消息发送到kafka,你可能直接在业务代码中调用kafka的API发送消息,这就是面向实现编程,这样非常不好,万一以后不用kafka,该用rocketMQ了怎么办?这时候,我们是不是定义一个发消息的接口,让上层直接调用接口即可。
  总结
  本文讲解了软件设计中个人认为最重要的一个设计原则,开闭原则,即对扩展开放,对修改关闭,这会指导我们写出扩展性良好的代码,设计出扩展性更好的架构。

2010年自驾青藏尼泊尔川藏纪行(八)第十天。拉孜县城很小,早上我们逛了一圈十来分钟,和西藏大多数县城一样,人烟稀少,还没有内地乡镇热闹,但遇到的藏民都很友好,人人脸上带着微笑,尤其是儿童,纯真的眼神是内地儿童少有的。萧江发源地美丽南溪南花山福地石林间,古往今来藏神仙。悟空大闹蟠桃会,奇迹还在眼前边。老虎流泪遗迹在,师姑戴笠留芳踪。胭脂鸳龙降甘露,二十四柱洞天巾。在现今的黄山市,知道西溪南的人多,但知道南溪南的可能真正的天空之城,建在云端的云南第二大侨乡,打通11条出国商路今天的自驾云南之旅来到了一座非常特别的县城,它就是位于滇南的红河县,其县城驻地迤萨镇是云南第二大侨乡,曾经马帮文化与对外贸易一度辉煌。进入红河县一路盘山公路,整座县城都建造在高山之探秘魔鬼城新疆游(5)8月20日我们离开布尔津前往奎屯,途中游览著名的克拉玛依石油城旁的魔鬼城。这是一处典型的雅丹地貌。雅丹地貌是干燥地区的一种风蚀地貌。河湖相土状沉积物所形成的地面,经强大的定向风的吹教科书式自救头条创作挑战赛我们一行5人游黄山后,自驾别克商务车于早上7点20分出发,从安徽黄山市汤口镇返回扬州。如果走高速不到400公里,可以到家吃中饭。但听说皖南川藏线很美,我俩和五连襟夫妇或许你不知道,武汉是个被埋没的宝藏旅游城市去过一趟武汉才后悔,你会发现,你真的是去迟了,在很多喜好旅行的人眼里,武汉,其实是一个被埋没的宝藏旅游城市!那里有黄鹤楼与武汉第一高楼绿地大厦,还有鹦鹉洲大桥与龟山电视塔,还可以去新疆旅行库车游记吃了库车大馕,再去逛老城继续新疆旅行,本期目的地库车。秋天,是这个城市以及这个省份最舒服的季节。这个季节,在布满树伞的库车老城里走一走会觉得,不太真实。走着走着,天空和眼前的画面就会变得有些模糊,感觉整个摆烂式旅游国庆假期走红,传递了什么信号?国庆假期过后,摆烂式旅游这一词汇走入大众的视野。在酒店发呆宅民宿玩手机没做攻略,在18线小城闲游成了大部分人的假期真实写照,以前嗤之以鼻的换个地方躺着换个地方玩手机的摆烂式旅游,在山东的奇美小城五莲的历史人文小记在山东半岛南部,有一座奇美的小城,名曰五莲,镶嵌在青岛潍坊日照之间,因境内有奇秀的五莲山而得名。明代万历年间,有一位蜀地的僧人,法号明开,云游天下名山大川,走到五莲山时,深深地被此实拍蒙古国街头老百姓真实生活(肯定和你想象中不一样)蒙古国地处蒙古高原,东南西三面与中国接壤,北面同俄罗斯的西伯利亚为邻。蒙古是一个地广人稀的草原之国,平均人口密度为每平方公里1。5人。人口以喀尔喀蒙古族为主,约占全国人口的80,此新疆哈密市历史上的三堡历史上的三堡柳树泉农场有处地方称三堡,那是我的故乡。自从有了丝绸之路,就有了各个商贾和达官贵人的东西往来。通往新疆的必经之路上慢慢形成了一个小镇三堡。它位于哈密以西六十公里处,北倚因红叶谷闻名遐迩的吉林东部小城蛟河蛟河位于吉林省东部,传说在很久以前,有一条大河横贯于此,河中出现过一条蛟龙,于是人们就把这条河取名为蛟河,蛟河县城由此得名。蛟河的森林面积约占总土地面积的三分之二,是吉林省主要林区金秋十月秋霜悄然降临,开着思皓花仙子去光雾山看红叶金秋十月,红叶似火,硕果累累,这在收获的季节,约上几个好友,开着我们可爱的思皓花仙子去巴中5A级风景区去看红叶。四川光雾山因红叶景观被誉为天下红叶第一山,以秀丽奇特的群峰为代表,苍90年代土掉渣的服饰,现在却成了爆款?难怪爸妈总嫌我土人总是后知后觉,看待时尚单品亦是如此。8090年代,正值我们小时候,爸妈年轻之时,总觉得那时候的服装很土气,可如今过去30年了,现在看起来却很时髦。时尚是个轮回,之时那时候的人对于仙牌ElieSaab2023夏日威风黎巴嫩设计师ElieSaab创立的同名品牌优雅华丽匠心独运深受时尚名流及皇室贵族的追捧与青睐,更是女明星竞相逐利的顶奢时尚资源,2023春夏成衣系列以SummerBreeze夏日的漂亮,从来都不是最重要的01hr我看着镜子里的自己,有一瞬间的恍惚。镜子里的女孩子,得并不是很好看,没有光泽的头发,黑的小脸和没有血色的嘴唇。就连标志这两个字,也仿佛跟我搭不上一点关系。朋友说,化妆会变得澳门服装节2022如期而至,广东十佳亮相开幕大秀10月20日,澳门服装节2022在澳门威尼斯人金光会展中心拉开帷幕,广东十佳服装设计师韩银月陈季红邓晓明亮相开幕大秀。开幕式上,来自广州深圳东莞台北香港澳门的六位时装设计师联袂贡献脸部做过线雕后还能做拉皮么?脸部做过线雕后还能做拉皮么?最近有求美者咨询说,之前做过线雕,现在还可以做拉皮手术么?一般情况下,做过正常的线雕,对面部造成的创伤还是比较小的。只要是度过了这种线雕的急性水肿期,理杨幂这么白大长腿,走到哪里都是一道亮丽的风景线啊明星受人追捧和喜爱除了自身高超的演技和美丽的颜值外,最让人心动的就是靠长腿出圈的女明星。腿长的人在生活中是一种非常大的优势,更不用提在娱乐圈了。在娱乐圈中可以说拥有大长腿的女明星,早餐到底吃什么最健康早餐通常吃的健康食品比较多,常见的食品主要有鸡蛋牛奶以及面包和豆浆等,这些食物容易被人体消化和吸收,可以为身体补充所需的能量。1鸡蛋鸡蛋里面含有人体所需要的蛋白质,营养价值比较高,百合的功能作用有哪些?其养生保健应用特色有什么?百合的功能作用有哪些?其养生保健应用使用特色有什么?1。百合滋养心肺清润安神。临床常用于既疗肺阴不足所致干咳少痰,或劳嗽久咳痰中带血,又诊心阴不足所致的失眠多梦精神恍惚烦躁不安等,微孔透爽婴儿纸尿裤贴肤舒爽,呵护宝宝健康纸尿裤对于婴幼儿宝宝而言,是日常生活必需品。在这凉爽的秋季依然需要给宝宝穿纸尿裤,一来是防止宝宝尿床,二来是可以防止细菌入侵宝宝的私密部位。对于新生儿而言,皮肤更加的娇嫩脆弱,更加
新航线来了!达州直飞城市又增3个关注我们,了解新时代达州!什么时候能飞昆明?什么时候能飞温州?什么时候网友们心心念念的新航线来了10月达州金垭机场增开3条航线以后可以直飞昆明温州宜宾啦开不开心,激不激动?!达州昆32个项目集中签约!淄博经开区产业新城呼之欲出总投资179。1亿元,32个项目集中签约!10月11日,2022年淄博市(经开区)招商引资项目集中签约活动暨科创制造示范园区集群开工仪式举行。仪式上,除了签约的32个项目,总投资1金平县打造绿美边寨绘就秀美村庄走进金平县勐桥乡卡房村茅草坪村民小组,一座秀美村庄镶嵌在青山绿水之中,村内绿树成荫瓜果飘香。鳞次栉比的青石板路,别具一格的农家庭院,布局合理的小菜园小花园小果园,无不呈现出宜家宜居一生必去的5个旅游景点,让人看一眼就上瘾,退休了就去小住几天人世间最美好的事情莫过于有花不完的钱,看不完的美景,吃不完的美食,但是有些地方总会让人像浪子一样收心,去到了这个地方以后,就不再想离开。一生必去的5个旅游景点,让人看一眼就上瘾,退冷知识!青岛高新区有一片3。4万平方米的热带雨林热带雨林,高山源地,瀑布落水,这座世界级生态文化旅游地,会成为青岛旅游的扛把子吗?你有没有过想去热带雨林冒险的冲动?摊牌了!小召可是一个狂热的热带雨林爱好者!夺宝奇兵让小召第一次对印度最大人肉洗衣场,一天洗衣20万件为何能存在100多年?凭借着低廉的工资,洗衣工战胜了洗衣机。正解局出品在印度最发达的城市孟买,贫民窟里藏着一个洗衣场。这个洗衣场,曾是获得奥斯卡奖的印度电影贫民窟的百万富翁的取景地。贫民窟的百万富翁剧照庐山自驾旅游攻略,看这一篇就够了首先是门票景区门票160车辆门票45一张,自驾从北门进入,到北门购买,相当于停车费吧,因为上山停车基本不收费(除了镇上商业街附近有个地下停车场收费之外,五六块一个小时)景区巴士门票去山东,行家不喝景芝,却专挑这4款廉价酒,当地人识货齐鲁大地,人杰地灵,风景壮丽。东岳泰山巍峨耸立,被尊为五岳之首,杜甫会当凌绝顶,一览众山小的诗句流传千古,至今读来仍是豪情万丈,慷慨激越。壮阔的山水,赋予了这里的人们豪爽热情乐观大为什么人不能吃得太饱?一个人的胃总是有限度的,如果吃得太饱,胃里就会充满食物。我们吃的食物是靠胃的蠕动来消化的,胃里东西太多,蠕动速度就慢,就不能产生足够多的胃酸来消化食物,这样容易造成消化不良。一旦消它是入秋补脑圣品,补肾健脑,润燥化痰有一种食物,形似大脑,食之味甘,补脑益智,还有益智果的称号,它就是核桃!核桃核桃,又称胡桃羌桃,与扁桃仁腰果榛子并称为世界著名的四大干果。核桃的故乡在中东的伊朗,汉代张骞出使西域后蜂蜜早上喝好,还是晚上喝好?列出2个时间段,二选一即可大多数人对蜂蜜的印象就是很甜,直接吃蜂蜜会感觉甜得发腻,因此要加入温水,有时还会搭配柠檬让其口感保持酸甜。而蜂蜜不只是提供丰富的糖类物质,还有其他的微量元素维生素矿物质,在保养身体