

本文字数:3161字
预计阅读时间:20分钟

01
设计模式
设计模式的概念出自《Design Patterns - Elements of Reusable Object-Oriented Software》中文名是《设计模式 - 可复用的面向对象软件元素》,该书是在1994 年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合力完成。书中提出的面向对象的设计模式七大基本准则也是我们在平时编码时要广泛遵守的,它们包括:
1、开闭原则(Open Closed Principle,OCP)
对外扩展开放,对内修改关闭。就是说新增需求时尽量通过扩展新类实现,而不是对原有代码修改增删。
2、单一职责原则(Single Responsibility Principle, SRP)
每个类、每个方法的职责(目的)都是单一的。不要让一个类或方法做太多纵向关联或横向并列的事,否则会增加维护负担。
3、里氏代换原则(Liskov Substitution Principle,LSP)
继承必须确保超类所拥有的性质在子类中仍然成立。就是只要父类出现的地方,都可以用子类替换。
4、依赖倒转原则(Dependency Inversion Principle,DIP)
高层模块不应该依赖低层模块,二者都应该依赖其抽象。就是尽量使用接口来做标准和规范,降低耦合度。
5、接口隔离原则(Interface Segregation Principle,ISP)
接口的功能尽可能单一。一个接口只做一类行为或事物的标准。
6、合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
类间关系尽量使用聚合、组合来实现,如果不可以的话再使用继承。目的还是为了降低类间耦合度,类间关系有六种,它们的耦合度大小关系是:泛化>实现>组合>聚合>关联>依赖,我们在设计架构时,尽量让类间关系靠近右边(就是采用耦合度更小的关系)。
7、最少知道原则(Least Knowledge Principle,LKP)/迪米特法则(Law of Demeter,LOD)
一个对象要对其他对象的成员尽可能少的知道。目的就是降低类间耦合度,提高代码的可复用性。
02
责任链模式
责任链模式属于行为型设计模式,英文名称Chain of Responsibility,其定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链模式将对请求的处理过程划分为多个独立的处理节点,节点间互相不感知具体计算过程。处理节点存在前后顺序处理关系,也可以视具体的业务特点跳过某些节。
03
业务场景应用案例
1、业务背景
一个视频文件从用户电脑到发布到网络上通常要经历,上传、转码、审核、发布四个阶段,本文描述的业务场景适用于视频的审核与发布阶段。不同来源的视频审核策略不同,不同时期的视频审核策略不同,不同级别用户生成的视频审核策略不同等等。视频在审核阶段需要根据不同的审核策略进行处理。
2、不用责任链模式
在业务发展的初期,没有采用设计模式来指导编写代码。针对不同的审核策略处理视频的业务代码伪码如下:
go
if (videoInfo.getUploadFrom().equals("狐友") || videoInfo.getUploadFrom().equals("新闻")) {
}else{
}
if (videoInfo.getUploadFrom().equals("特邀作者上传") || videoInfo.getUploadFrom().equals("抓取来源")) {
} else{
}
if (videoInfo.getUploadFrom().equals("广告不适") || videoInfo.getUploadFrom().equals("严肃")) {
} else{
}
if ("命中MD5黑名单") {
} else{
}
if (videoInfo.getUploadFrom().equals("普通视频")) {
} else{
}
if (videoInfo.getUploadFrom().equals("免审标签") || videoInfo.getUploadFrom().equals("免审产品") || videoInfo.getUploadFrom().equals("免审用户")) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用图片检测")) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用音频检测")) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用文本检测")) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用全部类型AI检测")) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用风险预测")) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用基因检测")) {
} else{
}
if (now().between(time1, time2)&& isSensitiveAreaIp()) {
} else{
}
if (videoInfo.getUploadFrom().equals("适用产品策略检测")) {
} else{
}
代码使用了大量的if else语句,且如果随着业务的发展需要增加新的处理逻辑则又要对源码添加if else语句,违反了开闭原则,使得代码的可扩展性非常差。如果if else中的逻辑比较复杂,常常会由于增加新的逻辑而引入bug。这种业务代码特别适合使用责任链模型进行重构。
3、应用责任链模式
责任链模式的本质是将请求与处理解耦,允许将请求沿着处理器链进行发送。收到请求后,每个处理器均可对请求进行处理,或将其传递给链上的下个处理器。当新增处理逻辑时,不会对请求逻辑造成影响,两者互不干扰。示例图如下:

