【趣味設計模式系列】之【責任鏈模式】

1. 簡介

責任鏈模式(Chain of Responsibility):使多個對象都有機會處理請求,從而避免了請求的發送者接受者之間的耦合關係。將這些對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有對象處理它爲止。java

2. 圖解

商城新開張,每一個訂單,能夠享受多張優惠券疊加減免
程序員

責任鏈模式
web

3. 案例實現

類圖以下
tomcat

  • 定義一個優惠券打折抽象類;
  • 抽象類包含一個指向自身的引用nextDiscountFilter,用來把對象串成鏈,原價計算優惠後的價格方法calculateBySourcePrice;

實現類
app

  • FullDistcountFliter滿200減20元;
  • FirstPurchaseDiscount首次購買減20元;
  • SecondPurchaseDiscountFilter第二件打9折;
  • HolidayDiscountFilter節日一概減5元.

接口與實現類ide

package com.wzj.chainOfResponsibility.example2;

/**
 * @Author: wzj
 * @Date: 2019/9/8 9:06
 * @Desc: 折扣優惠接口
 */
public abstract class DiscountFilter {

    // 下一個責任鏈成員
    protected DiscountFilter nextDiscountFilter;

    // 根據原價計算優惠後的價格
    public abstract int calculateBySourcePrice(int price);

}
package com.wzj.chainOfResponsibility.example2;

/**
 * @Author: wzj
 * @Date: 2019/9/8 9:07
 * @Desc: 滿200減20元
 */
public class FullDiscountFilter extends DiscountFilter{

    public int calculateBySourcePrice(int price) {
        if (price > 200){
            System.out.println("優惠滿減20元");
            price = price - 20;
        }

        if(this.nextDiscountFilter != null) {
            return super.nextDiscountFilter.calculateBySourcePrice(price);
        }
        return price;
    }
}
package com.wzj.chainOfResponsibility.example2;

/**
 * @Author: wzj
 * @Date: 2019/9/8 16:06
 * @Desc: 首次購買減20元
 */
public class FirstPurchaseDiscount extends DiscountFilter {
    public int calculateBySourcePrice(int price) {
        if (price > 100){
            System.out.println("首次購買減20元");
            price = price - 20;
        }

        if(this.nextDiscountFilter != null) {
            return super.nextDiscountFilter.calculateBySourcePrice(price);
        }
        return price;
    }
}
package com.wzj.chainOfResponsibility.example2;

/**
 * @Author: wzj
 * @Date: 2019/9/8 16:09
 * @Desc: 第二件打9折
 */
public class SecondPurchaseDiscountFilter  extends DiscountFilter{
    public int calculateBySourcePrice(int price) {

        System.out.println("第二件打9折");
        Double balance =  price * 0.9;

        if(this.nextDiscountFilter != null) {
            return super.nextDiscountFilter.calculateBySourcePrice(balance.intValue());
        }
        return price;
    }
}
package com.wzj.chainOfResponsibility.example2;

/**
 * @Author: wzj
 * @Date: 2019/9/8 16:02
 * @Desc: 節日一概減5元
 */
public class HolidayDiscountFilter extends DiscountFilter{
    public int calculateBySourcePrice(int price) {
        if (price > 20){
            System.out.println("節日一概減5元");
            price = price - 5;
        }

        if(this.nextDiscountFilter != null) {
            return super.nextDiscountFilter.calculateBySourcePrice(price);
        }
        return price;
    }
}

測試類源碼分析

package com.wzj.chainOfResponsibility.example2;

/**
 * @Author: wzj
 * @Date: 2019/9/8 16:17
 * @Desc: 
 */
public class TestDiscountFilter {
    public static void main(String[] args) {
        int price = 240;
        String productStr = String.format("商品清單:蘋果、香蕉、桔子, 商品總金額爲:%d元.", price);
        System.out.println(productStr);
        //聲明責任鏈上的全部節點
        FullDiscountFilter fulDF = new FullDiscountFilter();
        FirstPurchaseDiscount firstDF = new FirstPurchaseDiscount();
        SecondPurchaseDiscountFilter secDF = new SecondPurchaseDiscountFilter();
        HolidayDiscountFilter holDF = new HolidayDiscountFilter();
        //設置鏈中的順序:滿減->首購減->第二件減->假日減
        fulDF.nextDiscountFilter = firstDF;
        firstDF.nextDiscountFilter = secDF;
        secDF.nextDiscountFilter = holDF;
        holDF.nextDiscountFilter = null;
        int total = fulDF.calculateBySourcePrice(price);
        System.out.println(String.format("全部商品優惠價後金額爲:%d", total));
    }


}

執行結果性能

商品清單:蘋果、香蕉、桔子, 商品總金額爲:240元.
優惠滿減20元
首次購買減20元
第二件打9折
節日一概減5元
全部商品優惠價後金額爲:175

