一亿驾、前言
場地和場景的重要性
射擊??需要去靶場學(xué)習(xí)黄娘、滑雪??需要去雪場體驗糯而、開車??需要能上路實踐,而編程開發(fā)除了能完成產(chǎn)品的功能流程泊窘,還需要保證系統(tǒng)的可靠性能熄驼。就像你能聽到的一些系統(tǒng)監(jiān)控指標(biāo);QPS
烘豹、TPS
瓜贾、TP99
、TP999
携悯、可用率
祭芦、響應(yīng)時長
等等,而這些指標(biāo)的總和評估就是一個系統(tǒng)的健康度憔鬼。但如果你幾乎沒有聽到這樣的技術(shù)術(shù)語龟劲,也沒接觸過類似高并發(fā)場景,那么就很像駕駛證的科目1考了100分轴或,但不能上路昌跌。沒有這樣的技術(shù)場景給你訓(xùn)練,讓你不斷的體會系統(tǒng)的脾氣秉性照雁,即便你有再多的想法都沒法實現(xiàn)蚕愤。所以,如果真的想學(xué)習(xí)一定要去一個有實操的場景饺蚊,下水試試才能學(xué)會狗刨萍诱。
二、開發(fā)環(huán)境
- JDK 1.8
- Idea + Maven
三污呼、責(zé)任鏈模式介紹
擊鼓傳雷裕坊,看上圖你是否想起周星馳有一個電影,大家坐在海邊圍成一個圈曙求,拿著一個點燃的炸彈碍庵,互相傳遞。
責(zé)任鏈模式的核心是解決一組服務(wù)中的先后執(zhí)行處理關(guān)系?悟狱,就有點像你沒錢花了静浴,需要家庭財務(wù)支出審批,10塊錢以下找閨女審批挤渐,100塊錢先閨女審批在媳婦審批苹享。你可以理解想象成當(dāng)你要跳槽的時候被安排的明明白白的被各個領(lǐng)導(dǎo)簽字放行。
四、案例場景模擬
在本案例中我們模擬在618大促期間的業(yè)務(wù)系統(tǒng)上線審批流程場景
像是這些一線電商類的互聯(lián)網(wǎng)公司得问,阿里囤攀、京東、拼多多等宫纬,在618期間都會做一些運(yùn)營活動場景以及提供的擴(kuò)容備戰(zhàn)焚挠,就像過年期間百度的紅包一樣。但是所有開發(fā)的這些系統(tǒng)都需要陸續(xù)的上線漓骚,因為臨近618有時候也有一些緊急的調(diào)整的需要上線蝌衔,但為了保障線上系統(tǒng)的穩(wěn)定性是盡可能的減少上線的,也會相應(yīng)的增強(qiáng)審批力度蝌蹂。就像一級響應(yīng)噩斟、二級響應(yīng)一樣。
而這審批的過程在隨著特定時間點會增加不同級別的負(fù)責(zé)人加入孤个,每個人就像責(zé)任鏈模式中的每一個核心點剃允。對于研發(fā)小伙伴并不需要關(guān)心具體的審批流程處理細(xì)節(jié),只需要知道這個上線更嚴(yán)格齐鲤,級別也更高斥废,但對于研發(fā)人員來說同樣是點擊相同的提審按鈕,等待審核给郊。
接下來我們就模擬這樣一個業(yè)務(wù)訴求場景营袜,使用責(zé)任鏈的設(shè)計模式來實現(xiàn)此功能。
1. 場景模擬工程
└── src
└── main
└── java
└── org.itstack.demo.design
└── AuthService.java
- 這里的代碼結(jié)構(gòu)比較簡單丑罪,只有一個模擬審核和查詢審核結(jié)果的服務(wù)類荚板。相當(dāng)于你可以調(diào)用這個類去審核工程和獲取審核結(jié)構(gòu),這部分結(jié)果信息是模擬的寫到緩存實現(xiàn)吩屹。
2. 場景簡述
2.1 模擬審核服務(wù)
public class AuthService {
private static Map<String, Date> authMap = new ConcurrentHashMap<String, Date>();
public static Date queryAuthInfo(String uId, String orderId) {
return authMap.get(uId.concat(orderId));
}
public static void auth(String uId, String orderId) {
authMap.put(uId.concat(orderId), new Date());
}
}
- 這里面提供了兩個接口一個是查詢審核結(jié)果(
queryAuthInfo
)跪另、另外一個是處理審核(auth
)。 - 這部分是把由誰審核的和審核的單子ID作為唯一key值記錄到內(nèi)存Map結(jié)構(gòu)中煤搜。
五免绿、用一坨坨代碼實現(xiàn)
這里我們先使用最直接的方式來實現(xiàn)功能
按照我們的需求審批流程,平常系統(tǒng)上線只需要三級負(fù)責(zé)人審批就可以擦盾,但是到了618大促時間點嘲驾,就需要由二級負(fù)責(zé)以及一級負(fù)責(zé)人一起加入審批系統(tǒng)上線流程。在這里我們使用非常直接的if判斷方式來實現(xiàn)這樣的需求迹卢。
1. 工程結(jié)構(gòu)
└── src
└── main
└── java
└── org.itstack.demo.design
└── AuthController.java
- 這部分非常簡單的只包含了一個審核的控制類辽故,就像有些伙伴開始寫代碼一樣,一個類寫所有需求腐碱。
2. 代碼實現(xiàn)
public class AuthController {
private SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 時間格式化
public AuthInfo doAuth(String uId, String orderId, Date authDate) throws ParseException {
// 三級審批
Date date = AuthService.queryAuthInfo("1000013", orderId);
if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):待三級審批負(fù)責(zé)人 ", "王工");
// 二級審批
if (authDate.after(f.parse("2020-06-01 00:00:00")) && authDate.before(f.parse("2020-06-25 23:59:59"))) {
date = AuthService.queryAuthInfo("1000012", orderId);
if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):待二級審批負(fù)責(zé)人 ", "張經(jīng)理");
}
// 一級審批
if (authDate.after(f.parse("2020-06-11 00:00:00")) && authDate.before(f.parse("2020-06-20 23:59:59"))) {
date = AuthService.queryAuthInfo("1000011", orderId);
if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):待一級審批負(fù)責(zé)人 ", "段總");
}
return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):審批完成");
}
}
- 這里從上到下分別判斷了在指定時間范圍內(nèi)由不同的人員進(jìn)行審批誊垢,就像618上線的時候需要三個負(fù)責(zé)人都審批才能讓系統(tǒng)進(jìn)行上線。
- 像是這樣的功能看起來很簡單的,但是實際的業(yè)務(wù)中會有很多部門喂走,但如果這樣實現(xiàn)就很難進(jìn)行擴(kuò)展殃饿,并且在改動擴(kuò)展調(diào)整也非常麻煩。
3. 測試驗證
3.1 編寫測試類
@Test
public void test_AuthController() throws ParseException {
AuthController authController = new AuthController();
// 模擬三級負(fù)責(zé)人審批
logger.info("測試結(jié)果:{}", JSON.toJSONString(authController.doAuth("49848144", "1000998004813441", new Date())));
logger.info("測試結(jié)果:{}", "模擬三級負(fù)責(zé)人審批芋肠,王工");
AuthService.auth("1000013", "1000998004813441");
// 模擬二級負(fù)責(zé)人審批
logger.info("測試結(jié)果:{}", JSON.toJSONString(authController.doAuth("49848144", "1000998004813441", new Date())));
logger.info("測試結(jié)果:{}", "模擬二級負(fù)責(zé)人審批乎芳,張經(jīng)理");
AuthService.auth("1000012", "1000998004813441");
// 模擬一級負(fù)責(zé)人審批
logger.info("測試結(jié)果:{}", JSON.toJSONString(authController.doAuth("49848144", "1000998004813441", new Date())));
logger.info("測試結(jié)果:{}", "模擬一級負(fù)責(zé)人審批,段總");
AuthService.auth("1000011", "1000998004813441");
logger.info("測試結(jié)果:{}", "審批完成");
}
- 這里模擬每次查詢是否審批完成帖池,隨著審批的不同節(jié)點秒咐,之后繼續(xù)由不同的負(fù)責(zé)人進(jìn)行審批操作。
-
authController.doAuth
碘裕,是查看審批的流程節(jié)點、AuthService.auth
攒钳,是審批方法用于操作節(jié)點流程狀態(tài)帮孔。
3.2 測試結(jié)果
23:25:00.363 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0001","info":"單號:1000998004813441 狀態(tài):待三級審批負(fù)責(zé)人 王工"}
23:25:00.366 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:模擬三級負(fù)責(zé)人審批,王工
23:25:00.367 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0001","info":"單號:1000998004813441 狀態(tài):待二級審批負(fù)責(zé)人 張經(jīng)理"}
23:25:00.367 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:模擬二級負(fù)責(zé)人審批不撑,張經(jīng)理
23:25:00.368 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0001","info":"單號:1000998004813441 狀態(tài):待一級審批負(fù)責(zé)人 段總"}
23:25:00.368 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:模擬一級負(fù)責(zé)人審批文兢,段總
23:25:00.368 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:審批完成
Process finished with exit code 0
- 從測試結(jié)果上可以看到一層層的由不同的人員進(jìn)行審批,審批完成后到下一個人進(jìn)行處理焕檬。單看結(jié)果是滿足我們的訴求姆坚,只不過很難擴(kuò)展和調(diào)整流程,相當(dāng)于代碼寫的死死的实愚。
六兼呵、責(zé)任鏈模式重構(gòu)代碼
接下來使用裝飾器模式來進(jìn)行代碼優(yōu)化,也算是一次很小的重構(gòu)腊敲。
責(zé)任鏈模式可以讓各個服務(wù)模塊更加清晰击喂,而每一個模塊間可以通過next
的方式進(jìn)行獲取。而每一個next
是由繼承的統(tǒng)一抽象類實現(xiàn)的碰辅。最終所有類的職責(zé)可以動態(tài)的進(jìn)行編排使用懂昂,編排的過程可以做成可配置化。
1. 工程結(jié)構(gòu)
└── src
└── main
└── java
└── org.itstack.demo.design
├── impl
│ ├── Level1AuthLink.java
│ ├── Level2AuthLink.java
│ └── Level3AuthLink.java
├── AuthInfo.java
└── AuthLink.java
責(zé)任鏈類圖
責(zé)任鏈模式模型結(jié)構(gòu)
- 上圖是這個業(yè)務(wù)模型中責(zé)任鏈結(jié)構(gòu)的核心部分没宾,通過三個實現(xiàn)了統(tǒng)一抽象類
AuthLink
的不同規(guī)則凌彬,再進(jìn)行責(zé)任編排模擬出一條鏈路。這個鏈路就是業(yè)務(wù)中的責(zé)任鏈循衰。 - 一般在使用責(zé)任鏈時候如果是場景比較固定铲敛,可以通過寫死到代碼中進(jìn)行初始化。但如果業(yè)務(wù)場景經(jīng)常變化可以做成xml配置的方式進(jìn)行處理会钝,也可以落到庫里進(jìn)行初始化操作原探。
2. 代碼實現(xiàn)
2.1 責(zé)任鏈中返回對象定義
public class AuthInfo {
private String code;
private String info = "";
public AuthInfo(String code, String ...infos) {
this.code = code;
for (String str:infos){
this.info = this.info.concat(str);
}
}
// ...get/set
}
- 這個類的是包裝了責(zé)任鏈處理過程中返回結(jié)果的類,方面處理每個責(zé)任鏈的返回信息。
2.2 鏈路抽象類定義
public abstract class AuthLink {
protected Logger logger = LoggerFactory.getLogger(AuthLink.class);
protected SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 時間格式化
protected String levelUserId; // 級別人員ID
protected String levelUserName; // 級別人員姓名
private AuthLink next; // 責(zé)任鏈
public AuthLink(String levelUserId, String levelUserName) {
this.levelUserId = levelUserId;
this.levelUserName = levelUserName;
}
public AuthLink next() {
return next;
}
public AuthLink appendNext(AuthLink next) {
this.next = next;
return this;
}
public abstract AuthInfo doAuth(String uId, String orderId, Date authDate);
}
- 這部分是責(zé)任鏈咽弦,鏈接起來的核心部分徒蟆。
AuthLink next
,重點在于可以通過next
方式獲取下一個鏈路需要處理的節(jié)點型型。 -
levelUserId
段审、levelUserName
,是責(zé)任鏈中的公用信息闹蒜,標(biāo)記每一個審核節(jié)點的人員信息寺枉。 - 抽象類中定義了一個抽象方法,
abstract AuthInfo doAuth
绷落,這是每一個實現(xiàn)者必須實現(xiàn)的類姥闪,不同的審核級別處理不同的業(yè)務(wù)。
2.3 三個審核實現(xiàn)類
Level1AuthLink
public class Level1AuthLink extends AuthLink {
public Level1AuthLink(String levelUserId, String levelUserName) {
super(levelUserId, levelUserName);
}
public AuthInfo doAuth(String uId, String orderId, Date authDate) {
Date date = AuthService.queryAuthInfo(levelUserId, orderId);
if (null == date) {
return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):待一級審批負(fù)責(zé)人 ", levelUserName);
}
AuthLink next = super.next();
if (null == next) {
return new AuthInfo("0000", "單號:", orderId, " 狀態(tài):一級審批完成負(fù)責(zé)人", " 時間:", f.format(date), " 審批人:", levelUserName);
}
return next.doAuth(uId, orderId, authDate);
}
}
Level2AuthLink
public class Level2AuthLink extends AuthLink {
private Date beginDate = f.parse("2020-06-11 00:00:00");
private Date endDate = f.parse("2020-06-20 23:59:59");
public Level2AuthLink(String levelUserId, String levelUserName) throws ParseException {
super(levelUserId, levelUserName);
}
public AuthInfo doAuth(String uId, String orderId, Date authDate) {
Date date = AuthService.queryAuthInfo(levelUserId, orderId);
if (null == date) {
return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):待二級審批負(fù)責(zé)人 ", levelUserName);
}
AuthLink next = super.next();
if (null == next) {
return new AuthInfo("0000", "單號:", orderId, " 狀態(tài):二級審批完成負(fù)責(zé)人", " 時間:", f.format(date), " 審批人:", levelUserName);
}
if (authDate.before(beginDate) || authDate.after(endDate)) {
return new AuthInfo("0000", "單號:", orderId, " 狀態(tài):二級審批完成負(fù)責(zé)人", " 時間:", f.format(date), " 審批人:", levelUserName);
}
return next.doAuth(uId, orderId, authDate);
}
}
Level3AuthLink
public class Level3AuthLink extends AuthLink {
private Date beginDate = f.parse("2020-06-01 00:00:00");
private Date endDate = f.parse("2020-06-25 23:59:59");
public Level3AuthLink(String levelUserId, String levelUserName) throws ParseException {
super(levelUserId, levelUserName);
}
public AuthInfo doAuth(String uId, String orderId, Date authDate) {
Date date = AuthService.queryAuthInfo(levelUserId, orderId);
if (null == date) {
return new AuthInfo("0001", "單號:", orderId, " 狀態(tài):待三級審批負(fù)責(zé)人 ", levelUserName);
}
AuthLink next = super.next();
if (null == next) {
return new AuthInfo("0000", "單號:", orderId, " 狀態(tài):三級審批負(fù)責(zé)人完成", " 時間:", f.format(date), " 審批人:", levelUserName);
}
if (authDate.before(beginDate) || authDate.after(endDate)) {
return new AuthInfo("0000", "單號:", orderId, " 狀態(tài):三級審批負(fù)責(zé)人完成", " 時間:", f.format(date), " 審批人:", levelUserName);
}
return next.doAuth(uId, orderId, authDate);
}
}
- 如上三個類砌烁;
Level1AuthLink
筐喳、Level2AuthLink
、Level3AuthLink
函喉,實現(xiàn)了不同的審核級別處理的簡單邏輯避归。 - 例如第一個審核類中會先判斷是否審核通過,如果沒有審核通過則返回結(jié)果給調(diào)用方管呵,引導(dǎo)去審核梳毙。(這里簡單模擬審核后有時間信息不為空,作為判斷條件)
- 判斷完成后獲取下一個審核節(jié)點捐下;
super.next();
账锹,如果不存在下一個節(jié)點,則直接返回結(jié)果坷襟。 - 之后是根據(jù)不同的業(yè)務(wù)時間段進(jìn)行判斷是否需要牌废,二級和一級的審核。
- 最后返回下一個審核結(jié)果啤握;
next.doAuth(uId, orderId, authDate);
鸟缕,有點像遞歸調(diào)用。
3. 測試驗證
3.1 編寫測試類
@Test
public void test_AuthLink() throws ParseException {
AuthLink authLink = new Level3AuthLink("1000013", "王工")
.appendNext(new Level2AuthLink("1000012", "張經(jīng)理")
.appendNext(new Level1AuthLink("1000011", "段總")));
logger.info("測試結(jié)果:{}", JSON.toJSONString(authLink.doAuth("49848144", "1000998004813441", new Date())));
// 模擬三級負(fù)責(zé)人審批
AuthService.auth("1000013", "1000998004813441");
logger.info("測試結(jié)果:{}", "模擬三級負(fù)責(zé)人審批排抬,王工");
logger.info("測試結(jié)果:{}", JSON.toJSONString(authLink.doAuth("49848144", "1000998004813441", new Date())));
// 模擬二級負(fù)責(zé)人審批
AuthService.auth("1000012", "1000998004813441");
logger.info("測試結(jié)果:{}", "模擬二級負(fù)責(zé)人審批懂从,張經(jīng)理");
logger.info("測試結(jié)果:{}", JSON.toJSONString(authLink.doAuth("49848144", "1000998004813441", new Date())));
// 模擬一級負(fù)責(zé)人審批
AuthService.auth("1000011", "1000998004813441");
logger.info("測試結(jié)果:{}", "模擬一級負(fù)責(zé)人審批,段總");
logger.info("測試結(jié)果:{}", JSON.toJSONString(authLink.doAuth("49848144", "1000998004813441", new Date())));
}
- 這里包括最核心的責(zé)任鏈創(chuàng)建蹲蒲,實際的業(yè)務(wù)中會包裝到控制層番甩;
AuthLink authLink = new Level3AuthLink("1000013", "王工") .appendNext(new Level2AuthLink("1000012", "張經(jīng)理") .appendNext(new Level1AuthLink("1000011", "段總")));
通過把不同的責(zé)任節(jié)點進(jìn)行組裝,構(gòu)成一條完整業(yè)務(wù)的責(zé)任鏈届搁。 - 接下里不斷的執(zhí)行查看審核鏈路
authLink.doAuth(...)
缘薛,通過返回結(jié)果對數(shù)據(jù)進(jìn)行3窍育、2、1級負(fù)責(zé)人審核宴胧,直至最后審核全部完成漱抓。
3.2 測試結(jié)果
23:49:46.585 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0001","info":"單號:1000998004813441 狀態(tài):待三級審批負(fù)責(zé)人 王工"}
23:49:46.590 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:模擬三級負(fù)責(zé)人審批,王工
23:49:46.590 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0001","info":"單號:1000998004813441 狀態(tài):待二級審批負(fù)責(zé)人 張經(jīng)理"}
23:49:46.590 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:模擬二級負(fù)責(zé)人審批恕齐,張經(jīng)理
23:49:46.590 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0001","info":"單號:1000998004813441 狀態(tài):待一級審批負(fù)責(zé)人 段總"}
23:49:46.590 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:模擬一級負(fù)責(zé)人審批乞娄,段總
23:49:46.590 [main] INFO org.itstack.demo.design.test.ApiTest - 測試結(jié)果:{"code":"0000","info":"單號:1000998004813441 狀態(tài):一級審批完成負(fù)責(zé)人 時間:2020-06-18 23:49:46 審批人:段總"}
Process finished with exit code 0
- 從上述的結(jié)果可以看到我們的責(zé)任鏈已經(jīng)生效,按照責(zé)任鏈的結(jié)構(gòu)一層層審批显歧,直至最后輸出審批結(jié)束到一級完成的結(jié)果仪或。
- 這樣責(zé)任鏈的設(shè)計方式可以方便的進(jìn)行擴(kuò)展和維護(hù),也把if語句干掉了士骤。
七范删、總結(jié)
- 從上面代碼從if語句重構(gòu)到使用責(zé)任鏈模式開發(fā)可以看到,我們的代碼結(jié)構(gòu)變得清晰干凈了拷肌,也解決了大量if語句的使用到旦。并不是if語句不好,只不過if語句并不適合做系統(tǒng)流程設(shè)計廓块,但是在做判斷和行為邏輯處理中還是非常可以使用的契沫。
- 在我們前面學(xué)習(xí)結(jié)構(gòu)性模式中講到過組合模式带猴,它像是一顆組合樹一樣,我們搭建出一個流程決策樹懈万。其實這樣的模式也是可以和責(zé)任鏈模型進(jìn)行組合擴(kuò)展使用拴清,而這部分的重點在于如何關(guān)聯(lián)鏈路的關(guān)聯(lián),最終的執(zhí)行都是在執(zhí)行在中間的關(guān)系鏈会通。
- 責(zé)任鏈模式很好的處理單一職責(zé)和開閉原則口予,簡單了耦合也使對象關(guān)系更加清晰,而且外部的調(diào)用方并不需要關(guān)心責(zé)任鏈?zhǔn)侨绾芜M(jìn)行處理的(以上程序中可以把責(zé)任鏈的組合進(jìn)行包裝涕侈,在提供給外部使用)。但除了這些優(yōu)點外也需要是適當(dāng)?shù)膱鼍安胚M(jìn)行使用,避免造成性能以及編排混亂調(diào)試測試疏漏問題沦偎。