本文字數: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、不用責任鏈模式
在業務發展的初期,沒有采用設計模式來指導編寫代碼。針對不同的審核策略處理視頻的業務代碼偽碼如下:
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接口。
public interface Handler {void handle(Object videoInfo);void setNext(Handler handler);
}
然后構建三個具體的處理器實現類,實際工作中要對應原業務代碼實現相應數量的處理器類,這里為了陳述方便,只實現三個處理器類。
1、處理器1:
public class Handler1 implements Handler {
private Handler next;@Overridepublic void handle(Object videoInfo) {System.out.println("handler1處理");if?(next != null) {next.handle(videoInfo);}}@Overridepublic void setNext(Handler handler) {this.next = handler;}
}
2、處理器2:
public class Handler2 implements Handler {
private Handler next;@Overridepublic void handle(Object videoInfo) {System.out.println("handler2處理");if?(next != null) {next.handle(videoInfo);}}@Overridepublic void setNext(Handler handler) {this.next = handler;}
}
3、處理器3:
public class Handler3 implements Handler {
private Handler next;@Overridepublic void handle(Object videoInfo) {System.out.println("handler3處理");if?(next != null) {next.handle(videoInfo);}}@Overridepublic void setNext(Handler handler) {this.next = handler;}
}
4、最后是客戶端代碼,client:
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");}
}
打印結果為:
handler1處理
handler2處理
handler3處理
上面示例中的實現實際上是責任鏈模式的一個變種,即每個處理器都做出了自己的處理,沒有中途終止的情況。而責任鏈的編排工作全部由客戶端client實現,且每個處理器除了要關注自己的處理器代碼外,還需要持有下一個處理器的引用,以串聯成鏈,并且還負責對下一個處理器的調用。違反了職責單一原則,并不利于系統的維護與擴展,耦合性還是比較高的。
05
進階實現
上面示例的實現,并不利于系統的擴展,后續再增加處理器,仍然需要對client的代碼進行改動,以把新增加的處理器編排入鏈,增加了拼錯或拼少處理器的風險,實際上是clent的設計沒有遵循單一職責的原則,承接了不屬于其的職責導致,我們看看如何進一步的改造,來解決這個問題。可以嘗試增加一個處理器鏈路管理模塊負責承接處理器的編排職責,將處理器的編排職責從client代碼中解放出來,uml類圖如下:
首先仍然需要先定義一個Handler接口:
1、Handler接口如下:
public interface Handler {void handle(Object videoInfo);
}
這次接口只有一個handle方法了,去掉了setNext方法,因為責任鏈的編排交給鏈路管理類了,處理器的職責也變的單一了,只剩下了處理邏輯,十分簡潔。
2、處理器1的實現:
public class Handler1 implements Handler {@Overridepublic void handle(Object videoInfo) {if?(videoInfo.toString().contains("handler1")) {System.out.println("handler1處理");}}
}
這次加入了一個條件用于演示,僅當傳入的request對象包含了handler1字符串時,才觸發處理器1的處理,否則跳過處理器1。
3、處理器2的實現:
public class Handler2 implements Handler {
private Handler next;@Overridepublic void handle(Object videoInfo) {System.out.println("handler2處理");}
}
4、處理器3的實現:
public class Handler3 implements Handler {
private Handler next;@Overridepublic void handle(Object videoInfo) {System.out.println("handler3處理");}
}
5、鏈路管理器HandlerChain的實現:
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的代碼:
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");}
}
運行結果為:
handler2處理
handler3處理
在這一個版本的實現中,客戶端不再關心處理器的編排;處理器也不再持有下一個處理器的引用,只關注自身處理器業務代碼的實現;把處理器的編排工作交給了鏈路管理器,隨著業務增長需要新增處理器代碼時,只需要修改鏈路管理器即可。但這似乎還是沒有做到完全的遵守開閉原則,仍然需要修改鏈路管理器的源碼,那么有沒有一種實現可以完全做到遵守開閉原則呢??
06
最終實現
想要完全的遵守開閉原則,即當新增處理器代碼時,無需對已有的系統源碼進行修改,我們需要結合框架來實現。而在實際的工作中是通過結合spring框架的控制反轉與依賴注入特性實現的。首先將處理器的實現類定義為spring的bean,利用控制反轉特性,將處理器交由spring框架管理。
1、示例代碼為:
@Component
public class Handler1 implements Handler {@Overridepublic void handle(Object videoInfo) {if?(videoInfo.toString().contains("handler1")) {System.out.println("handler1處理");}}
}
通過添加一個@Component注解,即可實現控制反轉,將Handler1交由spring管理。其他處理器的實現類似。控制反轉的特性也體現了依賴倒轉原則,這里不再進一步討論了。
然后利用依賴注入特性,將所有的控制器注入到鏈路管理器中。
2、示例代碼為:
@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、示例代碼為:
@Component
public class Client {
@Autowired
private HandlerChain handlerChain;public void handle(Object videoInfo) {handlerChain.handle(videoInfo);}
}
同樣的client也將自身交由spring管理,利用spring的控制反轉與依賴注入特性實現了對鏈路管理器的調用。?
07
總結
通過使用責任鏈模式并遵循開閉原則、單一職責等設計原則對視頻審核與發布業務進行了重構,當業務新增處理器時,無需對原有的系統源碼進行修改,只需要關注新增的處理器本身即可,極大的增加了系統的靈活性與擴展性,也提高了系統的可維護性,減少了對復雜業務編排導致出現bug的幾率。