4. 應用責任鏈模式手寫過濾器

如今有這樣一個場景,程序員張三在某相親節目中找對象,在第一關和第二關分別被女孩滅燈pass掉,下面分別看看倆姑娘是如何過濾張三的。
小美對話張三
測試


小靜對話張三

此場景能夠模擬一個http請求如何通過過濾器到服務端,通過每個過濾器,能夠想象爲張三相親過程當中通過某個關卡,因爲不符合該過濾器的條件被姑娘過濾掉。
this

第一個版本v1

  • 定義一個請求類和一個響應類MyRequestMyResponse
  • 定義過濾器MyFilter
  • 定義兩個過濾器的實現類HeightFliterEducationalBackGroundFilter,分別實現身高過濾和學歷過濾;
  • 定義鏈條MyFilterChain,實現接口MyFilteradd方法實現往過濾鏈條裏添加過濾對象;doFilter方法實現全部過濾對象的過濾操做;

實現代碼以下

package com.wzj.chainOfResponsibility.example1.v1;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:14
 * @Desc: 請求
 */
public class MyRequest {

    String str;

}
package com.wzj.chainOfResponsibility.example1.v1;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:15
 * @Desc: 響應
 */
public class MyResponse {

    String str;

}
package com.wzj.chainOfResponsibility.example1.v1;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:00
 * @Desc: 模擬實現過濾器
 */
public interface MyFilter {

    void doFilter(MyRequest myRequest, MyResponse myResponse);

}
package com.wzj.chainOfResponsibility.example1.v1;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:36
 * @Desc: 責任鏈
 */
public class MyFilterChain implements MyFilter {

    List<MyFilter> list = new ArrayList<MyFilter>();

    public MyFilterChain add(MyFilter myFilter) {
        list.add(myFilter);
        return this;
    }

    public void doFilter(MyRequest myRequest, MyResponse myResponse) {
        for(MyFilter f : list ){
            f.doFilter(myRequest, myResponse);
        }
    }
}
package com.wzj.chainOfResponsibility.example1.v1;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:20
 * @Desc: 身高過濾器
 */
public class HeightFliter implements MyFilter {

    public void doFilter(MyRequest myRequest, MyResponse myResponse) {
        myRequest.str = myRequest.str.replace("170", "個子有點矮");
        myResponse.str += "【妹子挑剔,須要過濾身高】";
    }
}
package com.wzj.chainOfResponsibility.example1.v1;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:33
 * @Desc: 教育背景過濾器
 */
public class EducationalBackGroundFilter implements MyFilter {
    public void doFilter(MyRequest myRequest, MyResponse myResponse) {
        myRequest.str = myRequest.str.replace("學歷大專", "學歷不高");
        myResponse.str += "【妹子挑剔,須要過濾學歷】";
    }
}

測試類

package com.wzj.chainOfResponsibility.example1.v1;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:42
 * @Desc: 直觀的方式處理response,
 * 並將response的處理放在request的下面
 */
public class TestV1 {
    public static void main(String[] args) {
        MyRequest myRequest = new MyRequest();
        myRequest.str = "張三身高170,學歷大專,跪求妹子給個機會認識";
        System.out.println("request:" + myRequest.str);
        MyResponse myResponse = new MyResponse();
        myResponse.str = "";
        MyFilterChain chain = new MyFilterChain();
        chain.add(new HeightFliter()).add(new EducationalBackGroundFilter());
        chain.doFilter(myRequest, myResponse);
        System.out.println("response:" + myResponse.str);
    }

}

結果

request:張三身高170,學歷大專,跪求妹子給個機會認識
response:【妹子挑剔,須要過濾身高】【妹子挑剔,須要過濾學歷】

如今有以下需求,爲更好的模擬一次完整請求,在過濾請求時順序,響應請求時逆序,有何辦法能夠作到呢?

第二個版本v2

  • MyRequest類和MyResponse同v1
  • MyFilterChain中加入記錄具體責任鏈對象的下標index,實現記錄位置功能;
  • doFilter方法裏面實現遞歸調用,並把當前的鏈條MyFilterChain做爲參數一直向後傳遞;
    具體實現以下
package com.wzj.chainOfResponsibility.example1.v2;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:00
 * @Desc: 模擬實現過濾器
 */
public interface MyFilter {

    void doFilter(MyRequest myRequest, MyResponse myResponse, MyFilterChain myFilterChain);

}
package com.wzj.chainOfResponsibility.example1.v2;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:36
 * @Desc: 在MyFilterChain中處理加入位置的記錄index
 */
public class MyFilterChain implements MyFilter {

    List<MyFilter> list = new ArrayList<MyFilter>();

    int index = 0;

