【項目實踐】依賴注入用得好,設計模式輕鬆搞

以項目驅動學習,以實踐檢驗真知java

前言

設計模式是咱們編程道路上繞不開的一環,用好了設計模式可以讓代碼擁有良好的維護性、可讀性以及擴展性,它彷彿就是「優雅」的代名詞,各個框架和庫也都能見到它的身影。git

正是由於它有種種好處,因此不少人在開發時總想將某個設計模式用到項目中來,然而每每會用得比較彆扭。其中一部分緣由是業務需求並不太符合所用的設計模式,還有一部分緣由就是在Web項目中咱們對象都是交由Spring框架的Ioc容器來管理,不少設計模式沒法直接套用。那麼在真正的項目開發中,咱們就須要對設計模式作一個靈活的變通,讓其可以和框架結合,在實際開發中發揮出真正的優點。github

當項目引入IoC容器後,咱們通常是經過依賴注入來使用各個對象,將設計模式和框架結合的關鍵點就在於此!本文會講解如何經過依賴注入來完成咱們的設計模式,由淺入深,讓你在瞭解幾個設計模式的同時掌握依賴注入的一些妙用。web

本文全部代碼都放在Github上了,克隆下來便可運行查看效果。編程

實戰

單例模式

單例應該是不少人接觸的第一個設計模式,相比其餘設計模式來講單例的概念很是簡單,即在一個進程中,某個類從始至終只有一個實例對象。不過概念就算再簡單,仍是須要一點編碼才能實現,R 以前的文章 回字有四種寫法,那你知道單例有五種寫法嗎 就有詳細的講解,這裏對該設計模式就不過多介紹了,我們直接來看看在實際開發中如何運用該模式。設計模式

交由Spring IoC容器管理的對象稱之爲Bean,每一個Bean都有其做用域(scope),這個做用域能夠理解爲Spring控制Bean生命週期的方式。建立和銷燬是生命週期中必不可少的節點,單例模式的重點天然是對象的建立。而Spring建立對象的過程對於咱們來講是無感知的,即咱們只需配置好Bean,而後經過依賴注入就可使用對象了:bash

@Service //和@Component功能同樣,將該類聲明爲Bean交由容器管理
public class UserServiceImpl implements UserService{
}

@Controller
public class UserController {
    @Autowired // 依賴注入
    private UserService userService;
}
複製代碼

那這個對象的建立咱們該如何控制呢?微信

其實,Bean默認的做用範圍就是單例的,咱們無需手寫單例。要想驗證Bean是否爲單例很簡單,咱們在程序各個地方獲取Bean後打印其hashCode就能夠看是否爲同一個對象了,好比兩個不一樣的類中都注入了UserServicewebsocket

@Controller
public class UserController {
    @Autowired
    private UserService userService;
    
    public void test() {
        System.out.println(userService.hashCode());
    }
}

@Controller
public class OtherController {
    @Autowired
    private UserService userService;
    
    public void test() {
        System.out.println(userService.hashCode());
    }
}
複製代碼

打印結果會是兩個相同的hashCodemarkdown

爲何Spring默認會用單例的形式來實例化Bean呢?這天然是由於單例能夠節約資源,有不少類是不必實例化多個對象的。

若是咱們就是想每次獲取Bean時都建立一個對象呢?咱們能夠在聲明Bean的時候加上@Scope註解來配置其做用域:

@Service
@Scope("prototype")
public class UserServiceImpl implements UserService{
}
複製代碼

這樣當你每次獲取Bean時都會建立一個實例。

Bean的做用域有如下幾種,咱們能夠根據需求配置,大多數狀況下咱們用默認的單例就行了:

名稱 說明
singleton 默認做用範圍。每一個IoC容器只建立一個對象實例。
prototype 被定義爲多個對象實例。
request 限定在HTTP請求的生命週期內。每一個HTTP客戶端請求都有本身的對象實例。
session 限定在HttpSession的生命週期內。
application 限定在ServletContext的生命週期內。
websocket 限定在WebSocket的生命週期內。

