<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          責任鏈模式,一門甩鍋的技術(shù)

          共 14900字,需瀏覽 30分鐘

           ·

          2022-11-17 15:36

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進!你不來,我和你的競爭對手一起精進!

          編輯:業(yè)余草

          來源:juejin.cn/post/6963266532621156389

          推薦:https://www.xttblog.com/?p=5357

          自律才能自由

          好的程序員他實現(xiàn)的不僅僅是一個業(yè)務(wù)功能,而是一件藝術(shù)品。

          這到底是誰的鍋

          我們在平常的業(yè)務(wù)開發(fā)中,經(jīng)常會遇到很多復雜的邏輯判斷,大概的框架可能是像下面這樣子的:

          public void request() {
              if (conditionA()) {
                  // ...
                  return;
              }

              if (conditionB()) {
                  throw new RuntimeException("xxx");
              }

              if (conditionC()) {
                  // do other things
              }
          }

          private boolean conditionC() {
              return false;
          }

          private boolean conditionB() {
              return false;
          }

          private boolean conditionA() {
              return true;
          }

          如果是簡單的、不多變的業(yè)務(wù),倒也沒什么大問題。但要是在比較核心的、復雜的業(yè)務(wù),且同一個系統(tǒng)的代碼有多人去維護,那么要在上面的這段代碼中插入一段邏輯將會非常困難。在實現(xiàn)時,你可能會遇到這些問題:

          • 實現(xiàn)前:我需要考慮這個邏輯分支,應(yīng)該加在什么地方才能滿足要求?會不會被前置條件攔截了?
          • 實現(xiàn)時:邏輯分支要用到一些函數(shù)參數(shù),這些參數(shù)會不會對后續(xù)的邏輯有影響?而且參數(shù)會不會被前面的哪些參數(shù)給修改掉了?這樣我就得去了解前后所有的判斷邏輯才能正確實現(xiàn)我要的功能,不然就是要碰碰運氣,賭它沒改過了...
          • 實現(xiàn)后:加完之后,我要怎么測試?貌似還得構(gòu)造條件讓前面的判斷都通過才行...還是要了解前置條件的邏輯

          如果放到真實的業(yè)務(wù)場景,遇到的問題可能還不止這些。不禁感嘆,我就想多加一個邏輯分支!怎么就這么難!

          有什么辦法解決這些問題呢?顯然,當要實現(xiàn)一個功能時,需要了解的細節(jié)太多了,不符合單一職責的原則。無論是新增邏輯還是修改邏輯,都是有很強的侵入性的,也不符合開閉原則。我前后的邏輯細節(jié)不是我負責的,我要把這些鍋甩出去才行,要更好地甩鍋,那么這時候就要用到責任鏈模式了。

          甩鍋的套路

          責任鏈,顧名思義,是一個鏈條,鏈條中有很多個節(jié)點。映射到數(shù)據(jù)結(jié)構(gòu)上,則是一個有序的隊列,隊列中有很多個元素,每個元素都獨立處理自己的邏輯,并且在處理完之后,將流程傳遞到下一個節(jié)點。所以,在這個模式里,可以抽象出兩個角色:鏈和節(jié)點。其中,鏈負責處理請求和組裝節(jié)點,而每個節(jié)點則負責處理自己的業(yè)務(wù)邏輯,無需關(guān)心這個節(jié)點的上下游是如何處理的。

          因此,從用例的視角來看,可以得出下面的用例圖:

          用例圖

          那么,責任鏈是否可以解決上述的問題呢?上述問題,其實對應(yīng)著下面的幾個問題:

          • 要滿足需求,應(yīng)該在什么地方實現(xiàn)需求?
          • 實現(xiàn)這個需求會不會對其他模塊帶來影響,其他模塊又會不會對我實現(xiàn)的邏輯帶來什么影響?
          • 需求實現(xiàn)后該如何測試?

          由責任鏈的角色劃分可以很清楚地知道:

          • 對于第一個問題,應(yīng)該是鏈條這個角色應(yīng)該關(guān)心的,從業(yè)務(wù)的視角來安排節(jié)點在哪個位置實現(xiàn)即可。
          • 對于第二個問題,需求的實現(xiàn)由節(jié)點負責。對于責任鏈中的入?yún)ⅲ惶峁┳x方法,不提供寫方法,這樣可以很好地避免某個節(jié)點偷偷篡改參數(shù)的風險,對于其他節(jié)點來說,無需擔心其他節(jié)點對入?yún)⑦M行了修改。每個節(jié)點之間的職責分明,由責任鏈本身的結(jié)構(gòu)就決定了模塊之間的影響很小。
          • 對于第三個問題,節(jié)點的邏輯實現(xiàn)后,只需對節(jié)點邏輯本身做測試,至于能否邏輯能否執(zhí)行到這個節(jié)點上,則由鏈條設(shè)置的節(jié)點順序做保證。測試時只需保證順序正確即可,完全沒有必要從請求開始的地方開始執(zhí)行,構(gòu)造一堆條件讓代碼執(zhí)行到自己的邏輯上。

          上面提到的問題,都可以利用責任鏈模式很好地解決這些問題。那又該如何實現(xiàn)責任鏈模式呢?

          是時候展現(xiàn)真正的甩鍋技術(shù)了

          按照上面用例圖的定義,鏈條負責管理節(jié)點,是請求的入口,而節(jié)點是鏈條的其中一環(huán)。那么這兩者的關(guān)系屬于聚合關(guān)系。得到類圖如下:

          類圖

          甩鍋秘技一

          如果我們在Spring框架的基礎(chǔ)上進行開發(fā),那么我們很容易就可以實現(xiàn)一個簡單的責任鏈模式:

          @Component
          @RequiredArgsConstructor
          public class PolicyChain1 {

              private final List<Policy<ContextParams, String>> policies;

              public void filter(ContextParams contextParams) {
                  policies.forEach(policy -> {
                      policy.filter(contextParams);
                  });
              }
          }

          只要將 Policy 的實現(xiàn)類也標記為 Component,那么 Spring 的自動注入機制幫我們實現(xiàn)了 addPolicy 的方法,擺脫了繁瑣的添加節(jié)點的過程代碼。

          但是,這有一個很嚴重的問題,要怎么控制每個 Policy 之間的順序呢?這時可能你會想到用@Order注解解決這個問題。但是假設(shè) Policy 有幾十個,如果你需要在第 10 和第 11 個 Policy 插入一個 Policy,那么是不是要將從第 11 個開始往后的所有 Policy 都調(diào)整一下順序?想想都覺得麻煩。因此,這種方式,只能用于對順序無要求的情況,比如用來做權(quán)限校驗時,各個校驗條件互不相關(guān),也無先后順序的限制,那么就可以用這種方法實現(xiàn),擴展性強,實現(xiàn)也簡單。

          甩鍋秘技二

          可是需求上要求一定要按順序,那該怎么辦呢?上面已經(jīng)分析了指定 Order 的方式不可取,還有什么方法呢?

          其實上面的方法其實和插入一個數(shù)組時的操作十分相似。當要在數(shù)組中間插入一個元素時,插入位置之后的元素都要往后挪一位。對應(yīng)上述的做法,其實就是對應(yīng)的 Policy 的 Order 值要加 1。那么類似地,數(shù)組對插入的效率低,那換個效率高的做法,不就是鏈表么?我們可以將每個 Policy 都持有下一個要處理的 Policy 的引用,當這個 Policy 處理完之后,調(diào)用下一個 Policy 的 filter 方法,然后再將上一個 Policy 的引用修改一下,不就可以很好地完成插入操作了么?

          先畫一個類圖

          類圖

          這樣組織之后,PolicyA 需要持有 PolicyB 的引用,PolicyB 也需要持有 PolicyC 的引用。當需要在 B 和 C 之間加入一個 D 時,那么我就需要將 B 中的引用指向 D,然后 D 再指向 C 即可。

          但是,這樣組織之后,我并不知道這個鏈條的全貌,這個鏈條有哪些節(jié)點、順序是怎樣的,我并不能一下子推斷出來了。另外,這和上面推斷出來的用例圖不符,在用例中,鏈條才是負責節(jié)點的組裝的,現(xiàn)在相當于甩給了每個節(jié)點去做了,這明顯違反了單一職責原則啊!

          既然這樣,那我仍然把節(jié)點組裝放到鏈條里實現(xiàn),節(jié)點只實現(xiàn)邏輯,只是在組裝的時候,可以讓使用方顯式指定順序,這樣不就好了嗎?

          大概的實現(xiàn)是這樣:

          @Component
          @AllArgsConstructor
          public class PolicyChain2 {

              private SessionJoinDeniedPolicyHandler sessionJoinDeniedPolicyHandler;

              private SessionLockPolicyHandler sessionLockPolicyHandler;

              private SessionPasswordPolicyHandler sessionPasswordPolicyHandler;

              @PostConstruct
              public void init() {
                  sessionLockPolicyHandler.setNextHandler(sessionJoinDeniedPolicyHandler);
                  sessionJoinDeniedPolicyHandler.setNextHandler(sessionPasswordPolicyHandler);
              }

              public void filter(ContextParams params) {
                  sessionLockPolicyHandler.filter(params);
              }
          }

          這樣鏈條本身就需要知道各個節(jié)點都是什么,這樣才能把不同的節(jié)點組裝起來。

          // 策略抽象類
          public abstract class PolicyHandler<T{

              private PolicyHandler<T> nextHandler;

              void setNextHandler(PolicyHandler<T> handler) {
                  nextHandler = handler;
              }

              public void filter(T context) {
                  doFilter(context);
                  if (nextHandler != null) {
                      nextHandler.filter(context);
                  }
              }

              protected abstract void doFilter(T context);
          }

          // 策略實現(xiàn)類
          @Component
          public class SessionPasswordPolicyHandler extends PolicyHandler<ContextParams{

              @Override
              public void doFilter(ContextParams context) {
                  String requestParam = context.getRequestParam();
                  if (Objects.equals(requestParam, "ok")) {
                      return;
                  }

                  throw new RuntimeException("session password throw exception");
              }
          }

          對于節(jié)點本身,就只需要關(guān)注自身處理的業(yè)務(wù)邏輯了,使用方只要調(diào)用一下 PolicyChain 的 filter 方法,接下來的邏輯都會自動按順序完成了!

          看來這樣的實現(xiàn)差不多就可以滿足需求了!直到...我用這個秘技實現(xiàn)了一個計數(shù)器的時候...

          需求是這樣的,為了減少數(shù)據(jù)庫的壓力,我在一個加入房間的方法上加了一個注解,并用秘技二實現(xiàn)了一個計數(shù)器,以用于校驗加入的人數(shù)是否超出了房間限制的大小,這樣可以減少對數(shù)據(jù)庫的查詢次數(shù)。實現(xiàn)代碼大致如下:

          @ValidatePolicy
          public void filter() {
              join();
          }

          // validate注解對應(yīng)的攔截方法,此處省略了切面類的相關(guān)代碼,僅展示核心內(nèi)容
          public void validate() {
              strategyRouter.applyStrategy(ContextParams.builder()
                      .isJoinDenied(false)
                      .isLocked(false)
                      .password("123")
                      .build());
          }

          直到有一天,房間限制 3 人加入,此時房間里有 2 個人,執(zhí)行 join 方法,validate() 方法愉快地通過了校驗并將自身的計數(shù)器設(shè)置為 3,但是在執(zhí)行 join 方法的時候拋出了異常,原本應(yīng)該加入成功的第3個人并沒有加入成功。接著第 4 個人加入房間,因為房間內(nèi)只有 2 個人,那么第 4 個人應(yīng)該是加入成功的,但是因為計數(shù)器已經(jīng)被設(shè)置為 3,那么第 4 個人直接在校驗階段就拋異常了...

          那么,在秘技 2 的基礎(chǔ)上,當執(zhí)行后面的方法出異常時捕獲異常,然后把計數(shù)器校正就好了!可是,在這種實現(xiàn)方式下根本做不到,因為每個節(jié)點只專注于處理自己成功攔截時的邏輯,而忽略了自身邏輯處理完之后,后續(xù)邏輯出了異常時該怎么辦的情況。由此可得,秘技二能處理有順序的節(jié)點,能用于無狀態(tài)的前置校驗,但無法支持后續(xù)邏輯出現(xiàn)異常時,節(jié)點本身還需要處理回滾操作的情況。

          甩鍋秘技三

          基于上面的問題,我需要找到一種能支持回滾的實現(xiàn)方式。這時 我參考了 Spring Cloud Gateway 中 Filter 的實現(xiàn),發(fā)現(xiàn)有幾個特點:

          • 每個節(jié)點會依賴鏈條本身,當要執(zhí)行下一個節(jié)點的處理邏輯時,只需要調(diào)用chain.filter()方法即可。
          • 將節(jié)點順序的定義和節(jié)點的創(chuàng)建分開,避免了鏈條對具體節(jié)點的依賴,對節(jié)點的創(chuàng)建,可以通過工廠模式實現(xiàn),增強了擴展性。

          大致類圖如下:

          類圖

          首先我們看如何支持順序。在 FilterRouter 中,有一個 loadFilterDefinitions 的方法,子類可以重寫這個方法以定義責任鏈中存在哪些節(jié)點。鏈條本身變得不關(guān)心節(jié)點的順序了,轉(zhuǎn)而將節(jié)點順序的處理委托給另一個對象。同時,除了可以支持在 FilterRouter 用代碼顯式定義之外,還可以通過重寫 loadFilterDefinitions 的方式,從不同的來源指定節(jié)點順序,比如配置文件、外部系統(tǒng)等,使得順序的定義更靈活,擴展性更強。

          @RequiredArgsConstructor
          public abstract class FilterRouter<TR{

              private final Map<String, FilterFactory<T, R>> filterFactories;

              public List<Filter<T, R>> getFilters(T filterChainContext) {
                  final List<FilterDefinition> filterDefinitions = new ArrayList<>();
                  loadFilterDefinitions(filterChainContext, filterDefinitions);
                  List<Filter<T, R>> filters = filterDefinitions.stream().map(filterDefinition -> {
                      FilterFactory<T, R> filterFactory = filterFactories.get(filterDefinition.getName());
                      return filterFactory.apply();
                  }).collect(Collectors.toList());
                  filterDefinitions.clear();
                  return filters;
              }

              protected abstract void loadFilterDefinitions(T filterChainContext, List<FilterDefinition> filterDefinitions);
          }

          @Component
          public class DefaultFilterRouter extends FilterRouter<StringString{


              public DefaultFilterRouter(Map<String, FilterFactory<String, String>> filterFactories) {
                  super(filterFactories);
              }

              @Override
              protected void loadFilterDefinitions(String filterChainContext, List<FilterDefinition> filterDefinitions) {
                  filterDefinitions.add(new FilterDefinition(PasswordFilterFactory.KEY));
              }
          }

          接下來我們看下節(jié)點操作如何支持回滾。通過實現(xiàn)FilterFactory接口,可以在 apply 方法中執(zhí)行自身的校驗邏輯,并對后續(xù)的處理捕獲異常,當捕獲到異常時,在異常處理的代碼塊中處理回滾異常。另外,借助 Spring 框架的自動注入,將 Factory 聲明為 Component,這樣 FilterRouter 在收集 Filter 實現(xiàn)時,也免除了繁瑣的 add 方法。

          @Component
          public class PasswordFilterFactory implements FilterFactory<StringString{

              public static final String KEY = "passwordFilterFactory";

              @Override
              public Filter<String, String> apply() {
                  return (filterChainContext, filterChain) -> {
                      // validate
                      try {
                          return filterChain.filter(filterChainContext);
                      } catch (Exception e) {
                          // rollback
                      }

                      return "";
                  };
              }
          }

          至于 DefaultFilterChain 這個類,做的事情就是接收請求,將通過 FilterRouter 的 FilterFactory 生成 Filter 列表而已。代碼如下:

          public class DefaultFilterChain<TRimplements FilterChain<TR{

              private final T filterChainContext;

              private int index = 0;

              private final List<Filter<T, R>> filters = new ArrayList<>();

              public DefaultFilterChain(FilterRouter<T, R> filterRouter, T filterChainContext) {
                  this.filterChainContext = filterChainContext;
                  filters.addAll(filterRouter.getFilters(filterChainContext));
              }

              public R filter() throws Throwable {
                  return filter(filterChainContext);
              }

              @Override
              public R filter(T filterChainContext) throws Throwable {
                  int size = filters.size();
                  if (this.index < size) {
                      Filter<T, R> filter = filters.get(this.index);
                      index++;
                      return filter.filter(filterChainContext, this);
                  }

                  return null;
              }

              public void addLastFilter(Filter<T, R> filter) {
                  filters.add(filter);
              }
          }

          使用時的代碼:

          @Component
          @RequiredArgsConstructor
          public class Client {

              private final DefaultFilterRouter defaultFilterRouter;

              public void filter(String param) throws Throwable {
                  DefaultFilterChain<String, String> filterChain = new DefaultFilterChain<>(defaultFilterRouter, param);
                  filterChain.filter(param);
              }
          }

          至此,最后一種實現(xiàn)方式,既可以滿足對節(jié)點順序性的要求,也可以支持節(jié)點對后續(xù)邏輯出錯時的后置處理。同時也具備比較好的擴展性,可以實現(xiàn)從不同來源加載節(jié)點順序,可以通過 FilterFactory 實現(xiàn)不同的 Filter。接著將第 3 種秘技封裝成組件,這樣業(yè)務(wù)在接入的時候就可以優(yōu)雅甩鍋了。

          甩鍋總結(jié)

          上述列舉了 3 種責任鏈模式的實現(xiàn)方式,可以分別應(yīng)對三種場景:

          • 對節(jié)點順序無要求,可用秘技一,實現(xiàn)方式比較簡單
          • 對節(jié)點順序有要求,且所有節(jié)點的處理都是無狀態(tài)的,不需要進行后置處理的,可使用秘技二
          • 對節(jié)點順序有要求,且有其中一個節(jié)點的處理是有狀態(tài)的,需要進行后置處理的,可使用秘技三

          設(shè)計模式經(jīng)典書籍《設(shè)計模式:可復用面向?qū)ο筌浖幕A(chǔ)》中有一句話提到,“找到變化,封裝變化”。其實這是設(shè)計模式的底層邏輯。

          回顧整個過程,我們可以看到:

          • 從流水賬式的代碼,到秘技一,變化的是新增一段插入邏輯,最終封裝的效果,正是讓這段插入邏輯變成了其中一個節(jié)點的處理邏輯。
          • 從秘技一到秘技二,變化的是需要支持節(jié)點順序,而最終封裝的效果,則是將順序的定義內(nèi)聚在了鏈條的內(nèi)部,支持了自定義順序。
          • 從秘技二到秘技三,變化的是節(jié)點需要支持回滾,支持后置處理,而封裝的結(jié)果,就是將后續(xù)的處理的邏輯暴露給節(jié)點,但節(jié)點依賴的是鏈條本身,將后續(xù)的處理邏輯屏蔽起來,節(jié)點依然聚焦在自身的處理邏輯上。

          由此可見,過程式的代碼,到設(shè)計模式的演進,都并不是憑空捏造的,而是「由問題出發(fā),找到其核心的變化點,并對變化點進行封裝和抽象」,才慢慢形成最終比較理想的結(jié)果。

          瀏覽 75
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天操夜夜操天天日 | 小黄片视频免费观看 | 什么网址可以在线看国产毛片 | 久久精品黄色电影 | 骚逼五月婷婷影院 |