前言
最近準(zhǔn)備學(xué)習(xí)下之前項(xiàng)目中用到的設(shè)計(jì)模式,這里代碼都只展示核心業(yè)務(wù)代碼,省略去大多不重要的代碼。
代碼大多是之前一起工作的小伙伴coding出來(lái)的,我這里做一個(gè)學(xué)習(xí)和總結(jié),我相信技術(shù)能力的提高都是先從模仿開始的,學(xué)習(xí)別人的代碼及設(shè)計(jì)思想也是一種提升的方式。
?
后續(xù)還會(huì)有觀察者模式、責(zé)任鏈模式的博客產(chǎn)出,都是工作中正式運(yùn)用到的場(chǎng)景輸出,希望對(duì)看文章的你也有啟發(fā)和幫助。
一、業(yè)務(wù)需求
?
我之前做過(guò)在線問(wèn)診的需求,業(yè)務(wù)復(fù)雜,很多節(jié)點(diǎn)需要出發(fā)消息推送,比如用戶下單?需要給醫(yī)生推送短信和push、醫(yī)生接診?需要給用戶發(fā)送短信、push、微信等。產(chǎn)品說(shuō)后期會(huì)有很多不同的節(jié)點(diǎn)觸發(fā)消息發(fā)送。
?
這里就開始抽象需求,首先是發(fā)送消息,很多消息是同樣的策略,只是組裝的數(shù)據(jù)是動(dòng)態(tài)拼接的,所以抽象出:buildSms()、buildPush()、buildWechat()?等構(gòu)造消息體的方法,對(duì)于拼接字段相同的都采用同一策略,列入消息A、B需要通過(guò)醫(yī)生id拼接消息,消息C、D需要通過(guò)用戶id拼接消息,那么A、B就采用同一策略,C、D采用另一策略。
? 流程圖大致如下: ? 各個(gè)業(yè)務(wù)系統(tǒng)?根據(jù)策略構(gòu)造自己的消息體,然后通過(guò)kafka發(fā)送個(gè)底層服務(wù),進(jìn)行消息統(tǒng)一推送。 ? ?
二、策略模式
?
策略模式(Strategy?Pattern)指的是對(duì)象具備某個(gè)行為,但是在不同的場(chǎng)景中,該行為有不同的實(shí)現(xiàn)算法。比如一個(gè)人的交稅比率與他的工資有關(guān),不同的工資水平對(duì)應(yīng)不同的稅率。
策略模式?使用的就是面向?qū)ο蟮睦^承和多態(tài)機(jī)制,從而實(shí)現(xiàn)同一行為在不同場(chǎng)景下具備不同實(shí)現(xiàn)。 策略模式本質(zhì):分離算法,選擇實(shí)現(xiàn) ?
主要解決在有多重算法相似的情況下,使用if...else?或者switch...case所帶來(lái)的的復(fù)雜性和臃腫性。 ? ? 代碼示例: 1 class
Client { 2 public static void main(String[] args) { 3 ICalculator calculator =
new Add(); 4 Context context = new Context(calculator); 5 int result =
context.calc(1,2); 6 System.out.println(result); 7 } 8 9 10 interface
ICalculator {11 int calc(int a, int b); 12 } 13 14 15 static class Add
implements ICalculator { 16 @Override 17 public int calc(int a, int b) { 18
return a + b; 19 } 20 } 21 22 23 static class Sub implements ICalculator { 24
@Override25 public int calc(int a, int b) { 26 return a - b; 27 } 28 } 29 30
31 static class Multi implements ICalculator { 32 @Override 33 public int calc(
int a, int b) { 34 return a * b; 35 } 36 } 37 38 39 static class Divide
implements ICalculator { 40 @Override 41 public int calc(int a, int b) { 42
return a / b; 43 } 44 } 45 46 47 static class Context { 48 private
ICalculator mCalculator;49 50 51 public Context(ICalculator calculator) { 52
this.mCalculator = calculator; 53 } 54 55 56 public int calc(int a, int b) { 57
return this.mCalculator.calc(a, b); 58 } 59 }} ?
三、工作中實(shí)際代碼演示
? 為了代碼簡(jiǎn)潔和易懂,這里用的都是核心代碼片段,主要看策略使用的方式以及思想即可。 ?
1、消息枚舉類,這里因?yàn)橄⒊霭l(fā)節(jié)點(diǎn)眾多,所以每一個(gè)節(jié)點(diǎn)都會(huì)對(duì)應(yīng)一個(gè)枚舉類,枚舉中包含短信、push、微信、私信等內(nèi)容。
1 @Getter 2 public enum MsgCollectEnum { 3 4 /** 5 * 枚舉入口:用戶首次提問(wèn) 給醫(yī)生
文案內(nèi)容(醫(yī)生id拼連接) 6 */ 7 FIRST_QUESTION_CONTENT(2101, 1,
MsgSmsEnum.SMS_FIRST_QUESTION_CONTENT, MsgPushEnum.PUSH_FIRST_QUESTION_CONTENT,
MsgWechatEnum.WECHAT_FIRST_QUESTION_CONTENT); 8 9 10 /** 11 * 短信文案:用戶首次提問(wèn) 給醫(yī)生
文案內(nèi)容12 */ 13
SMS_FIRST_QUESTION_CONTENT(STTurnLinkEnum.DOCTOR_QUESTION_SETTING_PAGE.getStoapp(),
"您好,有一位用戶向您發(fā)起咨詢,請(qǐng)確認(rèn)接單,趕快進(jìn)入APP查看吧!{0}"); 14 15 16 /** 17 * Push文案:用戶首次提問(wèn) 給醫(yī)生
文案內(nèi)容18 */ 19
PUSH_FIRST_QUESTION_CONTENT(STTurnLinkEnum.DOCTOR_QUESTION_SETTING_PAGE.getStoapp(),
STPushAudioEnum.PAY_SUCCESS.getType(), "您好, 有一位用戶向您發(fā)起了咨詢服務(wù)"); 20 21 22 ......
23 }
?
?
2,消息節(jié)點(diǎn)觸發(fā)代碼
這里是構(gòu)造上下文MsgContext,主要策略分發(fā)的邏輯在最后一行,這里也會(huì)作為重點(diǎn)來(lái)講解 ? 1 MsgContext msgContext = new
MsgContext();2 msgContext.setDoctorId(questionDO.getDoctorId()); 3
msgContext.setReceiveUid(questionDO.getDrUid());4
msgContext.setMsgType(MsgCollectEnum.FIRST_QUESTION_CONTENT.getType());5 this
.stContextStrategyFactory.doStrategy(String.valueOf(msgContext.getMsgType()),
QuestionMsgStrategy.class).handleSeniority(msgContext);
?
3,策略分發(fā)
首先,通過(guò)QuestionMsgStrategy.class?找到對(duì)應(yīng)所有的beanMap,然后通過(guò)自定義注解找到所有對(duì)應(yīng)策略類,最后通過(guò)msgType找到指定的實(shí)現(xiàn)類。接著我們看下策略實(shí)現(xiàn)類
? 1 @Slf4j 2 public class STContextStrategyFactory { 3 public <O extends
STIContext, Textends STIContextStrategy<O>> STIContextStrategy<O>
doStrategy(String type, Class<T> clazz) { 4 Map<String, T> beanMap =
STSpringBeanUtils.getBeanMap(clazz); 5 if (MapUtils.isEmpty(beanMap)) { 6
log.error("獲取class:{} 為空", clazz.getName()); 7 } 8 try { 9 for
(Map.Entry<String, T> entry : beanMap.entrySet()) { 10 Object real =
STAopTargetUtils.getTarget(entry.getValue());11 STStrategyAnnotation annotation
= real.getClass().getAnnotation(STStrategyAnnotation.class); 12 List<String>
keySet = Splitter.on("-"
).omitEmptyStrings().trimResults().splitToList(annotation.type());13 if
(keySet.contains(type)) {14 return entry.getValue(); 15 } 16 } 17 } catch
(Exception e) {18 log.error("獲取目標(biāo)代理對(duì)象失敗:{}", e); 19 } 20 log.error("strategy
type = {} handle is null", type); 21 return null; 22 } 23 } ? ?
4,策略實(shí)現(xiàn)類
通過(guò)自定義注解,然后解析msgType值找到指定策略類,通過(guò)不同的策略類構(gòu)造的msg?發(fā)送給kafka。 ? 1 @Component 2
@STStrategyAnnotation(type = "2101-2104-2113-2016", description = "發(fā)給醫(yī)生,無(wú)其他附屬信息"
) 3 public class QuestionMsgSimpleToDoctorStrategyImpl extends
AbstractQuestionSendMsgStrategy { 4 5 6 @Autowired 7 private
RemoteMsgService remoteMsgService; 8 @Autowired 9 private
QuestionDetailService questionDetailService;10 11 12 @Override 13 public
StarSmsIn buildSmsIn(MsgContext context) {14 // do something 15 } 16 17 18
@Override19 public StarPushIn buildPushIn(MsgContext context) { 20 // do
something 21 } 22 23 24 ...... 25 26 27 } 28 29 30 @Slf4j 31 public abstract
class AbstractQuestionSendMsgStrategy implements QuestionMsgStrategy { 32 /** 33
* 構(gòu)建短信消息34 * 35 * @param context 36 * @return 37 */ 38 public abstract
StarSmsIn buildSmsIn(MsgContext context);39 40 41 /** 42 * 構(gòu)建push消息 43 * 44 *
@param context 45 * @return 46 */ 47 public abstract StarPushIn
buildPushIn(MsgContext context);48 49 50 /** 51 * 構(gòu)建微信公眾號(hào) 52 * 53 * @param
context54 * @return 55 */ 56 public abstract StarWeChatIn
buildWeChatIn(MsgContext context);57 58 59 @Override 60 public STResultInfo
handleSeniority(MsgContext msgContext) {61 // buildMsg and send kafka 62 } 63 }
? ?
四,策略模式缺點(diǎn)
整個(gè)消息系統(tǒng)的設(shè)計(jì)起初是基于此策略模式來(lái)實(shí)現(xiàn)的,但是在后續(xù)迭代開發(fā)中會(huì)發(fā)現(xiàn)越來(lái)越不好維護(hù),主要缺點(diǎn)如下:
a、接入消息推送的研發(fā)同學(xué)需要了解每個(gè)策略類,對(duì)于相同的策略進(jìn)行復(fù)用 b、節(jié)點(diǎn)越來(lái)越多,策略類也越來(lái)越多,系統(tǒng)不易維護(hù)
c、觸發(fā)節(jié)點(diǎn)枚舉類散落在各個(gè)業(yè)務(wù)系統(tǒng)中,經(jīng)常會(huì)有相同的節(jié)點(diǎn)而不同的msgType ?
針對(duì)于上述的缺點(diǎn),又重構(gòu)了一把消息系統(tǒng),此次是完全采用節(jié)點(diǎn)配置化方案,提供一個(gè)可視化頁(yè)面進(jìn)行配置,將要構(gòu)造的消息體通過(guò)配置寫入到數(shù)據(jù)庫(kù)中,代碼中通過(guò)不同的占位符進(jìn)行數(shù)據(jù)動(dòng)態(tài)替換。
這里就不再展示新版系統(tǒng)的代碼了,重構(gòu)后?接入方只需要構(gòu)造msgContext即可,再也不需要自己手動(dòng)去寫不同的策略類了。
熱門工具 換一換
