关于后端:软件设计中最关键的开闭原则究竟指什么呢

42次阅读

共计 7091 个字符,预计需要花费 18 分钟才能阅读完成。

前言
软件设计准则中有一条很要害的准则是开闭准则,就是所谓的对扩大凋谢,对批改敞开。集体感觉这条准则是十分重要的,间接关系到你的设计是否具备良好的扩展性,但也是绝对比拟难以了解和把握的,到底怎么的代码改变才被定义为“扩大”?怎么的代码改变才被定义为“批改”?怎么才算满足或违反“开闭准则”?别急,本文将开展具体论述。
举个例子好了解
为了更好的解释分明,间接上例子,这是监控告警的类,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<AlertHandler> 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; // 改变一:增加新字段

}
复制代码

增加新的处理器类 TimeoutAlertHandler

public class TimeoutAlertHandler extends AlertHandler {// 省略代码 …}
复制代码

批改 ApplicationContext 类增加注册 TimeoutAlertHandler

public 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 了怎么办?这时候,咱们是不是定义一个发消息的接口,让下层间接调用接口即可。

总结
本文解说了软件设计中集体认为最重要的一个设计准则,开闭准则,即对扩大凋谢,对批改敞开,这会领导咱们写出扩展性良好的代码,设计出扩展性更好的架构。

正文完
 0