16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
开闭原则:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
开闭原则的定义,说明了开闭原则并不是有绝对衡量标准的。对业务的修改,是否满足开闭原则,取决于你对业务颗粒度的理解。
平时写的非扩展代码例子,比如告警功能,check()中包含各种达到阈值即告警的if逻辑判断,如果后续继续加阈值条件的话,1:可能check方法的参数首先就要变了,不够用,此处修改了通用方法的签名,改动很大 2:修改逻辑要追加到check()方法内部,check()的单元测试方法也要相应修改,并且逻辑会越堆越多。具体示例代码如下:
public class Alert {// ...省略AlertRule/Notification属性和构造函数...// 改动一:添加参数timeoutCountpublic 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, "...");}}}
修改后得代码如下:引入handler的概念,一个handler对应一个告警规则(新增类的方式实现扩展)。check()方法的参数改成对象参数,避免修改影响到之前方法调用(参数类内部实现扩展)。
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);}@Overridepublic 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);}@Overridepublic void check(ApiStatInfo apiStatInfo) {if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}}}
建议调用方式:
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() {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);}}
- 上述的例子并不绝对是优化后的更好,假如告警规则不多,后续也不会有新增的复杂逻辑,原先的写法会更好,代码量小、可读性强、开发成本也低。所以我们在做扩展的时候,需要考虑衡量点,对于短期、明确会扩展的做扩展设计,反之则等待下次驱动设计时候才做代码重构
- 识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改(基于接口而非实现编程)。以对接Kafka为例,我们要将功能抽象成与Kafka无关的接口,接口设计如下:这样后续就算替换掉Kafka换成Rocket MQ也对上层应用无感知 ```java
// 这一部分体现了抽象意识 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 msgFormatter) {
//…
}
}
```
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
控制反转(IOC)
此处不和Spring框架的IOC联系在一起。这里的”控制”是对程序流程的控制,而”反转”指的是没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员”反转”到了框架。
控制反转实现的方法有很多,除了类似模板方法设计模式之外,还有依赖注入等方法。
控制反转是一个比较笼统的设计思想,一般用来指导框架层面的设计。
依赖注入(DI)
依赖注入是不通过new()的方式在类内部创建依赖类对象,而是将依赖对象在外部创建好之后,通过构造函数、函数方法等方式传递(或注入)给类使用。
通过依赖注入方式,将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活的替换依赖的类。另外节约了大量的类对象的创建和依赖的代码开发。
依赖注入框架(DI Framework)
对于我们自己来实现依赖注入的方式,很多时候不过是把手动new()移到更上层代码而已,还是需要程序员自己来实现。
这个时候我们就需要依赖注入框架。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现框架自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事。
现成的依赖注入框架有很多,比如Google Guice、Java Spring、Pico Container、Buttefly Container。Spring框架自称是控制反转容器,其实只是钟宽泛的描述,Spring框架的控制反转主要通过依赖注入来实现的。
依赖反转原则(DIP)
也称依赖倒置原则。高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来相互依赖。除此之外,抽象不要依赖具体细节,具体实现细节依赖抽象。
Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
