第二部分:理论二
如何理解“对扩展开放、修改关闭”?
开闭原则
- 开闭原则:SOLID 中的第二个原则,英文全称是 Open Closed Principle,简写为OCP。
- 开闭原则是 SOLID 中最难理解、最难掌握,同时也是最有用的一条原则。
- 对扩展开放、修改关闭,直接影响代码的扩展性,扩展性是代码质量最重要的衡量标准之一。
项目实战
- API 接口监控告警的代码
- Alert类:
- 属性两个:AlertRule 存储告警规则,Notification 是告警通知类
- 方法:check()主要业务逻辑两种报警场景
- 此时要添加一种报警场景,修改:
- check()增加一个参数
- 在check()方法内部增加新报警场景的逻辑代码
- 修改单元测试
- 用扩展的方案,要先重构Alert类:
- 将check()方法的多参数,封装成ApiStatInfo 类
- 引入handler概念,将多种报警场景放在各handler中
- 此时要添加一种报警场景,扩展:
- 在ApiStatInfo 类中新增属性
- 添加一个新的报警场景的handler类
- 重构后的代码十分灵活,添加新的报警场景,完全不需要修改check()的逻辑,只需要添加对应的handler即可。老的单元测试也不用修改,只需要添加新的单元测试。
原始 API 接口监控告警类 Alert 类的代码:
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 dur
long tps = requestCount / durationOfSeconds;
if (tps > rule.getMatchedRule(api).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
修改方案:
public class Alert {
// ... 省略 AlertRule/Notification 属性和构造函数...
// 改动一:添加参数 timeoutCount
public void check(String api, long requestCount, long errorCount, long tim
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, "...");
}
}
}
扩展方案:
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);
}
}
}
public class ApiStatInfo {// 省略 constructor/getter/setter 方法
private String api;
private long requestCount;
private long errorCount;
private long durationOfSeconds;
}
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);
}
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.getDurationOfSecon
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 类的使用方法:ApplicationContext 是一个单例类,负责 Alert 的创建、组装(alertRule 和notification 的依赖注入)、初始化(添加 handlers)工作。
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));
}
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 类中添加新的属性 timeoutCount。
- 第二处改动是:添加新的 TimeoutAlertHander 类。
- 第三处改动是:在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler。
- 第四处改动是:在使用 Alert 类的时候,需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。
改动之后的代码如下所示:
public class Alert { // 代码未改动... }
public class ApiStatInfo {// 省略 constructor/getter/setter 方法
private String api;
private long requestCount;
private long errorCount;
private long durationOfSeconds;
private long timeoutCount; // 改动一:添加新字段
}
public abstract class AlertHandler { // 代码未改动... }
public class TpsAlertHandler extends AlertHandler {// 代码未改动...}
public class ErrorAlertHandler extends AlertHandler {// 代码未改动...}
// 改动二:添加新的 handler
public class TimeoutAlertHandler extends AlertHandler {// 省略代码...}
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));
// 改动三:注册 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);
}
修改代码就意味着违背开闭原则吗?
- 当有新需求增加时,首先什么都不改不变是不可能的,好的方案和代码改动量大小无关
- 个人理解修改那就真的是在改,扩展实质都是在新增(模块、类、方法、属性)
- 到底是算修改还是扩展也跟你思考的粒度和单元有关系,要尽量通过增加属性、方法、类的方式
- 不要改动方法的内部逻辑
如何做到“对扩展开放、修改关闭”?
偏向顶层的指导思想
- 写代码的时候,多思考未开可能有哪些需求变更,设计代码结构留好扩展点,新的代码可能很灵活地插入扩展点
- 代码可变部分封装起来,隔离变化
- 代码不变部分抽象出来,以后扩展新的实现即可
具体方法论
- 最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程。
- 比如,通过 Kafka 来发送异步消息:
- 所有上层系统都依赖这组抽象的接口编程
- 通过依赖注入的方式来调用
- 当我们要替换新的消息队列的时候,写一个新的消息队列实现即可
具体代码:
// 这一部分体现了抽象意识
public interface MessageQueue { //... }
public class KafkaMessageQueue implements MessageQueue { //... }
public class RocketMQMessageQueue implements MessageQueue {//...}
public interface MessageFromatter { //... }
public class JsonMessageFromatter implements MessageFromatter {//...}
public class ProtoBufMessageFromatter implements MessageFromatter {//...}
public class Demo {
private MessageQueue msgQueue; // 基于接口而非实现编程
public Demo(MessageQueue msgQueue) { // 依赖注入
this.msgQueue = msgQueue;
}
// msgFormatter:多态、依赖注入
public void sendNotification(Notification notification, MessageFormatter m
//...
}
}
如何在项目中灵活应用开闭原则?
- 写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。
- 业务导向的项目,要足够熟悉业务需求,才能知道当下和未开可能支持的业务需求。
- 如果跟业务无关,是偏底层的系统,需要了解系统将被如何使用,才能预估将来可能添加那些功能。
- 也不要过度设计,识别所有扩展点是不可能的,有些复杂的也可以到有需求驱动的时候,通过代码重构的方式实现。
- 代码的扩展性与可读性是冲突的,要根据实际情况权衡取舍。