設計模式二三事
設計模式是衆多軟件開發人員經過長時間的試錯和應用總結出來的,解決特定問題的一系列方案。現行的部分教材在介紹設計模式時,有些會因爲案例脫離實際應用場景而令人費解,有些又會因爲場景簡單而顯得有些小題大做。
本文會根據在美團金融服務平臺設計開發時的經驗,結合實際的案例,並採用 “師生對話” 這種相對詼諧的形式去講解幾類常用設計模式的應用。希望能對想提升系統設計能力的同學有所幫助或啓發。
-
引言
-
獎勵的發放策略
-
任務模型的設計
-
活動的迭代重構
-
結語
-
參考資料
-
作者簡介
引言
話說這是在程序員世界裏一對師徒的對話:
“老師,我最近在寫代碼時總感覺自己的代碼很不優雅,有什麼辦法能優化嗎?”
“嗯,可以考慮通過教材系統學習,從註釋、命名、方法和異常等多方面實現整潔代碼。”
“然而,我想說的是,我的代碼是符合各種編碼規範的,但是從實現上卻總是感覺不夠簡潔,而且總是需要反覆修改!” 學生小明嘆氣道。
老師看了看小明的代碼說:“我明白了,這是系統設計上的缺陷。總結就是抽象不夠、可讀性低、不夠健壯。”
“對對對,那怎麼能迅速提高代碼的可讀性、健壯性、擴展性呢?” 小明急不可耐地問道。
老師敲了敲小明的頭:“不要太浮躁,沒有什麼方法能讓你立刻成爲系統設計專家。但是對於你的問題,我想設計模式可以幫到你。”
“設計模式?” 小明不解。
“是的。” 老師點了點頭,“世上本沒有路,走的人多了,便變成了路。在程序員的世界中,本沒有設計模式,寫代碼是人多了,他們便總結出了一套能提高開發和維護效率的套路,這就是設計模式。設計模式不是什麼教條或者範式,它可以說是一種在特定場景下普適且可複用的解決方案,是一種可以用於提高代碼可讀性、可擴展性、可維護性和可測性的最佳實踐。”
“哦哦,我懂了,那我應該如何去學習呢?”
“不急,接下來我來帶你慢慢了解設計模式。”
獎勵的發放策略
第一天,老師問小明:“你知道活動營銷嗎?”
“這我知道,活動營銷是指企業通過參與社會關注度高的已有活動,或整合有效的資源自主策劃大型活動,從而迅速提高企業及其品牌的知名度、美譽度和影響力,常見的比如有抽獎、紅包等。”
老師點點頭:“是的。我們假設現在就要做一個營銷,需要用戶參與一個活動,然後完成一系列的任務,最後可以得到一些獎勵作爲回報。活動的獎勵包含美團外賣、酒旅和美食等多種品類券,現在需要你幫忙設計一套獎勵發放方案。”
因爲之前有過類似的開發經驗,拿到需求的小明二話不說開始了編寫起了代碼:
// 獎勵服務
class RewardService {
// 外部服務
private WaimaiService waimaiService;
private HotelService hotelService;
private FoodService foodService;
// 使用對入參的條件判斷進行發獎
public void issueReward(String rewardType, Object ... params) {
if ("Waimai".equals(rewardType)) {
WaimaiRequest request = new WaimaiRequest();
// 構建入參
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,然後發給老師看。
“假如我們即將接入新的打車券,這是否意味着你必須要修改這部分代碼?” 老師問道。
小明愣了一愣,沒等反應過來老師又問:” 假如後面美團外賣的發券接口發生了改變或者替換,這段邏輯是否必須要同步進行修改?”
小明陷入了思考之中,一時間沒法回答。
經驗豐富的老師一針見血地指出了這段設計的問題:“你這段代碼有兩個主要問題,一是不符合開閉原則,可以預見,如果後續新增品類券的話,需要直接修改主幹代碼,而我們提倡代碼應該是對修改封閉的;二是不符合迪米特法則,發獎邏輯和各個下游接口高度耦合,這導致接口的改變將直接影響到代碼的組織,使得代碼的可維護性降低。”
小明恍然大悟:“那我將各個同下遊接口交互的功能抽象成單獨的服務,封裝其參數組裝及異常處理,使得發獎主邏輯與其解耦,是否就能更具備擴展性和可維護性?”
“這是個不錯的思路。之前跟你介紹過設計模式,這個案例就可以使用策略模式和適配器模式來優化。”
小明藉此機會學習了這兩個設計模式。首先是策略模式:
策略模式定義了一系列的算法,並將每一個算法封裝起來,使它們可以相互替換。策略模式通常包含以下角色:
抽象策略(Strategy)類:定義了一個公共接口,各種不同的算法以不同的方式實現這個接口,環境角色使用這個接口調用不同的算法,一般使用接口或抽象類實現。
具體策略(Concrete Strategy)類:實現了抽象策略定義的接口,提供具體的算法實現。
環境(Context)類:持有一個策略類的引用,最終給客戶端調用。
然後是適配器模式:
適配器模式:將一個類的接口轉換成客戶希望的另外一個接口,使得原本由於接口不兼容而不能一起工作的那些類能一起工作。適配器模式包含以下主要角色:
目標(Target)接口:當前系統業務所期待的接口,它可以是抽象類或接口。
適配者(Adaptee)類:它是被訪問和適配的現存組件庫中的組件接口。
適配器(Adapter)類:它是一個轉換器,通過繼承或引用適配者的對象,把適配者接口轉換成目標接口,讓客戶按目標接口的格式訪問適配者。
結合優化思路,小明首先設計出了策略接口,並通過適配器的思想將各個下游接口類適配成策略類:
// 策略接口
interface Strategy {
void issue(Object ... params);
}
// 外賣策略
class Waimai implements Strategy {
private WaimaiService waimaiService;
@Override
public void issue(Object... params) {
WaimaiRequest request = new WaimaiRequest();
// 構建入參
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);
}
}
然後,小明創建策略模式的環境類,並供獎勵服務調用:
// 使用分支判斷獲取的策略上下文
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!");
}
}
}
// 優化後的策略服務
class RewardService {
public void issueReward(String rewardType, Object ... params) {
Strategy strategy = StrategyContext.getStrategy(rewardType);
strategy.issue(params);
}
}
小明的代碼經過優化後,雖然結構和設計上比之前要複雜不少,但考慮到健壯性和拓展性,還是非常值得的。
“看,我這次優化後的版本是不是很完美?” 小明洋洋得意地說。
“耦合度確實降低了,但還能做的更好。”
“怎麼做?” 小明有點疑惑。
“我問你,策略類是有狀態的模型嗎?如果不是是否可以考慮做成單例的?”
“的確如此。” 小明似乎明白了。
“還有一點,環境類的獲取策略方法職責很明確,但是你依然沒有做到完全對修改封閉。”
經過老師的點撥,小明很快也領悟到了要點:“那我可以將策略類單例化以減少開銷,並實現自注冊的功能徹底解決分支判斷。”
小明列出單例模式的要點:
單例模式設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
最終,小明在策略環境類中使用一個註冊表來記錄各個策略類的註冊信息,並提供接口供策略類調用進行註冊。同時使用餓漢式單例模式去優化策略類的設計:
// 策略上下文,用於管理策略的註冊和獲取
class StrategyContext {
private static final Map<String, Strategy> registerMap = new HashMap<>();
// 註冊策略
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 {
// 類註冊方法
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();
// 構建入參
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);
}
}
最終,小明設計完成的結構類圖如下:
獎勵發放策略_類圖
如果使用了 Spring 框架,還可以利用 Spring 的 Bean 機制來代替上述的部分設計,直接使用@Component
和@PostConstruct
註解即可完成單例的創建和註冊,代碼會更加簡潔。
至此,經過了多次討論、反思和優化,小明終於得到了一套低耦合高內聚,同時符合開閉原則的設計。
“老師,我開始學會利用設計模式去解決已發現的問題。這次我做得怎麼樣?”
“合格。但是,依然要戒驕戒躁。”
任務模型的設計
“之前讓你設計獎勵發放策略你還記得嗎?” 老師忽然問道。
“當然記得。一個好的設計模式,能讓工作事半功倍。” 小明答道。
“嗯,那會提到了活動營銷的組成部分,除了獎勵之外,貌似還有任務吧。”
小明點了點頭,老師接着說:“現在,我想讓你去完成任務模型的設計。你需要重點關注狀態的流轉變更,以及狀態變更後的消息通知。”
小明欣然接下了老師給的難題。他首先定義了一套任務狀態的枚舉和行爲的枚舉:
// 任務狀態枚舉
@AllArgsConstructor
@Getter
enum TaskState {
INIT("初始化"),
ONGOING( "進行中"),
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;
}
然後,小明對開始編寫狀態變更功能:
class Task {
private Long taskId;
// 任務的默認狀態爲初始化
private TaskState state = TaskState.INIT;
// 活動服務
private ActivityService activityService;
// 任務管理器
private TaskManager taskManager;
// 使用條件分支進行任務更新
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;
// 任務完成後進對外部服務進行通知
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;
}
}
}
}
在上述的實現中,小明在updateState
方法中完成了 2 個重要的功能:
-
接收不同的行爲,然後更新當前任務的狀態;
-
當任務過期時,通知任務所屬的活動和任務管理器。
誠然,隨着小明的系統開發能力和代碼質量意識的提升,他能夠認識到這種功能設計存在缺陷。
“老師,我的代碼還是和之前說的那樣,不夠優雅。”
“哦,你自己說說看有什麼問題?”
“第一,方法中使用條件判斷來控制語句,但是當條件複雜或者狀態太多時,條件判斷語句會過於臃腫,可讀性差,且不具備擴展性,維護難度也大。且增加新的狀態時要添加新的 if-else 語句,這違背了開閉原則,不利於程序的擴展。”
老師表示同意,小明接着說:“第二,任務類不夠高內聚,它在通知實現中感知了其他領域或模塊的模型,如活動和任務管理器,這樣代碼的耦合度太高,不利於擴展。”
老師讚賞地說道:“很好,你有意識能夠自主發現代碼問題所在,已經是很大的進步了。”
“那這個問題應該怎麼去解決呢?” 小明繼續發問。
“這個同樣可以通過設計模式去優化。首先是狀態流轉的控制可以使用狀態模式,其次,任務完成時的通知可以用到觀察者模式。”
收到指示後,小明馬上去學習了狀態模式的結構:
狀態模式:對有狀態的對象,把複雜的 “判斷邏輯” 提取到不同的狀態對象中,允許狀態對象在其內部狀態發生改變時改變其行爲。狀態模式包含以下主要角色:
環境類(Context)角色:也稱爲上下文,它定義了客戶端需要的接口,內部維護一個當前狀態,並負責具體狀態的切換。
抽象狀態(State)角色:定義一個接口,用以封裝環境對象中的特定狀態所對應的行爲,可以有一個或多個行爲。
具體狀態(Concrete State)角色:實現抽象狀態所對應的行爲,並且在需要的情況下進行狀態切換。
根據狀態模式的定義,小明將 TaskState 枚舉類擴展成多個狀態類,並具備完成狀態的流轉的能力;然後優化了任務類的實現:
// 任務狀態抽象接口
interface State {
// 默認實現,不做任何處理
default void update(Task task, ActionType actionType) {
// do nothing
}
}
// 任務初始狀態
class TaskInit implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
task.setState(new TaskOngoing());
}
}
}
// 任務進行狀態
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());
}
}
}
// 任務暫停狀態
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());
}
}
}
// 任務完成狀態
class TaskFinished implements State {
}
// 任務過期狀態
class TaskExpired implements State {
}
@Data
class Task {
private Long taskId;
// 初始化爲初始態
private State state = new TaskInit();
// 更新狀態
public void updateState(ActionType actionType) {
state.update(this, actionType);
}
}
小明欣喜地看到,經過狀態模式處理後的任務類的耦合度得到降低,符合開閉原則。狀態模式的優點在於符合單一職責原則,狀態類職責明確,有利於程序的擴展。但是這樣設計的代價是狀態類的數目增加了,因此狀態流轉邏輯越複雜、需要處理的動作越多,越有利於狀態模式的應用。除此之外,狀態類的自身對於開閉原則的支持並沒有足夠好,如果狀態流轉邏輯變化頻繁,那麼可能要慎重使用。
處理完狀態後,小明又根據老師的指導使用觀察者模式去優化任務完成時的通知:
觀察者模式:指多個對象間存在一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。這種模式有時又稱作發佈 - 訂閱模式、模型 - 視圖模式,它是對象行爲型模式。觀察者模式的主要角色如下。
抽象主題(Subject)角色:也叫抽象目標類,它提供了一個用於保存觀察者對象的聚集類和增加、刪除觀察者對象的方法,以及通知所有觀察者的抽象方法。
具體主題(Concrete Subject)角色:也叫具體目標類,它實現抽象目標中的通知方法,當具體主題的內部狀態發生改變時,通知所有註冊過的觀察者對象。
抽象觀察者(Observer)角色:它是一個抽象類或接口,它包含了一個更新自己的抽象方法,當接到具體主題的更改通知時被調用。
具體觀察者(Concrete Observer)角色:實現抽象觀察者中定義的抽象方法,以便在得到目標的更改通知時更新自身的狀態。
小明首先設計好抽象目標和抽象觀察者,然後將活動和任務管理器的接收通知功能定製成具體觀察者:
// 抽象觀察者
interface Observer {
void response(Long taskId); // 反應
}
// 抽象目標
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);
}
}
}
// 活動觀察者
class ActivityObserver implements Observer {
private ActivityService activityService;
@Override
public void response(Long taskId) {
activityService.notifyFinished(taskId);
}
}
// 任務管理觀察者
class TaskManageObserver implements Observer {
private TaskManager taskManager;
@Override
public void response(Long taskId) {
taskManager.release(taskId);
}
}
最後,小明將任務進行狀態類優化成使用通用的通知方法,並在任務初始態執行狀態流轉時定義任務進行態所需的觀察者:
// 任務進行狀態
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());
}
}
}
// 任務初始狀態
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);
}
}
}
最終,小明設計完成的結構類圖如下:
任務模型設計_類圖
通過觀察者模式,小明讓任務狀態和通知方實現松耦合(實際上觀察者模式還沒能做到完全的解耦,如果要做進一步的解耦可以考慮學習並使用發佈 - 訂閱模式,這裏也不再贅述)。
至此,小明成功使用狀態模式設計出了高內聚、高擴展性、單一職責的任務的整個狀態機實現,以及做到松耦合的、符合依賴倒置原則的任務狀態變更通知方式。
“老師,我逐漸能意識到代碼的設計缺陷,並學會利用較爲複雜的設計模式做優化。”
“不錯,再接再厲!”
活動的迭代重構
“小明,這次又有一個新的任務。” 老師出現在正在認真閱讀《設計模式》的小明的面前。
“好的。剛好我已經學習了設計模式的原理,終於可以派上用場了。”
“之前你設計開發了活動模型,現在我們需要在任務型活動的參與方法上增加一層風險控制。”
“OK。藉此機會,我也想重構一下之前的設計。”
活動模型的特點在於其組成部分較多,小明原先的活動模型的構建方式是這樣的:
// 抽象活動接口
interface ActivityInterface {
void participate(Long userId);
}
// 活動類
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的構建部分依賴於活動的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的構建完全依賴於活動的type
if ("period".equals(type)) {
this.id = 0L;
this.name = "period" + name;
} else {
this.name = "normal" + name;
}
}
// 參與活動
@Override
public void participate(Long userId) {
// do nothing
}
}
// 任務型活動
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;
}
// 參與任務型活動
@Override
public void participate(Long userId) {
// 更新任務狀態爲進行中
task.getState().update(task, ActionType.START);
}
}
經過自主分析,小明發現活動的構造不夠合理,主要問題表現在:
-
活動的構造組件較多,導致可以組合的構造函數太多,尤其是在模型增加字段時還需要去修改構造函數;
-
部分組件的構造存在一定的順序關係,但是當前的實現沒有體現順序,導致構造邏輯比較混亂,並且存在部分重複的代碼。
發現問題後,小明回憶自己的學習成果,馬上想到可以使用創建型模式中的建造者模式去做重構:
建造者模式:指將一個複雜對象的構造與它的表示分離,使同樣的構建過程可以創建不同的表示。它是將一個複雜的對象分解爲多個簡單的對象,然後一步一步構建而成。它將變與不變相分離,即產品的組成部分是不變的,但每一部分是可以靈活選擇的。建造者模式的主要角色如下:
產品角色(Product):它是包含多個組成部件的複雜對象,由具體建造者來創建其各個零部件。
抽象建造者(Builder):它是一個包含創建產品各個子部件的抽象方法的接口,通常還包含一個返回複雜產品的方法 getResult()。
具體建造者 (Concrete Builder):實現 Builder 接口,完成複雜產品的各個部件的具體創建方法。
指揮者(Director):它調用建造者對象中的部件構造與裝配方法完成複雜對象的創建,在指揮者中不涉及具體產品的信息。
根據建造者模式的定義,上述活動的每個字段都是一個產品。於是,小明可以通過在活動裏面實現靜態的建造者類來簡易地實現:
// 活動類
class Activity implements ActivityInterface {
protected String type;
protected Long id;
protected String name;
protected Integer scene;
protected String material;
// 全參構造函數
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
}
// 靜態建造器類,使用奇異遞歸模板模式允許繼承並返回繼承建造器類
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);
}
}
}
// 任務型活動
class TaskActivity extends Activity {
protected Task task;
// 全參構造函數
public TaskActivity(String type, Long id, String name, Integer scene, String material, Task task) {
super(type, id, name, scene, material);
this.task = task;
}
// 參與任務型活動
@Override
public void participate(Long userId) {
// 更新任務狀態爲進行中
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);
}
}
}
小明發現,上面的建造器沒有使用諸如抽象建造器類等完整的實現,但是基本是完成了活動各個組件的建造流程。使用建造器的模式下,可以先按順序構建字段 type,然後依次構建其他組件,最後使用 build 方法獲取建造完成的活動。這種設計一方面封裝性好,構建和表示分離;另一方面擴展性好,各個具體的建造者相互獨立,有利於系統的解耦。可以說是一次比較有價值的重構。在實際的應用中,如果字段類型多,同時各個字段只需要簡單的賦值,可以直接引用 Lombok 的 @Builder 註解來實現輕量的建造者。
重構完活動構建的設計後,小明開始對參加活動方法增加風控。最簡單的方式肯定是直接修改目標方法:
public void participate(Long userId) {
// 對目標用戶做風險控制,失敗則拋出異常
Risk.doControl(userId);
// 更新任務狀態爲進行中
task.state.update(task, ActionType.START);
}
但是考慮到,最好能儘可能避免對舊方法的直接修改,同時爲方法增加風控,也是一類比較常見的功能新增,可能會在多處使用。
“老師,風險控制會出現在多種活動的參與方法中嗎?”
“有這個可能性。有的活動需要風險控制,有的不需要。風控像是在適當的時候對參與這個方法的裝飾。”
“對了,裝飾器模式!”
小明馬上想到用裝飾器模式來完成設計:
裝飾器模式的定義:指在不改變現有對象結構的情況下,動態地給該對象增加一些職責(即增加其額外功能)的模式,它屬於對象結構型模式。裝飾器模式主要包含以下角色:
抽象構件(Component)角色:定義一個抽象接口以規範準備接收附加責任的對象。
具體構件(ConcreteComponent)角色:實現抽象構件,通過裝飾角色爲其添加一些職責。
抽象裝飾(Decorator)角色:繼承抽象構件,幷包含具體構件的實例,可以通過其子類擴展具體構件的功能。
具體裝飾(ConcreteDecorator)角色:實現抽象裝飾的相關方法,並給具體構件對象添加附加的責任。
小明使用了裝飾器模式後,新的代碼就變成了這樣:
// 抽象裝飾角色
abstract class ActivityDecorator implements ActivityInterface {
protected ActivityInterface activity;
public ActivityDecorator(ActivityInterface activity) {
this.activity = activity;
}
public abstract void participate(Long userId);
}
// 能夠對活動做風險控制的包裝類
class RiskControlDecorator extends ActivityDecorator {
public RiskControlDecorator(ActivityInterface activity) {
super(activity);
}
@Override
public void participate(Long userId) {
// 對目標用戶做風險控制,失敗則拋出異常
Risk.doControl(userId);
// 更新任務狀態爲進行中
activity.participate(userId);
}
}
最終,小明設計完成的結構類圖如下:
活動迭代重構_類圖
最終,小明通過自己的思考分析,結合學習的設計模式知識,完成了活動模型的重構和迭代。
“老師,我已經能做到自主分析功能特點,併合理應用設計模式去完成程序設計和代碼重構了,實在太感謝您了。”
“設計模式作爲一種軟件設計的最佳實踐,你已經很好地理解並應用於實踐了,非常不錯。但學海無涯,還需持續精進!”
結語
本文以三個實際場景爲出發點,藉助小明和老師兩個虛擬的人物,試圖以一種較爲詼諧的 “對話” 方式來講述設計模式的應用場景、優點和缺點。如果大家想要去系統性地瞭解設計模式,也可以通過市面上很多的教材進行學習,都介紹了經典的 23 種設計模式的結構和實現。不過,很多教材的內容即便配合了大量的示例,但有時也會讓人感到費解,主要原因在於:一方面,很多案例比較脫離實際的應用場景;另一方面,部分設計模式顯然更適用於大型複雜的結構設計,而當其應用到簡單的場景時,彷彿讓代碼變得更加繁瑣、冗餘。因此,本文希望通過這種 “對話 + 代碼展示 + 結構類圖” 的方式,以一種更易懂的方式來介紹設計模式。
當然,本文只講述了部分比較常見的設計模式,還有其他的設計模式,仍然需要同學們去研讀經典著作,舉一反三,學以致用。我們也希望通過學習設計模式能讓更多的同學在系統設計能力上得到提升。
參考資料
-
Gamma E. 設計模式: 可複用面向對象軟件的基礎 [M]. 機械工業出版社, 2007.
-
弗里曼. Head First 設計模式 [M]. 中國電力出版社, 2007.
作者簡介
嘉凱、楊柳,來自美團金融服務平臺 / 聯名卡研發團隊。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/H2toewJKEwq1mXme_iMWkA