這裏要額外注意一點,Bean的單例並不能徹底算傳統意義上的單例,由於其做用域只能保證在IoC容器內保證只有一個對象實例,可是不能保證一個進程內只有一個對象實例。也就是說,若是你不經過Spring提供的方式獲取Bean,而是本身建立了一個對象,此時程序就會有多個對象存在了:

public void test() {
    // 本身new了一個對象
    System.out.println(new UserServiceImpl().hashCode());
}
複製代碼

這就是須要變通的地方,Spring能夠說在咱們平常開發中覆蓋了每個角落,只要本身不故意繞開Spring,那麼保證IoC容器內的單例基本就等同於保證了整個程序內的單例。

責任鏈模式

概念比較簡單的單例講解完後,我們再來看看責任鏈模式。

模式講解

該模式並不複雜:一個請求能夠被多個對象處理,這些對象鏈接成一條鏈而且沿着這條鏈傳遞請求,直到有對象處理它爲止。該模式的好處是讓請求者和接受者解耦,能夠動態增刪處理邏輯,讓處理對象的職責擁有了很是高的靈活性。咱們開發中經常使用的過濾器Filter和攔截器Interceptor就是運用了責任鏈模式。

光看介紹只會讓人云裏霧裏,咱們直接來看下該模式如何運用。

就拿工做中的請假審批來講,當咱們發起一個請假申請的時候,通常會有多個審批者,每一個審批者都表明着一個責任節點,都有本身的審批邏輯。咱們假設有如下審批者:

組長Leader:只能審批不超過三天的請假;

經理Manger:只能審批不超過七天的請假;

老闆Boss:可以審批任意天數。

我們先定義一個請假審批的對象:

public class Request {
    /** * 請求人姓名 */
    private String name;
    /** * 請假天數。爲了演示就簡單按成天來算,不弄什麼小時了 */
    private Integer day;

    public Request(String name, Integer day) {
        this.name = name;
        this.day = day;
    }
    
    // 省略get、set方法
}
複製代碼

按照傳統的寫法是接受者收到這個對象後經過條件判斷來進行相應的處理:

public class Handler {
    public void process(Request request) {
        System.out.println("---");

        // Leader審批
        if (request.getDay() <= 3) {
            System.out.println(String.format("Leader已審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));
            return;
        }
        System.out.println(String.format("Leader沒法審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));

        // Manger審批
        if (request.getDay() <= 7) {
            System.out.println(String.format("Manger已審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));
            return;
        }
        System.out.println(String.format("Manger沒法審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));

        // Boss審批
        System.out.println(String.format("Boss已審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));

        System.out.println("---");
    }
}
複製代碼

在客戶端模擬審批流程:

public class App {
    public static void main( String[] args ) {
        Handler handler = new Handler();
        handler.process(new Request("張三", 2));
        handler.process(new Request("李四", 5));
        handler.process(new Request("王五", 14));
    }
}
複製代碼

打印結果以下:

---
Leader已審批【張三】的【2】天請假申請
---
Leader沒法審批【李四】的【5】天請假申請
Manger已審批【李四】的【5】天請假申請
---
Leader沒法審批【王五】的【14】天請假申請
Manger沒法審批【王五】的【14】天請假申請
Boss已審批【王五】的【14】天請假申請
---
複製代碼

不難看出Handler類中的代碼充滿了壞味道!每一個責任節點間的耦合度很是高,若是要增刪某個節點,就要改動這一大段代碼,很不靈活。並且這裏演示的審批邏輯還只是打印一句話而已,在真實業務中處理邏輯可比這複雜多了,若是要改動起來簡直就是災難。

這時候咱們的責任鏈模式就派上用場了!咱們將每一個責任節點封裝成獨立的對象,而後將這些對象組合起來變成一個鏈條,並經過統一入口挨個處理。

首先,咱們要抽象出責任節點的接口,全部節點都實現該接口:

public interface Handler {
    /** * 返回值爲true,則表明放行,交由下一個節點處理 * 返回值爲false,則表明不放行 */
    boolean process(Request request);
}
複製代碼

以Leader節點爲例,實現該接口:

public class LeaderHandler implements Handler{
    @Override
    public boolean process(Request request) {
        if (request.getDay() <= 3) {
            System.out.println(String.format("Leader已審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));
            // 處理完畢,不放行
            return false;
        }
        System.out.println(String.format("Leader沒法審批【%s】的【%d】天請假申請", request.getName(), request.getDay()));
        // 放行
        return true;
    }
}
複製代碼

而後定義一個專門用來處理這些Handler的鏈條類:

public class HandlerChain {
    // 存放全部Handler
    private List<Handler> handlers = new LinkedList<>();

    // 給外部提供一個增長Handler的入口
    public void addHandler(Handler handler) {
        this.handlers.add(handler);
    }

    public void process(Request request) {
        // 依次調用Handler
        for (Handler handler : handlers) {
            // 若是返回爲false,停止調用
            if (!handler.process(request)) {
                break;
            }
        }
    }
    
}
複製代碼

如今咱們來看下使用責任鏈是怎樣執行審批流程的:

public class App {
    public static void main( String[] args ) {
        // 構建責任鏈
        HandlerChain chain = new HandlerChain();
        chain.addHandler(new LeaderHandler());
        chain.addHandler(new ManagerHandler());
        chain.addHandler(new BossHandler());
        // 執行多個流程
        chain.process(new Request("張三", 2));
        chain.process(new Request("李四", 5));
        chain.process(new Request("王五", 14));
    }
}
複製代碼

打印結果和前面一致。

這樣帶來的好處是顯而易見的,咱們能夠很是方便地增刪責任節點,修改某個責任節點的邏輯也不會影響到其餘的節點,每一個節點只需關注本身的邏輯。而且責任鏈是按照固定順序執行節點,按照本身想要的順序添加各個對象便可方便地排列順序。

此外責任鏈有不少變體,好比像Servlet的Filter執行下一個節點時還須要持有鏈條的引用:

public class MyFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        if (...) {
            // 經過鏈條引用來放行
            chain.doFilter(req, resp);
        } else {
            // 若是沒有調用chain的方法則表明停止往下傳遞
            ...
        }
    }
}
複製代碼

各責任鏈除了傳遞的方式不一樣,總體的鏈路邏輯也能夠有所不一樣。

咱們剛纔演示的是將請求交由某一個節點進行處理,只要有一個處理了,後續就不用處理了。有些責任鏈目的不是找到某一個節點來處理,而是每一個節點都作一些事,至關於一個流水線。

好比像剛纔的審批流程,咱們能夠將邏輯改成一個請假申請須要每個審批人都贊成纔算申請經過,Leader贊成了後轉給Manger審批,Manger贊成了後轉給Boss審批,只有Boss最終贊成了才生效。

形式有多種,其核心概念是將請求對象鏈式傳遞,不脫離這一點就均可以算做責任鏈模式,無需太死守定義。

配合框架

責任鏈模式中,咱們都是本身建立責任節點對象,而後將其添加到責任鏈條中。在實際開發中這樣就會有一個問題,若是咱們的責任節點裏依賴注入了其它的Bean,那麼手動建立對象的話則表明該對象就沒有交由Spring管理,那些屬性也就不會被依賴注入:

public class LeaderHandler implements Handler{
    @Autowired // 手動建立LeaderHandler則該屬性不會被注入
    private UserService userService;
}
複製代碼

此時咱們就必須將各個節點對象也交由Spring來管理,而後經過Spring來獲取這些對象實例,再將這些對象實例放置到責任鏈中。其實這種方式大部分人都接觸過,Spring MVC的攔截器Interceptor就是這樣使用的:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 獲取Bean,添加到責任鏈中(注意哦,這裏是調用的方法來獲取對象,而不是new出對象)
        registry.addInterceptor(loginInterceptor());
        registry.addInterceptor(authInterceptor());
    }
    
    // 經過@Bean註解將自定義攔截器交由Spring管理
    @Bean
    public LoginInterceptor loginInterceptor() {return new LoginInterceptor();}
    @Bean
    public AuthInterceptor authInterceptor() {return new AuthInterceptor();}
}
複製代碼

InterceptorRegistry就至關於鏈條類了,該對象由Spring MVC傳遞給咱們,好讓咱們添加攔截器,後續Spring MVC會自行調用責任鏈,咱們無需操心。

別人框架定義的責任鏈會由框架調用,那咱們自定義的責任鏈該如何調用呢?這裏有一個更爲簡便的方式,那就是將Bean依賴注入到集合中

咱們平常開發時都是使用依賴注入獲取單個Bean,這是由於咱們聲明的接口或者父類一般只需一個實現類就能夠搞定業務需求了。而剛纔咱們自定義的Handler接口下會有多個實現類,此時咱們就能夠一次性注入多個Bean!我們如今就來改造一下以前的代碼。

首先,將每一個Handler實現類加上@Service註解,將其聲明爲Bean:

@Service
public class LeaderHandler implements Handler{
    ...
}

@Service
public class ManagerHandler implements Handler{
    ...
}

@Service
public class BossHandler implements Handler{
    ...
}
複製代碼

而後咱們來改造一下咱們的鏈條類,將其也聲明爲一個Bean,而後直接在成員變量上加上@Autowired註解。既然都經過依賴注入來實現了,那麼就無需手動再新增責任節點,因此咱們將以前的添加節點的方法給去除:

@Service
public class HandlerChain {
    @Autowired
    private List<Handler> handlers;

    public void process(Request request) {
        // 依次調用Handler
        for (Handler handler : handlers) {
            // 若是返回爲false,停止調用
            if (!handler.process(request)) {
                break;
            }
        }
    }

}
複製代碼

沒錯,依賴注入很是強大,不止可以注入單個對象,還能夠注入多個!這樣一來就很是方便了,咱們只需實現Handler接口,將實現類聲明爲Bean,就會自動被注入到責任鏈中,咱們甚至都不用手動添加。要想執行責任鏈也特別簡單,只需獲取HandlerChain而後調用便可:

@Controller
public class UserController {
    @Autowired
    private HandlerChain chain;

    public void process() {
        chain.process(new Request("張三", 2));
        chain.process(new Request("李四", 5));
        chain.process(new Request("王五", 14));
    }
    
}
複製代碼

執行效果以下:

---
Boss已審批【張三】的【2】天請假申請
---
Boss已審批【李四】的【5】天請假申請
---
Boss已審批【王五】的【14】天請假申請
複製代碼

咦,所有都是Boss審批了,爲啥前面兩個節點沒有生效呢?由於咱們尚未配置Bean注入到集合中的順序,咱們須要加上@Order註解來控制Bean的裝配順序,數字越小越靠前:

@Order(1)
@Service
public class LeaderHandler implements Handler{
    ...
}

@Order(2)
@Service
public class ManagerHandler implements Handler{
    ...
}

@Order(3)
@Service
public class BossHandler implements Handler{
    ...
}
複製代碼

這樣咱們自定義的責任鏈模式就完美融入到Spring中了!

策略模式

乘熱打鐵,咱們如今再來說解一個新的模式!

模式講解

咱們開發中常常碰到這樣的需求:須要根據不一樣的狀況執行不一樣的操做。好比咱們購物最多見的郵費,不一樣的地區、不一樣的商品郵費都會不一樣。假設如今需求是這樣的:

包郵地區:沒有超過10KG的貨物免郵,10KG以上8元;

鄰近地區:沒有超過10KG的貨物8元,10KG以上16元;

偏遠地區:沒有超過10KG的貨物16元,10KG以上15KG如下24元, 15KG以上32元。

那咱們計算郵費的方法大概是這樣的:

// 爲了方便演示,重量和金額就簡單設置爲整型
public long calPostage(String zone, int weight) {
    // 包郵地區
    if ("freeZone".equals(zone)) {
        if (weight <= 10) {
            return 0;
        } else {
            return 8;
        }
    }

    // 近距離地區
    if ("nearZone".equals(zone)) {
        if (weight <= 10) {
            return 8;
        } else {
            return 16;
        }
    }

    // 偏遠地區
    if ("farZone".equals(zone)) {
        if (weight <= 10) {
            return 16;
        } else if (weight <= 15) {
            return 24;
        } else {
            return 32;
        }
    }
	
    return 0;
}
複製代碼

這麼點郵費規則就寫了如此長的代碼,要是規則稍微再複雜點簡直就更長了。並且若是規則有變,就要對這一大塊代碼縫縫補補,長此以往代碼就會變得很是難以維護。

咱們首先想到的優化方式是將每一塊的計算封裝成方法:

public long calPostage(String zone, int weight) {
    // 包郵地區
    if ("freeZone".equals(zone)) {
        return calFreeZonePostage(weight);
    }

    // 近距離地區
    if ("nearZone".equals(zone)) {
        return calNearZonePostage(weight);
    }

    // 偏遠地區
    if ("farZone".equals(zone)) {
        return calFarZonePostage(weight);
    }
	
    return 0;
}
複製代碼

這樣確實不錯,大部分狀況下也能知足需求,可依然不夠靈活。

由於這些規則都是寫死在咱們方法內的,若是調用者想使用本身的規則,或者常常修改規則呢?總不能動不動就修改咱們寫好的代碼吧。要知道郵費計算只是訂單價格計算的一個小環節,咱們當然能夠寫好幾種規則定式來提供服務,但也得容許別人自定義規則。此時,咱們更應該將郵費計算操做高度抽象成一個接口,有不一樣的計算規則就實現不一樣的類。不一樣規則表明不一樣策略,這種方式就是咱們的策略模式!咱們來看下具體寫法:

首先,封裝一個郵費計算接口:

public interface PostageStrategy {
    long calPostage(int weight);
}
複製代碼

而後,咱們將那幾個地區規則封裝成不一樣的實現類,拿包郵地區示例:

public class FreeZonePostageStrategy implements PostageStrategy{
    @Override
    public long calPostage(int weight) {
        if (weight <= 10) {
            return 0;
        } else {
            return 8;
        }
    }
}
複製代碼

最後,要應用策略的話咱們還須要一個專門類:

public class PostageContext {
    // 持有某個策略
    private PostageStrategy postageStrategy = new FreeZonePostageStrategy();
    // 容許調用方設置新的策略
    public void setPostageStrategy(PostageStrategy postageStrategy) {
        this.postageStrategy = postageStrategy;
    }
    // 供調用方執行策略
    public long calPostage(int weight) {
        return postageStrategy.calPostage(weight);
    }
}
複製代碼

這樣,調用方既可使用咱們已有的策略,也能夠很是方便地修改或自定義策略:

public long calPrice(User user, int weight) {
    PostageContext postageContext = new PostageContext();
    // 自定義策略
    if ("RudeCrab".equals(user.getName())) {
        // VIP客戶,20KG如下一概包郵,20KG以上只收5元
        postageContext.setPostageStrategy(w -> w <= 20 ? 0 : 5);
        return postageContext.calPostage(weight);
    }
    // 包郵地區策略
    if ("freeZone".equals(user.getZone())) {
        postageContext.setPostageStrategy(new FreeZonePostageStrategy());
        return postageContext.calPostage(weight);
    }
    // 鄰近地區策略
    if ("nearZone".equals(user.getZone())) {
        postageContext.setPostageStrategy(new NearZonePostageStrategy());
        return postageContext.calPostage(weight);
    }
    
    ...
    
    return 0;
}
複製代碼

能夠看到,簡單的邏輯直接使用Lambda表達式就完成了自定義策略,若邏輯複雜的話也能夠直接新建一個實現類來完成。

這就是策略模式的魅力所在,容許調用方使用不一樣的策略來獲得不一樣的結果,以達到最大的靈活性!

儘管好處不少,但策略模式缺點也很明顯:

  • 可能會形成策略類過多的狀況,有多少規則就有多少類
  • 策略模式只是將邏輯分發到不一樣實現類中,調用方的if、else一個都沒減小。
  • 調用方須要知道全部策略類才能使用現有的邏輯。

大部分缺點能夠配合工廠模式或者反射來解決,但這樣又增長了系統的複雜性。那有沒有既能彌補缺點又不復雜的方案呢,固然是有的,這就是我接下來要講解的內容。在策略模式配合Spring框架的同時,也能彌補模式自己的缺點!

配合框架

通過責任鏈模式我們就能夠發現,其實所謂的配合框架就是將咱們的對象交給Spring來管理,而後經過Spring調用Bean便可。策略模式中,我們每一個策略類都是手動實例化的,那我們要作的第一步毫無疑問就是將這些策略類聲明爲Bean:

@Service("freeZone") // 註解中的值表明Bean的名稱,這裏爲何要這樣作,等下我會講解
public class FreeZonePostageStrategy implements PostageStrategy{
	...
}

@Service("nearZone")
public class NearZonePostageStrategy implements PostageStrategy{
	...
}

@Service("farZone")
public class FarZonePostageStrategy implements PostageStrategy{
	...
}
複製代碼

而後咱們就要經過Spring獲取這些Bean。有人可能會天然聯想到,咱們仍是將這些實現類都注入到一個集合中,而後遍歷使用。這確實能夠,不過太麻煩了。依賴注入但是很是強大的,不只能將Bean注入到集合中,還能將其注入到Map中

來看具體用法:

@Controller
public class OrderController {
    @Autowired
    private Map<String, PostageStrategy> map;

    public void calPrice(User user, int weight) {
        map.get(user.getZone()).calPostage(weight);
    }
}
複製代碼

大聲告訴我,清不清爽!簡不簡潔!優不優雅!

依賴注入可以將Bean注入到Map中,其中Key爲Bean的名稱,Value爲Bean對象,這也就是我前面要在@Service註解上設置值的緣由,只有這樣才能將讓調用方直接經過Map的get方法獲取到Bean,繼而使用該Bean對象。

咱們以前的PostageContext類能夠不要了,何時想調用某策略,直接在調用處注入Map便可。

經過這種方式,咱們不只讓策略模式徹底融入到Spring框架中,還完美解決了if、else過多等問題!咱們要想新增策略,只需新建一個實現類並將其聲明成Bean就好了,原有調用方無需改動一行代碼便可生效。

小貼士:若是一個接口或者父類有多個實現類,但我又只想依賴注入單個對象,可使用@Qualifier("Bean的名稱")註解來獲取指定的Bean。

總結

本文介紹了三種設計模式,以及各設計模式在Spring框架下是如何運用的!這三種設計模式對應的依賴注入方式以下:

  • 單例模式:依賴注入單個對象
  • 責任鏈模式:依賴注入集合
  • 策略模式:依賴注入Map

將設計模式和Spring框架配合的關鍵點就在於,如何將模式中的對象交由Spring管理。這是本文的核心,這一點思考清楚了,各個設計模式才能靈活使用。

講解到這裏就結束了,本文全部代碼都放在Github,克隆下來便可運行。若是對你有幫助,能夠點贊關注,我會持續更新更多原創【項目實踐】的!

微信上轉載請聯繫公衆號【RudeCrab】開啓白名單,其餘地方轉載請標明原地址、原做者!

相關文章
相關標籤/搜索