4、UML类图
通过对责任链模式的定义,需要定义 处理器接口或父类;具体的处理器类;请求接口或父类;具体的请求类;客户端类。但在实际的使用过程中,请求接口和请求类往往退化成一个request对象,在处理器链路里面传递,类图如下所示:

04
初级实现
我们根据uml类图,尝试一下如何应用责任链模式对原来的业务代码进行改造。首先定义一个Handler接口。
go
public interface Handler {
void handle(Object videoInfo);
void setNext(Handler handler);
}
然后构建三个具体的处理器实现类,实际工作中要对应原业务代码实现相应数量的处理器类,这里为了陈述方便,只实现三个处理器类。
1、处理器1:
go
public class Handler1 implements Handler {
private Handler next;
@Override
public void handle(Object videoInfo) {
System.out.println("handler1处理");
if (next != null) {
next.handle(videoInfo);
}
}
@Override
public void setNext(Handler handler) {
this.next = handler;
}
}
2、处理器2:
go
public class Handler2 implements Handler {
private Handler next;
@Override
public void handle(Object videoInfo) {
System.out.println("handler2处理");
if (next != null) {
next.handle(videoInfo);
}
}
@Override
public void setNext(Handler handler) {
this.next = handler;
}
}
3、处理器3:
go
public class Handler3 implements Handler {
private Handler next;
@Override
public void handle(Object videoInfo) {
System.out.println("handler3处理");
if (next != null) {
next.handle(videoInfo);
}
}
@Override
public void setNext(Handler handler) {
this.next = handler;
}
}
4、最后是客户端代码,client:
go
public class Client {
public static void main(String[] args) {
Handler handler1 = new Handler1();
Handler handler2 = new Handler2();
Handler handler3 = new Handler3();
handler1.setNext(handler2);
handler2.setNext(handler3);
handler1.handle("videoInfo");
}
}
go
打印结果为:
handler1处理
handler2处理
handler3处理
上面示例中的实现实际上是责任链模式的一个变种,即每个处理器都做出了自己的处理,没有中途终止的情况。而责任链的编排工作全部由客户端client实现,且每个处理器除了要关注自己的处理器代码外,还需要持有下一个处理器的引用,以串联成链,并且还负责对下一个处理器的调用。违反了职责单一原则,并不利于系统的维护与扩展,耦合性还是比较高的。
05
进阶实现
上面示例的实现,并不利于系统的扩展,后续再增加处理器,仍然需要对client的代码进行改动,以把新增加的处理器编排入链,增加了拼错或拼少处理器的风险,实际上是clent的设计没有遵循单一职责的原则,承接了不属于其的职责导致,我们看看如何进一步的改造,来解决这个问题。可以尝试增加一个处理器链路管理模块负责承接处理器的编排职责,将处理器的编排职责从client代码中解放出来,uml类图如下:

