JAVA設(shè)計(jì)模式第十二講:大廠實(shí)踐 - 美團(tuán): 設(shè)計(jì)模式二三事
設(shè)計(jì)模式是眾多軟件開發(fā)人員經(jīng)過長(zhǎng)時(shí)間的試錯(cuò)和應(yīng)用總結(jié)出來的,解決特定問題的一系列方案?,F(xiàn)行的部分教材在介紹設(shè)計(jì)模式時(shí),有些會(huì)因?yàn)榘咐撾x實(shí)際應(yīng)用場(chǎng)景而令人費(fèi)解,有些又會(huì)因?yàn)閳?chǎng)景簡(jiǎn)單而顯得有些小題大做。本文是設(shè)計(jì)模式第十二講,會(huì)結(jié)合在美團(tuán)金融服務(wù)平臺(tái)設(shè)計(jì)開發(fā)時(shí)的經(jīng)驗(yàn),結(jié)合實(shí)際的案例,并采用“師生對(duì)話”這種相對(duì)詼諧的形式 去講解三類常用設(shè)計(jì)模式的應(yīng)用。希望能對(duì)想提升系統(tǒng)設(shè)計(jì)能力的同學(xué)有所幫助或啟發(fā)。
1、引言
話說這是在程序員世界里一對(duì)師徒的對(duì)話:
“老師,我最近在寫代碼時(shí)總感覺自己的代碼很不優(yōu)雅,有什么辦法能優(yōu)化嗎?”
“嗯,可以考慮通過教材系統(tǒng)學(xué)習(xí),從 注釋、命名、方法和異常等多方面實(shí)現(xiàn)整潔代碼?!?/p>
“然而,我想說的是,我的代碼是符合各種編碼規(guī)范的,但是從實(shí)現(xiàn)上卻總是感覺不夠簡(jiǎn)潔,而且總是需要反復(fù)修改!”學(xué)生小明嘆氣道。
老師看了看小明的代碼說:“我明白了,這是系統(tǒng)設(shè)計(jì)上的缺陷。總結(jié)就是抽象不夠、可讀性低、不夠健壯?!?/p>
“對(duì)對(duì)對(duì),那怎么能迅速提高代碼的可讀性、健壯性、擴(kuò)展性呢?”小明急不可耐地問道。
老師敲了敲小明的頭:“不要太浮躁,沒有什么方法能讓你立刻成為系統(tǒng)設(shè)計(jì)專家。但是對(duì)于你的問題,我想設(shè)計(jì)模式可以幫到你。”
“設(shè)計(jì)模式?”小明不解。
“是的?!崩蠋燑c(diǎn)了點(diǎn)頭,“世上本沒有路,走的人多了,便變成了路。在程序員的世界中,本沒有設(shè)計(jì)模式,寫代碼是人多了,他們便總結(jié)出了一套能提高開發(fā)和維護(hù)效率的套路,這就是設(shè)計(jì)模式。設(shè)計(jì)模式不是什么教條或者范式,它可以說是一種在特定場(chǎng)景下普適且可復(fù)用的解決方案,是一種可以用于提高代碼可讀性、可擴(kuò)展性、可維護(hù)性和可測(cè)性的最佳實(shí)踐?!?/p>
2、獎(jiǎng)勵(lì)的發(fā)放策略
第一天,老師問小明:“你知道活動(dòng)營(yíng)銷嗎?”
“這我知道,活動(dòng)營(yíng)銷是指企業(yè)通過參與社會(huì)關(guān)注度高的已有活動(dòng),或整合有效的資源自主策劃大型活動(dòng),從而迅速提高企業(yè)及其品牌的知名度、美譽(yù)度和影響力,常見的比如有抽獎(jiǎng)、紅包等?!?/p>
老師點(diǎn)點(diǎn)頭:“是的。我們假設(shè)現(xiàn)在就要做一個(gè)營(yíng)銷,需要用戶參與一個(gè)活動(dòng),然后完成一系列的任務(wù),最后可以得到一些獎(jiǎng)勵(lì)作為回報(bào)?;顒?dòng)的獎(jiǎng)勵(lì)包含美團(tuán)外賣、酒旅和美食等多種品類券,現(xiàn)在需要你幫忙設(shè)計(jì)一套獎(jiǎng)勵(lì)發(fā)放方案?!?/p>
因?yàn)橹坝羞^類似的開發(fā)經(jīng)驗(yàn),拿到需求的小明二話不說開始了編寫起了代碼:
// 獎(jiǎng)勵(lì)服務(wù)
class RewardService {
// 外部服務(wù)
private WaimaiService waimaiService;
private HotelService hotelService;
private FoodService foodService;
// 使用對(duì)入?yún)⒌臈l件判斷進(jìn)行發(fā)獎(jiǎng)
public void issueReward(String rewardType, Object ... params) {
if ("Waimai".equals(rewardType)) {
WaimaiRequest request = new WaimaiRequest();
// 構(gòu)建入?yún)?/span>
request.setWaimaiReq(params);
waimaiService.issueWaimai(request);
} else if ("Hotel".equals(rewardType)) {
HotelRequest request = new HotelRequest();
request.addHotelReq(params);
hotelService.sendPrize(request);
} else if ("Food".equals(rewardType)) {
FoodRequest request = new FoodRequest(params);
foodService.getCoupon(request);
} else {
throw new IllegalArgumentException("rewardType error!");
}
}
}
小明很快寫好了Demo,然后發(fā)給老師看。
“假如我們即將接入新的打車券,這是否意味著你必須要修改這部分代碼?”老師問道。
- 沒有滿足開閉原則
小明愣了一愣,沒等反應(yīng)過來老師又問:”假如后面美團(tuán)外賣的發(fā)券接口發(fā)生了改變或者替換,這段邏輯是否必須要同步進(jìn)行修改?”
- 沒有滿足可拓展性
小明陷入了思考之中,一時(shí)間沒法回答。
經(jīng)驗(yàn)豐富的老師一針見血地指出了這段設(shè)計(jì)的問題:“你這段代碼有兩個(gè)主要問題,一是不符合開閉原則,可以預(yù)見,如果后續(xù)新增品類券的話,需要直接修改主干代碼,而我們提倡代碼應(yīng)該是對(duì)修改封閉的;二是不符合迪米特法則,發(fā)獎(jiǎng)邏輯和各個(gè)下游接口高度耦合,這導(dǎo)致接口的改變將直接影響到代碼的組織,使得代碼的可維護(hù)性降低?!?/p>
小明恍然大悟:“那我將各個(gè)同下游接口交互的功能抽象成單獨(dú)的服務(wù),封裝其參數(shù)組裝及異常處理,使得發(fā)獎(jiǎng)主邏輯與其解耦,是否就能更具備擴(kuò)展性和可維護(hù)性?”
“這是個(gè)不錯(cuò)的思路。之前跟你介紹過設(shè)計(jì)模式,這個(gè)案例就可以使用策略模式和適配器模式來優(yōu)化?!?/p>
小明借此機(jī)會(huì)學(xué)習(xí)了這兩個(gè)設(shè)計(jì)模式。首先是策略模式:
策略模式定義了一系列的算法,并將每一個(gè)算法封裝起來,使它們可以相互替換。策略模式通常包含以下角色:
- 抽象策略(Strategy)類:定義了一個(gè)公共接口,各種不同的算法以不同的方式實(shí)現(xiàn)這個(gè)接口,環(huán)境角色使用這個(gè)接口調(diào)用不同的算法,一般使用接口或抽象類實(shí)現(xiàn)。
- 具體策略(Concrete Strategy)類:實(shí)現(xiàn)了抽象策略定義的接口,提供具體的算法實(shí)現(xiàn)。
- 環(huán)境(Context)類:持有一個(gè)策略類的引用,最終給客戶端調(diào)用。
- 策略模式可以參考我的這篇文章 JAVA設(shè)計(jì)模式第四講:行為型設(shè)計(jì)模式
- 10.3節(jié)
然后是適配器模式:
適配器模式:將一個(gè)類的接口轉(zhuǎn)換成客戶希望的另外一個(gè)接口,使得原本由于接口不兼容而不能一起工作的那些類能一起工作。適配器模式包含以下主要角色:
- 目標(biāo)(Target)接口:當(dāng)前系統(tǒng)業(yè)務(wù)所期待的接口,它可以是抽象類或接口。
- 適配者(Adaptee)類:它是被訪問和適配的現(xiàn)存組件庫(kù)中的組件接口。
- 適配器(Adapter)類:它是一個(gè)轉(zhuǎn)換器,通過繼承或引用適配者的對(duì)象,把適配者接口轉(zhuǎn)換成目標(biāo)接口,讓客戶按目標(biāo)接口的格式訪問適配者。
- 適配器模式可以參考我的這篇文章:JAVA設(shè)計(jì)模式第三講:結(jié)構(gòu)型設(shè)計(jì)模式
- 9.4節(jié)
結(jié)合優(yōu)化思路,小明首先設(shè)計(jì)出了策略接口,并通過適配器的思想將各個(gè)下游接口類適配成策略類:
// 策略接口
interface Strategy {
void issue(Object ... params);
}
// 外賣策略
class Waimai implements Strategy {
private WaimaiService waimaiService;
@Override
public void issue(Object... params) {
WaimaiRequest request = new WaimaiRequest();
// 構(gòu)建入?yún)?/span>
request.setWaimaiReq(params);
waimaiService.issueWaimai(request);
}
}
// 酒旅策略
class Hotel implements Strategy {
private HotelService hotelService;
@Override
public void issue(Object... params) {
HotelRequest request = new HotelRequest();
request.addHotelReq(params);
hotelService.sendPrize(request);
}
}
// 美食策略
class Food implements Strategy {
private FoodService foodService;
@Override
public void issue(Object... params) {
FoodRequest request = new FoodRequest(params);
foodService.payCoupon(request);
}
}
然后,小明創(chuàng)建策略模式的環(huán)境類,并供獎(jiǎng)勵(lì)服務(wù)調(diào)用:
// 使用分支判斷獲取的策略上下文
class StrategyContext {
public static Strategy getStrategy(String rewardType) {
switch (rewardType) {
case "Waimai":
return new Waimai();
case "Hotel":
return new Hotel();
case "Food":
return new Food();
default:
throw new IllegalArgumentException("rewardType error!");
}
}
}
// 優(yōu)化后的策略服務(wù)
class RewardService {
public void issueReward(String rewardType, Object ... params) {
Strategy strategy = StrategyContext.getStrategy(rewardType);
strategy.issue(params);
}
}
小明的代碼經(jīng)過優(yōu)化后,雖然結(jié)構(gòu)和設(shè)計(jì)上比之前要復(fù)雜不少,但考慮到健壯性和拓展性,還是非常值得的。
“看,我這次優(yōu)化后的版本是不是很完美?”小明洋洋得意地說。
“耦合度確實(shí)降低了,但還能做的更好?!?/p>
“怎么做?”小明有點(diǎn)疑惑。
“我問你,策略類是有狀態(tài)的模型嗎?如果不是是否可以考慮做成單例的?”
“的確如此?!毙∶魉坪趺靼琢恕?/p>
“還有一點(diǎn),環(huán)境類的獲取策略方法職責(zé)很明確,但是你 依然沒有做到完全對(duì)修改封閉?!?/p>
經(jīng)過老師的點(diǎn)撥,小明很快也領(lǐng)悟到了要點(diǎn):“那我可以將策略類單例化以減少開銷,并實(shí)現(xiàn)自注冊(cè)的功能徹底解決分支判斷?!?/p>
小明列出單例模式的要點(diǎn):
單例模式設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問其唯一的對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。
最終,小明在策略環(huán)境類中使用一個(gè)注冊(cè)表來記錄各個(gè)策略類的注冊(cè)信息,并提供接口供策略類調(diào)用進(jìn)行注冊(cè)。同時(shí)使用餓漢式單例模式去優(yōu)化策略類的設(shè)計(jì)
- 單例模式可以參考我的這篇文章:JAVA設(shè)計(jì)模式第二講:創(chuàng)建型設(shè)計(jì)模式
- 8.1節(jié)
// 策略上下文,用于管理策略的注冊(cè)和獲取
class StrategyContext {
private static final Map<String, Strategy> registerMap = new HashMap<>();
// 注冊(cè)策略
public static void registerStrategy(String rewardType, Strategy strategy) {
registerMap.putIfAbsent(rewardType, strategy);
}
// 獲取策略
public static Strategy getStrategy(String rewardType) {
return registerMap.get(rewardType);
}
}
// 抽象策略類
abstract class AbstractStrategy implements Strategy {
// 類注冊(cè)方法
public void register() {
StrategyContext.registerStrategy(getClass().getSimpleName(), this);
}
}
// 單例外賣策略
class Waimai extends AbstractStrategy implements Strategy {
private static final Waimai instance = new Waimai();
private WaimaiService waimaiService;
private Waimai() {
register();
}
public static Waimai getInstance() {
return instance;
}
@Override
public void issue(Object... params) {
WaimaiRequest request = new WaimaiRequest();
// 構(gòu)建入?yún)?/span>
request.setWaimaiReq(params);
waimaiService.issueWaimai(request);
}
}
// 單例酒旅策略
class Hotel extends AbstractStrategy implements Strategy {
private static final Hotel instance = new Hotel();
private HotelService hotelService;
private Hotel() {
register();
}
public static Hotel getInstance() {
return instance;
}
@Override
public void issue(Object... params) {
HotelRequest request = new HotelRequest();
request.addHotelReq(params);
hotelService.sendPrize(request);
}
}
// 單例美食策略
class Food extends AbstractStrategy implements Strategy {
private static final Food instance = new Food();
private FoodService foodService;
private Food() {
register();
}
public static Food getInstance() {
return instance;
}
@Override
public void issue(Object... params) {
FoodRequest request = new FoodRequest(params);
foodService.payCoupon(request);
}
}
最終,小明設(shè)計(jì)完成的結(jié)構(gòu)類圖如下:
如果使用了Spring框架,還可以利用Spring的Bean機(jī)制來代替上述的部分設(shè)計(jì),直接使用@Component
和@PostConstruct
注解即可完成單例的創(chuàng)建和注冊(cè),代碼會(huì)更加簡(jiǎn)潔。
借助Spring 強(qiáng)大的依賴注入
// 消除if/else的關(guān)鍵代碼,定義了一個(gè)StrategyHolder 來當(dāng)做工廠類
@Component
public class StrategyHolder {
// 關(guān)鍵功能 Spring 會(huì)自動(dòng)將 EntStrategy 接口的類注入到這個(gè)Map中
@Autowired
private Map<String, ProtocolChangeStrategy> strategyMap;
public ProtocolChangeStrategy getBy(String entNum) {
return strategyMap.get(entNum);
}
}
- 這個(gè)Map的key值就是你的 bean id,你可以用@Component(“value”)的方式設(shè)置,像我上面直接用默認(rèn)的方式的話,就是首字母小寫。value值則為對(duì)應(yīng)的策略實(shí)現(xiàn)類。
至此,經(jīng)過了多次討論、反思和優(yōu)化,小明終于得到了一套低耦合高內(nèi)聚,同時(shí)符合開閉原則的設(shè)計(jì)。
“老師,我開始學(xué)會(huì)利用設(shè)計(jì)模式去解決已發(fā)現(xiàn)的問題。這次我做得怎么樣?”
“合格。但是,依然要戒驕戒躁?!?/p>
3、任務(wù)模型的設(shè)計(jì)
“之前讓你設(shè)計(jì)獎(jiǎng)勵(lì)發(fā)放策略你還記得嗎?”老師忽然問道。
“當(dāng)然記得。一個(gè)好的設(shè)計(jì)模式,能讓工作事半功倍?!毙∶鞔鸬?。
“嗯,那會(huì)提到了活動(dòng)營(yíng)銷的組成部分,除了獎(jiǎng)勵(lì)之外,貌似還有任務(wù)吧?!?/p>
小明點(diǎn)了點(diǎn)頭,老師接著說:“現(xiàn)在,我想讓你去完成任務(wù)模型的設(shè)計(jì)。你需要重點(diǎn)關(guān)注狀態(tài)的流轉(zhuǎn)變更,以及狀態(tài)變更后的消息通知?!?/p>
小明欣然接下了老師給的難題。他首先定義了一套任務(wù)狀態(tài)的枚舉和行為的枚舉:
// 任務(wù)狀態(tài)枚舉
@AllArgsConstructor
@Getter
enum TaskState {
INIT("初始化"),
ONGOING( "進(jìn)行中"),
PAUSED("暫停中"),
FINISHED("已完成"),
EXPIRED("已過期")
;
private final String message;
}
// 行為枚舉
@AllArgsConstructor
@Getter
enum ActionType {
START(1, "開始"),
STOP(2, "暫停"),
ACHIEVE(3, "完成"),
EXPIRE(4, "過期")
;
private final int code;
private final String message;
}
然后,小明對(duì)開始編寫狀態(tài)變更功能:
class Task {
private Long taskId;
// 任務(wù)的默認(rèn)狀態(tài)為初始化
private TaskState state = TaskState.INIT;
// 活動(dòng)服務(wù)
private ActivityService activityService;
// 任務(wù)管理器
private TaskManager taskManager;
// 使用條件分支進(jìn)行任務(wù)更新
public void updateState(ActionType actionType) {
if (state == TaskState.INIT) {
if (actionType == ActionType.START) {
state = TaskState.ONGOING;
}
} else if (state == TaskState.ONGOING) {
if (actionType == ActionType.ACHIEVE) {
state = TaskState.FINISHED;
// 任務(wù)完成后進(jìn)對(duì)外部服務(wù)進(jìn)行通知
activityService.notifyFinished(taskId);
taskManager.release(taskId);
} else if (actionType == ActionType.STOP) {
state = TaskState.PAUSED;
} else if (actionType == ActionType.EXPIRE) {
state = TaskState.EXPIRED;
}
} else if (state == TaskState.PAUSED) {
if (actionType == ActionType.START) {
state = TaskState.ONGOING;
} else if (actionType == ActionType.EXPIRE) {
state = TaskState.EXPIRED;
}
}
}
}
在上述的實(shí)現(xiàn)中,小明在updateState
方法中完成了2個(gè)重要的功能:
- 接收不同的行為,然后更新當(dāng)前任務(wù)的狀態(tài);
- 當(dāng)任務(wù)過期時(shí),通知任務(wù)所屬的活動(dòng)和任務(wù)管理器。
誠(chéng)然,隨著小明的系統(tǒng)開發(fā)能力和代碼質(zhì)量意識(shí)的提升,他能夠認(rèn)識(shí)到這種功能設(shè)計(jì)存在缺陷。
“老師,我的代碼還是和之前說的那樣,不夠優(yōu)雅?!?/p>
“哦,你自己說說看有什么問題?”
“第一,方法中使用條件判斷來控制語句,但是當(dāng)條件復(fù)雜或者狀態(tài)太多時(shí),條件判斷語句會(huì)過于臃腫,可讀性差,且不具備擴(kuò)展性,維護(hù)難度也大。且增加新的狀態(tài)時(shí)要添加新的if-else語句,這違背了開閉原則,不利于程序的擴(kuò)展?!?/p>
老師表示同意,小明接著說:“第二,任務(wù)類不夠高內(nèi)聚,它在通知實(shí)現(xiàn)中感知了其他領(lǐng)域或模塊的模型,如活動(dòng)和任務(wù)管理器,這樣代碼的耦合度太高,不利于擴(kuò)展。”
老師贊賞地說道:“很好,你有意識(shí)能夠自主發(fā)現(xiàn)代碼問題所在,已經(jīng)是很大的進(jìn)步了?!?/p>
“那這個(gè)問題應(yīng)該怎么去解決呢?”小明繼續(xù)發(fā)問。
“這個(gè)同樣可以通過設(shè)計(jì)模式去優(yōu)化。首先是狀態(tài)流轉(zhuǎn)的控制可以使用狀態(tài)模式,其次,任務(wù)完成時(shí)的通知可以用到觀察者模式。”
收到指示后,小明馬上去學(xué)習(xí)了狀態(tài)模式的結(jié)構(gòu):
狀態(tài)模式:對(duì)有狀態(tài)的對(duì)象,把復(fù)雜的“判斷邏輯”提取到不同的狀態(tài)對(duì)象中,允許狀態(tài)對(duì)象在其內(nèi)部狀態(tài)發(fā)生改變時(shí)改變其行為。狀態(tài)模式包含以下主要角色:
- 環(huán)境類(Context)角色:也稱為上下文,它定義了客戶端需要的接口,內(nèi)部維護(hù)一個(gè)當(dāng)前狀態(tài),并負(fù)責(zé)具體狀態(tài)的切換。
- 抽象狀態(tài)(State)角色:定義一個(gè)接口,用以封裝環(huán)境對(duì)象中的特定狀態(tài)所對(duì)應(yīng)的行為,可以有一個(gè)或多個(gè)行為。
- 具體狀態(tài)(Concrete State)角色:實(shí)現(xiàn)抽象狀態(tài)所對(duì)應(yīng)的行為,并且在需要的情況下進(jìn)行狀態(tài)切換。
- 狀態(tài)模式可以參考我的這篇文章: JAVA設(shè)計(jì)模式第四講:行為型設(shè)計(jì)模式
- 10.5節(jié)
根據(jù)狀態(tài)模式的定義,小明將TaskState枚舉類擴(kuò)展成多個(gè)狀態(tài)類,并具備完成狀態(tài)的流轉(zhuǎn)的能力;然后優(yōu)化了任務(wù)類的實(shí)現(xiàn):
// 任務(wù)狀態(tài)抽象接口
interface State {
// 默認(rèn)實(shí)現(xiàn),不做任何處理
default void update(Task task, ActionType actionType) {
// do nothing
}
}
// 任務(wù)初始狀態(tài)
class TaskInit implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
task.setState(new TaskOngoing());
}
}
}
// 任務(wù)進(jìn)行狀態(tài)
class TaskOngoing implements State {
private ActivityService activityService;
private TaskManager taskManager;
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.ACHIEVE) {
task.setState(new TaskFinished());
// 通知
activityService.notifyFinished(taskId);
taskManager.release(taskId);
} else if (actionType == ActionType.STOP) {
task.setState(new TaskPaused());
} else if (actionType == ActionType.EXPIRE) {
task.setState(new TaskExpired());
}
}
}
// 任務(wù)暫停狀態(tài)
class TaskPaused implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
task.setState(new TaskOngoing());
} else if (actionType == ActionType.EXPIRE) {
task.setState(new TaskExpired());
}
}
}
// 任務(wù)完成狀態(tài)
class TaskFinished implements State {
}
// 任務(wù)過期狀態(tài)
class TaskExpired implements State {
}
@Data
class Task {
private Long taskId;
// 初始化為初始態(tài)
private State state = new TaskInit();
// 更新狀態(tài)
public void updateState(ActionType actionType) {
state.update(this, actionType);
}
}
小明欣喜地看到,經(jīng)過狀態(tài)模式處理后的任務(wù)類的耦合度得到降低,符合開閉原則。狀態(tài)模式的優(yōu)點(diǎn)在于符合單一職責(zé)原則,狀態(tài)類職責(zé)明確,有利于程序的擴(kuò)展。但是這樣設(shè)計(jì)的代價(jià)是狀態(tài)類的數(shù)目增加了,因此狀態(tài)流轉(zhuǎn)邏輯越復(fù)雜、需要處理的動(dòng)作越多,越有利于狀態(tài)模式的應(yīng)用。除此之外,狀態(tài)類的自身對(duì)于開閉原則的支持并沒有足夠好,如果狀態(tài)流轉(zhuǎn)邏輯變化頻繁,那么可能要慎重使用。
處理完?duì)顟B(tài)后,小明又根據(jù)老師的指導(dǎo)使用觀察者模式去優(yōu)化任務(wù)完成時(shí)的通知:
觀察者模式:指多個(gè)對(duì)象間存在一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新。這種模式有時(shí)又稱作發(fā)布-訂閱模式、模型-視圖模式,它是對(duì)象行為型模式。觀察者模式的主要角色如下。
- 抽象主題(Subject)角色:也叫抽象目標(biāo)類,它提供了一個(gè)用于保存觀察者對(duì)象的聚集類和增加、刪除觀察者對(duì)象的方法,以及通知所有觀察者的抽象方法。
- 具體主題(Concrete Subject)角色:也叫具體目標(biāo)類,它實(shí)現(xiàn)抽象目標(biāo)中的通知方法,當(dāng)具體主題的內(nèi)部狀態(tài)發(fā)生改變時(shí),通知所有注冊(cè)過的觀察者對(duì)象。
- 抽象觀察者(Observer)角色:它是一個(gè)抽象類或接口,它包含了一個(gè)更新自己的抽象方法,當(dāng)接到具體主題的更改通知時(shí)被調(diào)用。
- 具體觀察者(Concrete Observer)角色:實(shí)現(xiàn)抽象觀察者中定義的抽象方法,以便在得到目標(biāo)的更改通知時(shí) 更新自身的狀態(tài)。
- 觀察者模式可以參考我的這篇文章 JAVA設(shè)計(jì)模式第四講:行為型設(shè)計(jì)模式
- 10.1節(jié)
小明首先設(shè)計(jì)好抽象目標(biāo)和抽象觀察者,然后將活動(dòng)和 任務(wù)管理器的接收通知功能 定制成具體觀察者:
// 抽象觀察者
interface Observer {
void response(Long taskId); // 反應(yīng)
}
// 抽象目標(biāo)
abstract class Subject {
protected List<Observer> observers = new ArrayList<Observer>();
// 增加觀察者方法
public void add(Observer observer) {
observers.add(observer);
}
// 刪除觀察者方法
public void remove(Observer observer) {
observers.remove(observer);
}
// 通知觀察者方法
public void notifyObserver(Long taskId) {
for (Observer observer : observers) {
observer.response(taskId);
}
}
}
// 活動(dòng)觀察者
class ActivityObserver implements Observer {
private ActivityService activityService;
@Override
public void response(Long taskId) {
activityService.notifyFinished(taskId);
}
}
// 任務(wù)管理觀察者
class TaskManageObserver implements Observer {
private TaskManager taskManager;
@Override
public void response(Long taskId) {
taskManager.release(taskId);
}
}
最后,小明將任務(wù)進(jìn)行狀態(tài)類優(yōu)化成使用通用的通知方法,并在任務(wù)初始態(tài)執(zhí)行狀態(tài)流轉(zhuǎn)時(shí) 定義任務(wù)進(jìn)行態(tài)所需的觀察者:
// 任務(wù)進(jìn)行狀態(tài)
class TaskOngoing extends Subject implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.ACHIEVE) {
task.setState(new TaskFinished());
// 通知
notifyObserver(task.getTaskId());
} else if (actionType == ActionType.STOP) {
task.setState(new TaskPaused());
} else if (actionType == ActionType.EXPIRE) {
task.setState(new TaskExpired());
}
}
}
// 任務(wù)初始狀態(tài)
class TaskInit implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
// 增加觀察者方法
TaskOngoing taskOngoing = new TaskOngoing();
taskOngoing.add(new ActivityObserver());
taskOngoing.add(new TaskManageObserver());
task.setState(taskOngoing);
}
}
}
最終,小明設(shè)計(jì)完成的結(jié)構(gòu)類圖如下:
通過觀察者模式,小明讓任務(wù)狀態(tài)和通知方實(shí)現(xiàn)松耦合(實(shí)際上觀察者模式還沒能做到完全的解耦,如果要做進(jìn)一步的解耦可以考慮學(xué)習(xí)并使用發(fā)布-訂閱模式,這里也不再贅述)。
至此,小明成功使用狀態(tài)模式設(shè)計(jì)出了高內(nèi)聚、高擴(kuò)展性、單一職責(zé)的任務(wù)的整個(gè)狀態(tài)機(jī)實(shí)現(xiàn),以及做到松耦合的、符合依賴倒置原則的任務(wù)狀態(tài)變更通知方式。
“老師,我逐漸能意識(shí)到代碼的設(shè)計(jì)缺陷,并學(xué)會(huì)利用較為復(fù)雜的設(shè)計(jì)模式做優(yōu)化?!?/p>
“不錯(cuò),再接再厲!”
4、活動(dòng)的迭代重構(gòu)
“小明,這次又有一個(gè)新的任務(wù)?!崩蠋煶霈F(xiàn)在正在認(rèn)真閱讀《設(shè)計(jì)模式》的小明的面前。
“好的。剛好我已經(jīng)學(xué)習(xí)了設(shè)計(jì)模式的原理,終于可以派上用場(chǎng)了?!?/p>
“之前你設(shè)計(jì)開發(fā)了活動(dòng)模型,現(xiàn)在我們需要在任務(wù)型活動(dòng)的參與方法上增加一層風(fēng)險(xiǎn)控制。”
“OK。借此機(jī)會(huì),我也想重構(gòu)一下之前的設(shè)計(jì)。”
活動(dòng)模型的特點(diǎn)在于其組成部分較多,小明原先的活動(dòng)模型的構(gòu)建方式是這樣的:
// 抽象活動(dòng)接口
interface ActivityInterface {
void participate(Long userId);
}
// 活動(dòng)類
class Activity implements ActivityInterface {
private String type;
private Long id;
private String name;
private Integer scene;
private String material;
public Activity(String type) {
this.type = type;
// id的構(gòu)建部分依賴于活動(dòng)的type
if ("period".equals(type)) {
id = 0L;
}
}
public Activity(String type, Long id) {
this.type = type;
this.id = id;
}
public Activity(String type, Long id, Integer scene) {
this.type = type;
this.id = id;
this.scene = scene;
}
public Activity(String type, String name, Integer scene, String material) {
this.type = type;
this.scene = scene;
this.material = material;
// name的構(gòu)建完全依賴于活動(dòng)的type
if ("period".equals(type)) {
this.id = 0L;
this.name = "period" + name;
} else {
this.name = "normal" + name;
}
}
// 參與活動(dòng)
@Override
public void participate(Long userId) {
// do nothing
}
}
// 任務(wù)型活動(dòng)
class TaskActivity extends Activity {
private Task task;
public TaskActivity(String type, String name, Integer scene, String material, Task task) {
super(type, name, scene, material);
this.task = task;
}
// 參與任務(wù)型活動(dòng)
@Override
public void participate(Long userId) {
// 更新任務(wù)狀態(tài)為進(jìn)行中
task.getState().update(task, ActionType.START);
}
}
經(jīng)過自主分析,小明發(fā)現(xiàn)活動(dòng)的構(gòu)造不夠合理,主要問題表現(xiàn)在:
- 活動(dòng)的構(gòu)造組件較多,導(dǎo)致可以組合的構(gòu)造函數(shù)太多,尤其是在模型增加字段時(shí)還需要去修改構(gòu)造函數(shù);
- 部分組件的構(gòu)造存在一定的順序關(guān)系,但是當(dāng)前的實(shí)現(xiàn)沒有體現(xiàn)順序,導(dǎo)致構(gòu)造邏輯比較混亂,并且存在部分重復(fù)的代碼。
發(fā)現(xiàn)問題后,小明回憶自己的學(xué)習(xí)成果,馬上想到可以使用創(chuàng)建型模式中的建造者模式去做重構(gòu):
建造者模式:指將一個(gè)復(fù)雜對(duì)象的構(gòu)造與它的表示分離,使同樣的構(gòu)建過程可以創(chuàng)建不同的表示。它是將一個(gè)復(fù)雜的對(duì)象分解為多個(gè)簡(jiǎn)單的對(duì)象,然后一步一步構(gòu)建而成。它將變與不變相分離,即產(chǎn)品的組成部分是不變的,但每一部分是可以靈活選擇的。建造者模式的主要角色如下:
- 產(chǎn)品角色(Product):它是包含多個(gè)組成部件的復(fù)雜對(duì)象,由具體建造者來創(chuàng)建其各個(gè)零部件。
- 抽象建造者(Builder):它是一個(gè)包含創(chuàng)建產(chǎn)品各個(gè)子部件的抽象方法的接口,通常還包含一個(gè)返回復(fù)雜產(chǎn)品的方法 getResult()。
- 具體建造者(Concrete Builder):實(shí)現(xiàn) Builder 接口,完成復(fù)雜產(chǎn)品的各個(gè)部件的具體創(chuàng)建方法。
- 指揮者(Director):它調(diào)用建造者對(duì)象中的部件構(gòu)造與裝配方法完成復(fù)雜對(duì)象的創(chuàng)建,在指揮者中不涉及具體產(chǎn)品的信息。
- 建造者模式可以參考我的這篇文章 JAVA設(shè)計(jì)模式第二講:創(chuàng)建型設(shè)計(jì)模式
- 8.3節(jié)
根據(jù)建造者模式的定義,上述活動(dòng)的每個(gè)字段都是一個(gè)產(chǎn)品。于是,小明可以通過在活動(dòng)里面實(shí)現(xiàn)靜態(tài)的建造者類來簡(jiǎn)易地實(shí)現(xiàn):
// 活動(dòng)類
class Activity implements ActivityInterface {
protected String type;
protected Long id;
protected String name;
protected Integer scene;
protected String material;
// 全參構(gòu)造函數(shù)
public Activity(String type, Long id, String name, Integer scene, String material) {
this.type = type;
this.id = id;
this.name = name;
this.scene = scene;
this.material = material;
}
@Override
public void participate(Long userId) {
// do nothing
}
// 靜態(tài)建造器類,使用奇異遞歸模板模式允許繼承并返回繼承建造器類
public static class Builder<T extends Builder<T>> {
protected String type;
protected Long id;
protected String name;
protected Integer scene;
protected String material;
public T setType(String type) {
this.type = type;
return (T) this;
}
public T setId(Long id) {
this.id = id;
return (T) this;
}
public T setId() {
if ("period".equals(this.type)) {
this.id = 0L;
}
return (T) this;
}
public T setScene(Integer scene) {
this.scene = scene;
return (T) this;
}
public T setMaterial(String material) {
this.material = material;
return (T) this;
}
public T setName(String name) {
if ("period".equals(this.type)) {
this.name = "period" + name;
} else {
this.name = "normal" + name;
}
return (T) this;
}
public Activity build(){
return new Activity(type, id, name, scene, material);
}
}
}
// 任務(wù)型活動(dòng)
class TaskActivity extends Activity {
protected Task task;
// 全參構(gòu)造函數(shù)
public TaskActivity(String type, Long id, String name, Integer scene, String material, Task task) {
super(type, id, name, scene, material);
this.task = task;
}
// 參與任務(wù)型活動(dòng)
@Override
public void participate(Long userId) {
// 更新任務(wù)狀態(tài)為進(jìn)行中
task.getState().update(task, ActionType.START);
}
// 繼承建造器類
public static class Builder extends Activity.Builder<Builder> {
private Task task;
public Builder setTask(Task task) {
this.task = task;
return this;
}
public TaskActivity build(){
return new TaskActivity(type, id, name, scene, material, task);
}
}
}
小明發(fā)現(xiàn),上面的建造器沒有使用諸如抽象建造器類等完整的實(shí)現(xiàn),但是基本是完成了活動(dòng)各個(gè)組件的建造流程。使用建造器的模式下,可以先按順序構(gòu)建字段type,然后依次構(gòu)建其他組件,最后使用build方法獲取建造完成的活動(dòng)。這種設(shè)計(jì)一方面封裝性好,構(gòu)建和表示分離;另一方面擴(kuò)展性好,各個(gè)具體的建造者相互獨(dú)立,有利于系統(tǒng)的解耦。可以說是一次比較有價(jià)值的重構(gòu)。在實(shí)際的應(yīng)用中,如果字段類型多,同時(shí)各個(gè)字段只需要簡(jiǎn)單的賦值,可以直接引用Lombok的 @Builder注解來實(shí)現(xiàn)輕量的建造者。
- Lombok的使用和注意事項(xiàng)可以參考這篇文章:開發(fā)工具篇第十二講:常用開發(fā)庫(kù) - Lombok工具庫(kù)詳解
重構(gòu)完活動(dòng)構(gòu)建的設(shè)計(jì)后,小明開始對(duì)參加活動(dòng)方法增加風(fēng)控。最簡(jiǎn)單的方式肯定是直接修改目標(biāo)方法:
public void participate(Long userId) {
// 對(duì)目標(biāo)用戶做風(fēng)險(xiǎn)控制,失敗則拋出異常
Risk.doControl(userId);
// 更新任務(wù)狀態(tài)為進(jìn)行中
task.state.update(task, ActionType.START);
}
但是考慮到,最好能盡可能避免對(duì)舊方法的直接修改,同時(shí)為方法增加風(fēng)控,也是一類比較常見的功能新增,可能會(huì)在多處使用。
“老師,風(fēng)險(xiǎn)控制會(huì)出現(xiàn)在多種活動(dòng)的參與方法中嗎?”
“有這個(gè)可能性。有的活動(dòng)需要風(fēng)險(xiǎn)控制,有的不需要。風(fēng)控像是在適當(dāng)?shù)臅r(shí)候?qū)⑴c這個(gè)方法的裝飾?!?/p>
“對(duì)了,裝飾器模式!”
小明馬上想到用裝飾器模式來完成設(shè)計(jì):
裝飾器模式的定義:指在不改變現(xiàn)有對(duì)象結(jié)構(gòu)的情況下,動(dòng)態(tài)地給該對(duì)象增加一些職責(zé)(即增加其額外功能)的模式,它屬于對(duì)象結(jié)構(gòu)型模式。裝飾器模式主要包含以下角色:
- 抽象構(gòu)件(Component)角色:定義一個(gè)抽象接口以規(guī)范準(zhǔn)備接收附加責(zé)任的對(duì)象。
- 具體構(gòu)件(ConcreteComponent)角色:實(shí)現(xiàn)抽象構(gòu)件,通過裝飾角色為其添加一些職責(zé)。
- 抽象裝飾(Decorator)角色:繼承抽象構(gòu)件,并包含具體構(gòu)件的實(shí)例,可以通過其子類擴(kuò)展具體構(gòu)件的功能。
- 具體裝飾(ConcreteDecorator)角色:實(shí)現(xiàn)抽象裝飾的相關(guān)方法,并給具體構(gòu)件對(duì)象添加附加的責(zé)任。
- 裝飾者模式可以參考我的這篇文章 JAVA設(shè)計(jì)模式第三講:結(jié)構(gòu)型設(shè)計(jì)模式
- 9.3節(jié)
小明使用了裝飾器模式后,新的代碼就變成了這樣:
// 抽象裝飾角色
abstract class ActivityDecorator implements ActivityInterface {
protected ActivityInterface activity;
public ActivityDecorator(ActivityInterface activity) {
this.activity = activity;
}
public abstract void participate(Long userId);
}
// 能夠?qū)顒?dòng)做風(fēng)險(xiǎn)控制的包裝類
class RiskControlDecorator extends ActivityDecorator {
public RiskControlDecorator(ActivityInterface activity) {
super(activity);
}
@Override
public void participate(Long userId) {
// 對(duì)目標(biāo)用戶做風(fēng)險(xiǎn)控制,失敗則拋出異常
Risk.doControl(userId);
// 更新任務(wù)狀態(tài)為進(jìn)行中
activity.participate(userId);
}
}
最終,小明設(shè)計(jì)完成的結(jié)構(gòu)類圖如下:
最終,小明通過自己的思考分析,結(jié)合學(xué)習(xí)的設(shè)計(jì)模式知識(shí),完成了活動(dòng)模型的重構(gòu)和迭代。
“老師,我已經(jīng)能做到自主分析功能特點(diǎn),并合理應(yīng)用設(shè)計(jì)模式去完成程序設(shè)計(jì)和代碼重構(gòu)了,實(shí)在太感謝您了。”
“設(shè)計(jì)模式作為一種軟件設(shè)計(jì)的最佳實(shí)踐,你已經(jīng)很好地理解并應(yīng)用于實(shí)踐了,非常不錯(cuò)。但學(xué)海無涯,還需持續(xù)精進(jìn)!”
5、結(jié)語
本文以三個(gè)實(shí)際場(chǎng)景為出發(fā)點(diǎn),借助小明和老師兩個(gè)虛擬的人物,試圖以一種較為詼諧的“對(duì)話”方式來講述設(shè)計(jì)模式的應(yīng)用場(chǎng)景、優(yōu)點(diǎn)和缺點(diǎn)。如果大家想要去系統(tǒng)性地了解設(shè)計(jì)模式,也可以通過市面上很多的教材進(jìn)行學(xué)習(xí),都介紹了經(jīng)典的23種設(shè)計(jì)模式的結(jié)構(gòu)和實(shí)現(xiàn)。不過,很多教材的內(nèi)容即便配合了大量的示例,但有時(shí)也會(huì)讓人感到費(fèi)解,主要原因在于:一方面,很多案例比較脫離實(shí)際的應(yīng)用場(chǎng)景;另一方面,部分設(shè)計(jì)模式顯然更適用于大型復(fù)雜的結(jié)構(gòu)設(shè)計(jì),而當(dāng)其應(yīng)用到簡(jiǎn)單的場(chǎng)景時(shí),仿佛讓代碼變得更加繁瑣、冗余。因此,本文希望通過這種“對(duì)話+代碼展示+結(jié)構(gòu)類圖”的方式,以一種更易懂的方式來介紹設(shè)計(jì)模式。
當(dāng)然,本文只講述了部分比較常見的設(shè)計(jì)模式,還有其他的設(shè)計(jì)模式,仍然需要同學(xué)們?nèi)パ凶x經(jīng)典著作,舉一反三,學(xué)以致用。我們也希望通過學(xué)習(xí)設(shè)計(jì)模式能讓更多的同學(xué)在系統(tǒng)設(shè)計(jì)能力上得到提升。
6、參考資料
- [1] java-design-patterns.com
- [2] Java設(shè)計(jì)模式:23種設(shè)計(jì)模式全面解析
作者簡(jiǎn)介
嘉凱、楊柳,來自美團(tuán)金融服務(wù)平臺(tái)/聯(lián)名卡研發(fā)團(tuán)隊(duì)。文章來源:http://www.zghlxwxcb.cn/news/detail-672418.html
文章來源:設(shè)計(jì)模式二三事文章來源地址http://www.zghlxwxcb.cn/news/detail-672418.html
到了這里,關(guān)于JAVA設(shè)計(jì)模式第十二講:大廠實(shí)踐 - 美團(tuán): 設(shè)計(jì)模式二三事的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!