    public MyFilterChain add(MyFilter myFilter) {
        list.add(myFilter);
        return this;
    }

    public void doFilter(MyRequest myRequest, MyResponse myResponse, MyFilterChain myFilterChain) {
        if(index == list.size())
            return;
        MyFilter myFilter = list.get(index);
        index ++;
        myFilter.doFilter(myRequest, myResponse, myFilterChain);
    }
}
package com.wzj.chainOfResponsibility.example1.v2;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:20
 * @Desc: 身高過濾器
 */
public class HeightFliter implements MyFilter {

    public void doFilter(MyRequest myRequest, MyResponse myResponse, MyFilterChain myFilterChain) {
        myRequest.str = myRequest.str.replace("170", "個子有點矮");
        myFilterChain.doFilter(myRequest, myResponse, myFilterChain);
        myResponse.str += "【妹子挑剔,須要過濾身高】";
    }
}
package com.wzj.chainOfResponsibility.example1.v2;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:33
 * @Desc: 教育背景過濾器
 */
public class EducationalBackGroundFilter implements MyFilter {
    public void doFilter(MyRequest myRequest, MyResponse myResponse, MyFilterChain myFilterChain) {
        myRequest.str = myRequest.str.replace("學歷大專", "學歷不高");
        myFilterChain.doFilter(myRequest, myResponse, myFilterChain);
        myResponse.str += "【妹子挑剔,須要過濾學歷】";
    }
}

測試類

package com.wzj.chainOfResponsibility.example1.v2;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:42
 * @Desc: 過濾請求時順序,響應請求時逆序,在MyFilterChain中處理加入位置的記錄,
 * 同時在MyFilter中加入第三個參數MyFilterChain,讓鏈條遞歸實現倒序
 */
public class TestV2 {
    public static void main(String[] args) {
        MyRequest myRequest = new MyRequest();
        myRequest.str = "張三身高170,學歷大專,跪求妹子給個機會認識";
        System.out.println("request:" + myRequest.str);
        MyResponse myResponse = new MyResponse();
        myResponse.str = "";
        MyFilterChain chain = new MyFilterChain();
        chain.add(new HeightFliter()).add(new EducationalBackGroundFilter());
        chain.doFilter(myRequest, myResponse, chain);
        System.out.println("response:" + myResponse.str);
    }

}

結果中顯示先過濾學歷,後過濾身高。

request:張三身高170,學歷大專,跪求妹子給個機會認識
response:【妹子挑剔,須要過濾學歷】【妹子挑剔,須要過濾身高】

因爲MyFilterChain在作doFilter時,始終傳遞的是當前MyFilterChain的同一個實例,故能夠簡化MyFilterChain

第三個版本v3

  • MyFilterChain去除MyFilter接口;
  • doFilter方法去除第三個參數MyFilterChain
    具體實現以下
package com.wzj.chainOfResponsibility.example1.v3;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:36
 * @Desc: 過濾器徹底模式,去掉實現接口,並將doFilter方法中的chain
 */
public class MyFilterChain{

    List<MyFilter> list = new ArrayList<MyFilter>();

    int index = 0;

    public MyFilterChain add(MyFilter myFilter) {
        list.add(myFilter);
        return this;
    }

    public void doFilter(MyRequest myRequest, MyResponse myResponse) {
        if(index == list.size())
            return;
        MyFilter myFilter = list.get(index);
        index ++;
        myFilter.doFilter(myRequest, myResponse, this);
    }
}
package com.wzj.chainOfResponsibility.example1.v3;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:20
 * @Desc: 身高過濾器
 */
public class HeightFliter implements MyFilter {

    public void doFilter(MyRequest myRequest, MyResponse myResponse, MyFilterChain myFilterChain) {
        myRequest.str = myRequest.str.replace("170", "個子有點矮");
        myFilterChain.doFilter(myRequest, myResponse);
        myResponse.str += "【妹子挑剔,須要過濾身高】";
    }
}
package com.wzj.chainOfResponsibility.example1.v3;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:33
 * @Desc: 教育背景過濾器
 */
public class EducationalBackGroundFilter implements MyFilter {
    public void doFilter(MyRequest myRequest, MyResponse myResponse, MyFilterChain myFilterChain) {
        myRequest.str = myRequest.str.replace("學歷大專", "學歷不高");
        myFilterChain.doFilter(myRequest, myResponse);
        myResponse.str += "【妹子挑剔,須要過濾學歷】";
    }
}

測試類

package com.wzj.chainOfResponsibility.example1.v3;

/**
 * @Author: wzj
 * @Date: 2019/9/7 20:42
 * @Desc: 過濾器徹底模式
 */