首先仍然需要先定义一个Handler接口:
1、Handler接口如下:
go
public interface Handler {
void handle(Object videoInfo);
}
这次接口只有一个handle方法了,去掉了setNext方法,因为责任链的编排交给链路管理类了,处理器的职责也变的单一了,只剩下了处理逻辑,十分简洁。
2、处理器1的实现:
go
public class Handler1 implements Handler {
@Override
public void handle(Object videoInfo) {
if (videoInfo.toString().contains("handler1")) {
System.out.println("handler1处理");
}
}
}
这次加入了一个条件用于演示,仅当传入的request对象包含了handler1字符串时,才触发处理器1的处理,否则跳过处理器1。
3、处理器2的实现:
go
public class Handler2 implements Handler {
private Handler next;
@Override
public void handle(Object videoInfo) {
System.out.println("handler2处理");
}
}
4、处理器3的实现:
go
public class Handler3 implements Handler {
private Handler next;
@Override
public void handle(Object videoInfo) {
System.out.println("handler3处理");
}
}
5、链路管理器HandlerChain的实现:
go
public class HandlerChain {
private List<Handler> list = new ArrayList<>();
public void add(Handler handler) {
this.list.add(handler);
}
public void handle(Object videoInfo) {
list.forEach(handler -> handler.handle(videoInfo));
}
}
链路管理器负责处理器的编排,在本示例中,只是简单的按顺序调用每一个处理器进行处理,在实际工作中会增加处理器的优先级特性,也可以结合apollo等配置中心实现动态的处理器编排。
6、客户端client的代码:
go
public class Client {
public static void main(String[] args) {
Handler handler1 = new Handler1();
Handler handler2 = new Handler2();
Handler handler3 = new Handler3();
HandlerChain handlerChain = new HandlerChain();
handlerChain.add(handler1);
handlerChain.add(handler2);
handlerChain.add(handler3);
handlerChain.handle("videoInfo");
}
}
go
运行结果为:
handler2处理
handler3处理
在这一个版本的实现中,客户端不再关心处理器的编排;处理器也不再持有下一个处理器的引用,只关注自身处理器业务代码的实现;把处理器的编排工作交给了链路管理器,随着业务增长需要新增处理器代码时,只需要修改链路管理器即可。但这似乎还是没有做到完全的遵守开闭原则,仍然需要修改链路管理器的源码,那么有没有一种实现可以完全做到遵守开闭原则呢?
06
最终实现
想要完全的遵守开闭原则,即当新增处理器代码时,无需对已有的系统源码进行修改,我们需要结合框架来实现。而在实际的工作中是通过结合spring框架的控制反转与依赖注入特性实现的。首先将处理器的实现类定义为spring的bean,利用控制反转特性,将处理器交由spring框架管理。
1、示例代码为:
go
@Component
public class Handler1 implements Handler {
@Override
public void handle(Object videoInfo) {
if (videoInfo.toString().contains("handler1")) {
System.out.println("handler1处理");
}
}
}
通过添加一个@Component注解,即可实现控制反转,将Handler1交由spring管理。其他处理器的实现类似。控制反转的特性也体现了依赖倒转原则,这里不再进一步讨论了。
然后利用依赖注入特性,将所有的控制器注入到链路管理器中。
2、示例代码为:
go
@Component
public class HandlerChain {
@Autowired
private List<Handler> list;
public void handle(Object videoInfo) {
list.forEach(handler -> handler.handle(videoInfo));
}
}
可以看到链路管理器代码新增了@Component注解将其自身交由spring管理,然后在list属性上新增了@Autowired注解,spring会把其管理的所有实现了Handler接口的bean自动注入到该list对象中,这里遵循了里氏代换原则。当新增一个handler实现时,只要其实现了Handler接口,该处理器就会在spring启动时自动注入到链路管理器的list属性中,从而实现了完全遵守开闭原则,即当新增处理器逻辑时,无需对系统原有代码进行修改,只需要扩展新的处理器代码即可。
而客户端client的实现也会更加精简:
3、示例代码为:
go
@Component
public class Client {
@Autowired
private HandlerChain handlerChain;
public void handle(Object videoInfo) {
handlerChain.handle(videoInfo);
}
}
同样的client也将自身交由spring管理,利用spring的控制反转与依赖注入特性实现了对链路管理器的调用。
07
总结
通过使用责任链模式并遵循开闭原则、单一职责等设计原则对视频审核与发布业务进行了重构,当业务新增处理器时,无需对原有的系统源码进行修改,只需要关注新增的处理器本身即可,极大的增加了系统的灵活性与扩展性,也提高了系统的可维护性,减少了对复杂业务编排导致出现bug的几率。