以項目驅動學習,以實踐檢驗真知
前言
設計模式是我們編程道路上繞不開的一環,用好了設計模式能夠讓代碼擁有良好的維護性、可讀性以及擴展性,它仿佛就是“優雅”的代名詞,各個框架和庫也都能見到它的身影。
正是因為它有種種好處,所以很多人在開發時總想將某個設計模式用到項目中來,然而往往會用得比較別扭。其中一部分原因是業務需求并不太符合所用的設計模式,還有一部分原因就是在Web項目中我們對象都是交由Spring框架的Ioc容器來管理,很多設計模式無法直接套用。那么在真正的項目開發中,我們就需要對設計模式做一個靈活的變通,讓其能夠和框架結合,在實際開發中發揮出真正的優勢。
當項目引入IoC容器后,我們一般是通過依賴注入來使用各個對象,將設計模式和框架結合的關鍵點就在于此!本文會講解如何通過依賴注入來完成我們的設計模式,由淺入深,讓你在了解幾個設計模式的同時掌握依賴注入的一些妙用。
本文所有代碼都放在Github上了,克隆下來即可運行查看效果。
實戰
單例模式
單例應該是很多人接觸的第一個設計模式,相比其他設計模式來說單例的概念非常簡單,即在一個進程中,某個類從始至終只有一個實例對象。不過概念就算再簡單,還是需要一點編碼才能實現,R 之前的文章 回字有四種寫法,那你知道單例有五種寫法嗎 就有詳細的講解,這里對該設計模式就不過多介紹了,咱們直接來看看在實際開發中如何運用該模式。
交由Spring IoC容器管理的對象稱之為Bean,每個Bean都有其作用域(scope),這個作用域可以理解為Spring控制Bean生命周期的方式。創建和銷毀是生命周期中必不可少的節點,單例模式的重點自然是對象的創建。而Spring創建對象的過程對于我們來說是無感知的,即我們只需配置好Bean,然后通過依賴注入就可以使用對象了:
@Service //和@Component功能一樣,將該類聲明為Bean交由容器管理
public class UserServiceImpl implements UserService{
}
@Controller
public class UserController {
@Autowired // 依賴注入
private UserService userService;
}
那這個對象的創建我們該如何控制呢?
其實,Bean默認的作用范圍就是單例的,我們無需手寫單例。要想驗證Bean是否為單例很簡單,我們在程序各個地方獲取Bean后打印其hashCode
就可以看是否為同一個對象了,比如兩個不同的類中都注入了UserService
:
@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());
}
}
打印結果會是兩個相同的hashCode
。
為什么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】開啟白名單,其他地方轉載請標明原地址、原作者!