青島網(wǎng)站設(shè)計(jì)哪家便宜抖音廣告投放代理商
文章目錄
- 一、定義:觀察者模式
- 二、模擬場(chǎng)景:觀察者模式
- 2.1 觀察者模式
- 2.2 引入依賴
- 2.3 工程結(jié)構(gòu)
- 2.4 模擬搖號(hào)
- 2.4.1 搖號(hào)服務(wù)接口
- 2.4.2 搖號(hào)返回結(jié)果類
- 三、違背方案:觀察者模式
- 3.0 引入依賴
- 3.1 工程結(jié)構(gòu)
- 3.2 添加搖號(hào)接口和實(shí)現(xiàn)
- 3.2.1 搖號(hào)服務(wù)接口
- 3.2.2 搖號(hào)服務(wù)接口實(shí)現(xiàn)類
- 3.3 單元測(cè)試
- 四、改善代碼:觀察者模式
- 4.0 引入依賴
- 4.1 工程結(jié)構(gòu)
- 4.2 觀察者模式結(jié)構(gòu)圖
- 4.3 添加事件監(jiān)聽和管理器
- 4.3.1 定義事件監(jiān)聽接口
- 4.3.2 短信息事件監(jiān)聽接口實(shí)現(xiàn)
- 4.3.3 MQ發(fā)送事件監(jiān)聽接口實(shí)現(xiàn)
- 4.3.4 事件處理器
- 4.4 搖號(hào)抽象類及其實(shí)現(xiàn)
- 4.4.1 業(yè)務(wù)抽象類
- 4.4.2 業(yè)務(wù)接口實(shí)現(xiàn)類
- 4.5 單元測(cè)試
- 五、總結(jié):觀察者模式
一、定義:觀察者模式
- **觀察者模式:**當(dāng)一個(gè)行為發(fā)生時(shí)傳遞信息給另外一個(gè)用戶接收做出相應(yīng)的處理,兩者之間沒有直接的耦合關(guān)聯(lián)。
- 除了生活中的場(chǎng)景外,在我們編程開發(fā)中也會(huì)常用到一些觀察者的模式或者組件。例如:
- 經(jīng)常使用的 MQ 服務(wù):雖然 MQ 服務(wù)是有一個(gè)通知中心并不是每一個(gè)類服務(wù)進(jìn)行通知,但整體上也可以算作是觀察者模式的思路設(shè)計(jì)。
- 類似事件監(jiān)聽總線:讓主線服務(wù)與其他輔線業(yè)務(wù)分離,為了使系統(tǒng)降低耦合和增強(qiáng)擴(kuò)展性,也會(huì)使用觀察者模式進(jìn)行處理。
二、模擬場(chǎng)景:觀察者模式
2.1 觀察者模式
- 模擬每次小客車指標(biāo)搖號(hào)事件通知場(chǎng)景
- 可能大部分人看到這個(gè)案例一定會(huì)想到自己每次搖號(hào)都不中的場(chǎng)景,收到一個(gè)遺憾的短信通知。當(dāng)然目前的搖號(hào)系統(tǒng)不會(huì)給你發(fā)短信,而是由百度或者一些其他插件發(fā)的短信。
- 那么假如這個(gè)類似的搖號(hào)功能由你來開發(fā),并且需要對(duì)外部的用戶做一些事件通知以及需要在主流外再添加一些額外的輔助流程時(shí)該如何處理呢?
- 基本很多人對(duì)于這樣的通知事件類的實(shí)現(xiàn)往往比較粗獷,直接在類里面添加就可以了。
- ①考慮這可能不會(huì)怎么擴(kuò)展,②是壓根就沒考慮過擴(kuò)展。
- 但如果你有仔細(xì)思考過你的核心類功能會(huì)發(fā)現(xiàn),這里面有一些核心主鏈路,還有一部分是輔助功能。
- 比如完成了某個(gè)行為后需要觸發(fā) MQ 給外部,以及做一些消息 PUSH 給用戶等。
- 這些都不算做是核心流程鏈路,是可以通過事件通知的方式進(jìn)行處理。
- 基本很多人對(duì)于這樣的通知事件類的實(shí)現(xiàn)往往比較粗獷,直接在類里面添加就可以了。
2.2 引入依賴
pom.xml
<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.62</version></dependency><!-- LOGGING begin --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.5</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>1.7.5</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.0.9</version><exclusions><exclusion><artifactId>slf4j-api</artifactId><groupId>org.slf4j</groupId></exclusion></exclusions></dependency>
</dependencies>
2.3 工程結(jié)構(gòu)
design-19.0-0
|——src|——main|--java|--com.lino.design|-LotteryResult.java|-MinibusTargetService.java
2.4 模擬搖號(hào)
2.4.1 搖號(hào)服務(wù)接口
MinibusTargetService.java
package com.lino.design;/*** @description: 小客車指標(biāo)調(diào)控服務(wù)*/
public class MinibusTargetService {/*** 模擬搖號(hào)** @param uId 用戶編號(hào)* @return 結(jié)果*/public String lottery(String uId) {return Math.abs(uId.hashCode()) % 2 == 0 ?"恭喜你,編碼".concat(uId).concat("在本次搖號(hào)中簽"): "很遺憾,編碼".concat(uId).concat("在本次搖號(hào)未中簽或搖號(hào)資格已過期");}
}
2.4.2 搖號(hào)返回結(jié)果類
LotteryResult.java
package com.lino.design;import java.util.Date;/*** @description: 搖號(hào)結(jié)果類*/
public class LotteryResult {/*** 用戶ID*/private String uId;/*** 搖號(hào)信息*/private String msg;/*** 業(yè)務(wù)時(shí)間*/private Date dateTime;public LotteryResult(String uId, String msg, Date dateTime) {this.uId = uId;this.msg = msg;this.dateTime = dateTime;}public String getuId() {return uId;}public void setuId(String uId) {this.uId = uId;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Date getDateTime() {return dateTime;}public void setDateTime(Date dateTime) {this.dateTime = dateTime;}
}
三、違背方案:觀察者模式
按照需求需要在原有的搖號(hào)接口中添加 MQ 消息發(fā)送以及短信息通知功能。
如果是最直接的方式那么可以直接在方法中補(bǔ)充功能即可。
3.0 引入依賴
<dependencies><dependency><groupId>com.lino</groupId><artifactId>design-19.0-0</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
3.1 工程結(jié)構(gòu)
design-19.0-1
|——src|——main|--java|--com.lino.design|-LotteryService.java|-LotteryServiceImpl.java|--test|--com.lino.design.test|-ApiTest.java
3.2 添加搖號(hào)接口和實(shí)現(xiàn)
3.2.1 搖號(hào)服務(wù)接口
LotteryService.java
package com.lino.design;/*** @description: 搖號(hào)接口*/
public interface LotteryService {/*** 搖號(hào)** @param uId 用戶編號(hào)* @return 搖號(hào)結(jié)果*/LotteryResult doDraw(String uId);
}
3.2.2 搖號(hào)服務(wù)接口實(shí)現(xiàn)類
LotteryServiceImpl.java
package com.lino.design;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;/*** @description: 搖號(hào)接口實(shí)現(xiàn)類*/
public class LotteryServiceImpl implements LotteryService {private Logger logger = LoggerFactory.getLogger(LotteryServiceImpl.class);private MinibusTargetService minibusTargetService = new MinibusTargetService();@Overridepublic LotteryResult doDraw(String uId) {// 搖號(hào)String lottery = minibusTargetService.lottery(uId);// 發(fā)短信logger.info("給用戶 {} 發(fā)送短信通知(短信):{}", uId, lottery);// 發(fā)MQ信息logger.info("記錄用戶 {} 搖號(hào)結(jié)果(MQ):{}", uId, lottery);return new LotteryResult(uId, lottery, new Date());}
}
- 從上面的方法實(shí)現(xiàn)中可以看到,整體過程包括三部分:搖號(hào)、發(fā)短信、發(fā) MQ 消息,而這部分都是順序調(diào)用的。
- 除了 搖號(hào) 接口調(diào)用外,后面的兩部分都是非核心主鏈路功能,而且會(huì)隨著后續(xù)的業(yè)務(wù)需求發(fā)展而不斷的調(diào)整和擴(kuò)充,在這樣的開發(fā)方式下就非常不利于維護(hù)。
3.3 單元測(cè)試
ApiTest.java
package com.lino.design.test;import com.alibaba.fastjson.JSON;
import com.lino.design.LotteryResult;
import com.lino.design.LotteryService;
import com.lino.design.LotteryServiceImpl;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @description: 單元測(cè)試*/
public class ApiTest {private Logger logger = LoggerFactory.getLogger(ApiTest.class);@Testpublic void test() {LotteryService lotteryService = new LotteryServiceImpl();LotteryResult result = lotteryService.doDraw("2765789109876");logger.info("測(cè)試結(jié)果:{}", JSON.toJSONString(result));}
}
- 測(cè)試過程中提供對(duì)搖號(hào)服務(wù)接口的調(diào)用
測(cè)試結(jié)果
17:06:14.931 [main] INFO com.lino.design.LotteryServiceImpl - 給用戶 2765789109876 發(fā)送短信通知(短信):很遺憾,編碼2765789109876在本次搖號(hào)未中簽或搖號(hào)資格已過期
17:06:14.931 [main] INFO com.lino.design.LotteryServiceImpl - 記錄用戶 2765789109876 搖號(hào)結(jié)果(MQ):很遺憾,編碼2765789109876在本次搖號(hào)未中簽或搖號(hào)資格已過期
17:06:15.122 [main] INFO com.lino.design.test.ApiTest - 測(cè)試結(jié)果:{"dateTime":1675760774946,"msg":"很遺憾,編碼2765789109876在本次搖號(hào)未中簽或搖號(hào)資格已過期","uId":"2765789109876"}
四、改善代碼:觀察者模式
4.0 引入依賴
<dependencies><dependency><groupId>com.lino</groupId><artifactId>design-19.0-0</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
4.1 工程結(jié)構(gòu)
design-19.0-2
|——src|——main|--java|--com.lino.design|--event| |--listener| | |--EventListener.java| | |--MessageEventListener.java| | |--MQEventListener.java| |--EventManager.java|-LotteryService.java|-LotteryServiceImpl.java|--test|--com.lino.design.test|-ApiTest.java
4.2 觀察者模式結(jié)構(gòu)圖
- 從上圖可以分為三大塊:事件監(jiān)聽、事件處理、具體的業(yè)務(wù)流程。
- 另外在業(yè)務(wù)流程中
LotteryService
定義的是抽象類,因?yàn)檫@樣可以通過抽象類將事件功能屏蔽,外部業(yè)務(wù)流程開發(fā)者不需要知道具體的通知操作。
- 另外在業(yè)務(wù)流程中
- 右下角圓圈圖表示的是核心流程與非核心流程的結(jié)構(gòu)。
- 一般在開發(fā)中會(huì)把主線流程開發(fā)完成后,再使用通知的方式處理輔助流程。他們可以是異步的,在 MQ 以及定時(shí)任務(wù)的處理下,保證最終一致性。
4.3 添加事件監(jiān)聽和管理器
4.3.1 定義事件監(jiān)聽接口
EventListener.java
package com.lino.design.event.listener;import com.lino.design.LotteryResult;/*** @description: 事件監(jiān)聽接口*/
public interface EventListener {/*** 監(jiān)聽事件** @param result 搖號(hào)結(jié)果*/void doEvent(LotteryResult result);
}
- 接口定義了基本的事件類,這里如果方法的入?yún)⑿畔㈩愋褪亲兓?#xff0c;可以使用泛型
T
。
4.3.2 短信息事件監(jiān)聽接口實(shí)現(xiàn)
MessageEventListener.java
package com.lino.design.event.listener;import com.lino.design.LotteryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @description: 短消息事件監(jiān)聽實(shí)現(xiàn)*/
public class MessageEventListener implements EventListener {private Logger logger = LoggerFactory.getLogger(MessageEventListener.class);@Overridepublic void doEvent(LotteryResult result) {logger.info("給用戶 {} 發(fā)送短信通知(短信):{}", result.getuId(), result.getMsg());}
}
4.3.3 MQ發(fā)送事件監(jiān)聽接口實(shí)現(xiàn)
MQEventListener.java
package com.lino.design.event.listener;import com.lino.design.LotteryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @description: MQ事件監(jiān)聽實(shí)現(xiàn)*/
public class MQEventListener implements EventListener {private Logger logger = LoggerFactory.getLogger(MQEventListener.class);@Overridepublic void doEvent(LotteryResult result) {logger.info("記錄用戶 {} 搖號(hào)結(jié)果(MQ):{}", result.getuId(), result.getMsg());}
}
4.3.4 事件處理器
EventManager.java
package com.lino.design.event;import com.lino.design.LotteryResult;
import com.lino.design.event.listener.EventListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @description: 事件處理器*/
public class EventManager {Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();public EventManager(Enum<EventType>... operations) {for (Enum<EventType> operation : operations) {this.listeners.put(operation, new ArrayList<>());}}public enum EventType {/*** 事件類型*/MQ, Message}/*** 訂閱** @param eventType 事件類型* @param listener 監(jiān)聽*/public void subscribe(Enum<EventType> eventType, EventListener listener) {List<EventListener> users = listeners.get(eventType);users.add(listener);}/*** 取消訂閱** @param eventType 事件類型* @param listener 監(jiān)聽*/public void unsubscribe(Enum<EventType> eventType, EventListener listener) {List<EventListener> users = listeners.get(eventType);users.remove(listener);}/*** 通知** @param eventType 事件類型* @param result 結(jié)果*/public void notify(Enum<EventType> eventType, LotteryResult result) {List<EventListener> users = listeners.get(eventType);for (EventListener listener : users) {listener.doEvent(result);}}
}
- 整個(gè)處理的實(shí)現(xiàn)上提供了三個(gè)主要方法:訂閱
subscribe
、取消訂閱unsubscribe
、通知notify
。這三個(gè)方法分別用于對(duì)監(jiān)聽事件的添加和使用。 - 另外因?yàn)槭录胁煌念愋?#xff0c;這里使用率枚舉的方式進(jìn)行處理,也方便讓外部在規(guī)定下使用事件,而不至于亂傳信息。
- 枚舉:
EventType.MQ
、EventType.Message
- 枚舉:
4.4 搖號(hào)抽象類及其實(shí)現(xiàn)
4.4.1 業(yè)務(wù)抽象類
LotteryService.java
package com.lino.design;import com.lino.design.event.EventManager;
import com.lino.design.event.listener.MQEventListener;
import com.lino.design.event.listener.MessageEventListener;/*** @description: 搖號(hào)抽象類*/
public abstract class LotteryService {private EventManager eventManager;public LotteryService() {eventManager = new EventManager(EventManager.EventType.MQ, EventManager.EventType.Message);eventManager.subscribe(EventManager.EventType.MQ, new MQEventListener());eventManager.subscribe(EventManager.EventType.Message, new MessageEventListener());}public LotteryResult draw(String uId) {LotteryResult lotteryResult = doDraw(uId);eventManager.notify(EventManager.EventType.MQ, lotteryResult);eventManager.notify(EventManager.EventType.Message, lotteryResult);return lotteryResult;}/*** 執(zhí)行搖號(hào)** @param uId 用戶編號(hào)* @return 結(jié)果*/protected abstract LotteryResult doDraw(String uId);
}
- 使用抽象類的方式定義實(shí)現(xiàn)方法,可以在方法中擴(kuò)展需要的額外調(diào)用。
- 并提供抽象類
abstract LotteryResult doDraw(String uId)
,讓類的繼承者實(shí)現(xiàn)。 - 同時(shí),方法的定義使用的是
protected
,也就是保證將來外部的調(diào)用方不會(huì)調(diào)用到此方法,只有調(diào)用到draw(String uId)
才能完成事件通知。
- 并提供抽象類
- 此種方式到實(shí)現(xiàn)是在抽象類中寫好一個(gè)基本的方法,在方法中完成新增邏輯到同時(shí),再增加抽象類的使用,而這個(gè)抽象的定義會(huì)由繼承者實(shí)現(xiàn)。
- 另外,在構(gòu)造函數(shù)中提供了對(duì)事件的定義:
eventManager.subscribe(EventManager.EventType.MQ, new MQEventListener())
。- 在使用時(shí)也采用枚舉的方式通知使用者,傳了哪些類型
eventManager.EventType.Message
,就執(zhí)行哪些事件通知,按需添加。
4.4.2 業(yè)務(wù)接口實(shí)現(xiàn)類
LotteryServiceImpl.java
package com.lino.design;import java.util.Date;/*** @description: 搖號(hào)服務(wù)實(shí)現(xiàn)* @author: lingjian* @createDate: 2023/2/6 17:02*/
public class LotteryServiceImpl extends LotteryService {private MinibusTargetService minibusTargetService = new MinibusTargetService();@Overrideprotected LotteryResult doDraw(String uId) {// 搖號(hào)String lottery = minibusTargetService.lottery(uId);// 結(jié)果return new LotteryResult(uId, lottery, new Date());}
}
- 業(yè)務(wù)流程的實(shí)現(xiàn),沒有額外的輔助流程,只有核心流程的處理。
4.5 單元測(cè)試
ApiTest.java
package com.lino.design.test;import com.alibaba.fastjson.JSON;
import com.lino.design.LotteryResult;
import com.lino.design.LotteryService;
import com.lino.design.LotteryServiceImpl;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @description: 單元測(cè)試*/
public class ApiTest {private Logger logger = LoggerFactory.getLogger(ApiTest.class);@Testpublic void test_draw() {LotteryService lotteryService = new LotteryServiceImpl();LotteryResult result = lotteryService.draw("1000000101010019");logger.info("測(cè)試結(jié)果:{}", JSON.toJSONString(result));}
}
測(cè)試結(jié)果
19:45:45.314 [main] INFO c.l.d.event.listener.MQEventListener - 記錄用戶 1000000101010019 搖號(hào)結(jié)果(MQ):恭喜你,編碼1000000101010019在本次搖號(hào)中簽
19:45:45.319 [main] INFO c.l.d.e.l.MessageEventListener - 給用戶 1000000101010019 發(fā)送短信通知(短信):恭喜你,編碼1000000101010019在本次搖號(hào)中簽
19:45:45.398 [main] INFO com.lino.design.test.ApiTest - 測(cè)試結(jié)果:{"dateTime":1675770345311,"msg":"恭喜你,編碼1000000101010019在本次搖號(hào)中簽","uId":"1000000101010019"}
五、總結(jié):觀察者模式
- 從基本的過程式開發(fā),到使用觀察者模式面向?qū)ο箝_發(fā),可以看到使用設(shè)計(jì)模式改造后,拆分出來核心流程與輔助流程的代碼。
- 代碼中的核心流程一般不會(huì)發(fā)生經(jīng)常變化,輔助流程會(huì)隨著業(yè)務(wù)的變化而變化,包括營銷、裂變和促活等。
- 因此使用設(shè)計(jì)模式編碼就顯得非常有必要。
- 觀察者模式從結(jié)構(gòu)上滿足開閉原則,當(dāng)需要新增其他的監(jiān)聽事件或修改監(jiān)聽邏輯時(shí),不需要改動(dòng)事件處理類。
- 觀察者模式可能不能控制調(diào)用順序以及需要做一些事件結(jié)果的返回操作,所以在使用的過程時(shí)需要考慮場(chǎng)景的適用性。