public class TestV3 {
    public static void main(String[] args) {
        MyRequest myRequest = new MyRequest();
        myRequest.str = "張三身高170,學歷大專,跪求妹子給個機會認識";
        System.out.println("request:" + myRequest.str);
        MyResponse myResponse = new MyResponse();
        myResponse.str = "";
        MyFilterChain chain = new MyFilterChain();
        chain.add(new HeightFliter()).add(new EducationalBackGroundFilter());
        chain.doFilter(myRequest, myResponse);
        System.out.println("response:" + myResponse.str);
    }

}

結果

request:張三身高170,學歷大專,跪求妹子給個機會認識
response:【妹子挑剔,須要過濾學歷】【妹子挑剔,須要過濾身高】

5. 過濾器源碼分析

在web項目中常常須要配置知足咱們須要的各類過濾器filter,來過濾知足咱們自定義信息,不煩看一下tomcat中的源碼中是如何作到的,下面以tomcat8.5.37的源碼來分析具體實現。
找到ApplicationFilterFactory類,其中的createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet)方法建立了責任鏈,

public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {

        // 如何沒有servlet執行,返回null
        if (servlet == null)
            return null;

        // 建立和初始化過濾器鏈對象
        ApplicationFilterChain filterChain = null;
        if (request instanceof Request) {
            Request req = (Request) request;
            if (Globals.IS_SECURITY_ENABLED) {
                filterChain = new ApplicationFilterChain();
            } else {
                filterChain = (ApplicationFilterChain) req.getFilterChain();
                if (filterChain == null) {
                    filterChain = new ApplicationFilterChain();
                    req.setFilterChain(filterChain);
                }
            }
        } else {
         
            filterChain = new ApplicationFilterChain();
        }

        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

        // 獲取上下中的過濾器的映射集合
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();

        // If there are no filter mappings, we are done
        if ((filterMaps == null) || (filterMaps.length == 0))
            return (filterChain);

        // 獲取匹配的過濾器映射信息
        DispatcherType dispatcher =
                (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

        String requestPath = null;
        Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
        if (attribute != null){
            requestPath = attribute.toString();
        }

        String servletName = wrapper.getName();

        // 對過濾器鏈添加對應的路徑匹配過濾器
        for (int i = 0; i < filterMaps.length; i++) {
            if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
                continue;
            }
            if (!matchFiltersURL(filterMaps[i], requestPath))
                continue;
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                context.findFilterConfig(filterMaps[i].getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // 添加匹配servlet name的過濾器
        for (int i = 0; i < filterMaps.length; i++) {
            if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
                continue;
            }
            if (!matchFiltersServlet(filterMaps[i], servletName))
                continue;
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                context.findFilterConfig(filterMaps[i].getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // 返回過濾器責任鏈
        return filterChain;
    }

執行過濾器中的doFilter方法,會調用一個 internalDoFilter() 方法

public final class ApplicationFilterChain implements FilterChain {


......


     /**
     * 維護當前過濾器鏈的位置信息
     */
    private int pos = 0;


    /**
     * 當前過濾器的長度
     */
    private int n = 0;


......

@Override
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedExceptionAction<Void>() {
                        @Override
                        public Void run()
                            throws ServletException, IOException {
                            internalDoFilter(req,res);
                            return null;
                        }
                    }
                );
            } catch( PrivilegedActionException pe) {
                Exception e = pe.getException();
                if (e instanceof ServletException)
                    throw (ServletException) e;
                else if (e instanceof IOException)
                    throw (IOException) e;
                else if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                else
                    throw new ServletException(e.getMessage(), e);
            }
        } else {
            internalDoFilter(request,response);
        }
    }

    private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // 若是有下一個,則調用下一個過濾器
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    /// 調用Filter的doFilter()方法 , doFilter 又會調用 internalDoFilter,一直遞歸下去, 直到調用完全部的過濾器
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.filter"), e);
            }
            return;
        }

        // 從最後一個過濾器開始調用
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }

            if (request.isAsyncSupported() && !servletSupportsAsync) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(null);
                lastServicedResponse.set(null);
            }
        }
    }

}

6. 責任鏈模式總結

優勢

  • 將請求發送者和接收處理者分開。請求者能夠不用知道是誰處理的,處理者能夠不用知道請求的全貌,二者解耦,提升系統的靈活性。

缺點

  • 性能問題,每一個請求都是從鏈頭遍歷到鏈尾,特別是在鏈比較長的時候,性能是一個很是大的問題;
  • 調試不很方便,特別是鏈條比較長,相似遞歸的方式,調試的時候邏輯可能比較複雜。

注意事項

  • 鏈中節點數量須要控制,避免出現超長鏈的狀況,通常的作法是在Handler中設置一個最大節點數量,在setNext方法中判斷是否已是超過其閾值,超過則不容許該鏈創建,避免無心識地破壞系統性能。
相關文章
相關標籤/搜索