第二部分:理论二

第二部分:理论二

如何理解“对扩展开放、修改关闭”?

开闭原则

  • 开闭原则:SOLID 中的第二个原则,英文全称是 Open Closed Principle,简写为OCP。
  • 开闭原则是 SOLID 中最难理解、最难掌握,同时也是最有用的一条原则。
  • 对扩展开放、修改关闭,直接影响代码的扩展性,扩展性是代码质量最重要的衡量标准之一。

项目实战

  • API 接口监控告警的代码
  • Alert类:
    • 属性两个:AlertRule 存储告警规则,Notification 是告警通知类
    • 方法:check()主要业务逻辑两种报警场景
  • 此时要添加一种报警场景,修改:
    1. check()增加一个参数
    2. 在check()方法内部增加新报警场景的逻辑代码
    3. 修改单元测试
  • 用扩展的方案,要先重构Alert类:
    • 将check()方法的多参数,封装成ApiStatInfo 类
    • 引入handler概念,将多种报警场景放在各handler中
  • 此时要添加一种报警场景,扩展:
    1. 在ApiStatInfo 类中新增属性
    2. 添加一个新的报警场景的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);
}

修改代码就意味着违背开闭原则吗?

  1. 当有新需求增加时,首先什么都不改不变是不可能的,好的方案和代码改动量大小无关
  2. 个人理解修改那就真的是在改,扩展实质都是在新增(模块、类、方法、属性)
  3. 到底是算修改还是扩展也跟你思考的粒度和单元有关系,要尽量通过增加属性、方法、类的方式
  4. 不要改动方法的内部逻辑

如何做到“对扩展开放、修改关闭”?

偏向顶层的指导思想

  1. 写代码的时候,多思考未开可能有哪些需求变更,设计代码结构留好扩展点,新的代码可能很灵活地插入扩展点
  2. 代码可变部分封装起来,隔离变化
  3. 代码不变部分抽象出来,以后扩展新的实现即可

具体方法论

  1. 最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程。
  2. 比如,通过 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
		//...
	}
}

如何在项目中灵活应用开闭原则?

  1. 写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。
  2. 业务导向的项目,要足够熟悉业务需求,才能知道当下和未开可能支持的业务需求。
  3. 如果跟业务无关,是偏底层的系统,需要了解系统将被如何使用,才能预估将来可能添加那些功能。
  4. 也不要过度设计,识别所有扩展点是不可能的,有些复杂的也可以到有需求驱动的时候,通过代码重构的方式实现。
  5. 代码的扩展性与可读性是冲突的,要根据实际情况权衡取舍。
上一篇:关于联网和通知


下一篇:Laravel 自定义通知频道,至 企业微信机器人