平時我們寫代碼呢,多數情況都是流水線式寫代碼,基本就可以實現業務邏輯了。如何在寫代碼中找到樂趣呢,我覺得,最好的方式就是:使用設計模式優化自己的業務代碼。今天跟大家聊聊日常工作中,我都使用過哪些設計模式。
1.策略模式
1.1 業務場景
假設有這樣的業務場景,大數據系統把文件推送過來,根據不同類型采取不同的解析方式。多數的小伙伴就會寫出以下的代碼:
if(type=="A"){
//按照A格式解析
}else if(type=="B"){
//按B格式解析
}else{
//按照默認格式解析
}
這個代碼可能會存在哪些問題呢?
- 如果分支變多,這里的代碼就會變得臃腫,難以維護,可讀性低。
- 如果你需要接入一種新的解析類型,那只能在原有代碼上修改。
說得專業一點的話,就是以上代碼,違背了面向對象編程的開閉原則以及單一原則。
- 開閉原則(對於擴展是開放的,但是對於修改是封閉的):增加或者刪除某個邏輯,都需要修改到原來代碼
- 單一原則(規定一個類應該只有一個發生變化的原因):修改任何類型的分支邏輯代碼,都需要改動當前類的代碼。
如果你的代碼就是醬紫:有多個if...else
等條件分支,並且每個條件分支,可以封裝起來替換的,我們就可以使用策略模式來優化。
1.2 策略模式定義
策略模式定義了算法族,分別封裝起來,讓它們之間可以相互替換,此模式讓算法的變化獨立於使用算法的的客戶。這個策略模式的定義是不是有點抽象呢?那我們來看點通俗易懂的比喻:
假設你跟不同性格類型的小姐姐約會,要用不同的策略,有的請電影比較好,有的則去吃小吃效果不錯,有的去逛街買買買最合適。當然,目的都是為了得到小姐姐的芳心,請看電影、吃小吃、逛街就是不同的策略。
策略模式針對一組算法,將每一個算法封裝到具有共同接口的獨立的類中,從而使得它們可以相互替換。
1.3 策略模式使用
策略模式怎么使用呢?醬紫實現的:
- 一個接口或者抽象類,里面兩個方法(一個方法匹配類型,一個可替換的邏輯實現方法)
- 不同策略的差異化實現(就是說,不同策略的實現類)
- 使用策略模式
1.3.1 一個接口,兩個方法
public interface IFileStrategy {
//屬於哪種文件解析類型
FileTypeResolveEnum gainFileType();
//封裝的公用算法(具體的解析方法)
void resolve(Object objectparam);
}
1.3.2 不同策略的差異化實現
A 類型策略具體實現
@Component
public class AFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() {
return FileTypeResolveEnum.File_A_RESOLVE;
}
@Override
public void resolve(Object objectparam) {
logger.info("A 類型解析文件,參數:{}",objectparam);
//A類型解析具體邏輯
}
}
B 類型策略具體實現
@Component
public class BFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() {
return FileTypeResolveEnum.File_B_RESOLVE;
}
@Override
public void resolve(Object objectparam) {
logger.info("B 類型解析文件,參數:{}",objectparam);
//B類型解析具體邏輯
}
}
默認類型策略具體實現
@Component
public class DefaultFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() {
return FileTypeResolveEnum.File_DEFAULT_RESOLVE;
}
@Override
public void resolve(Object objectparam) {
logger.info("默認類型解析文件,參數:{}",objectparam);
//默認類型解析具體邏輯
}
}
1.3.3 使用策略模式
如何使用呢?我們借助spring
的生命周期,使用ApplicationContextAware
接口,把對用的策略,初始化到map
里面。然后對外提供resolveFile
方法即可。
/**
* @author 公眾號:撿田螺的小男孩
*/
@Component
public class StrategyUseService implements ApplicationContextAware{
private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();
public void resolveFile(FileTypeResolveEnum fileTypeResolveEnum, Object objectParam) {
IFileStrategy iFileStrategy = iFileStrategyMap.get(fileTypeResolveEnum);
if (iFileStrategy != null) {
iFileStrategy.resolve(objectParam);
}
}
//把不同策略放到map
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IFileStrategy> tmepMap = applicationContext.getBeansOfType(IFileStrategy.class);
tmepMap.values().forEach(strategyService -> iFileStrategyMap.put(strategyService.gainFileType(), strategyService));
}
}
2. 責任鏈模式
2.1 業務場景
我們來看一個常見的業務場景,下訂單。下訂單接口,基本的邏輯,一般有參數非空校驗、安全校驗、黑名單校驗、規則攔截等等。很多伙伴會使用異常來實現:
public class Order {
public void checkNullParam(Object param){
//參數非空校驗
throw new RuntimeException();
}
public void checkSecurity(){
//安全校驗
throw new RuntimeException();
}
public void checkBackList(){
//黑名單校驗
throw new RuntimeException();
}
public void checkRule(){
//規則攔截
throw new RuntimeException();
}
public static void main(String[] args) {
Order order= new Order();
try{
order.checkNullParam();
order.checkSecurity ();
order.checkBackList();
order2.checkRule();
System.out.println("order success");
}catch (RuntimeException e){
System.out.println("order fail");
}
}
}
這段代碼使用了異常來做邏輯條件判斷,如果后續邏輯越來越復雜的話,會出現一些問題:如異常只能返回異常信息,不能返回更多的字段,這時候需要自定義異常類。
並且,阿里開發手冊規定:禁止用異常做邏輯判斷。
【強制】 異常不要用來做流程控制,條件控制。說明:異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
如何優化這段代碼呢?可以考慮責任鏈模式
2.2 責任鏈模式定義
當你想要讓一個以上的對象有機會能夠處理某個請求的時候,就使用責任鏈模式。
責任鏈模式為請求創建了一個接收者對象的鏈。執行鏈上有多個對象節點,每個對象節點都有機會(條件匹配)處理請求事務,如果某個對象節點處理完了,就可以根據實際業務需求傳遞給下一個節點繼續處理或者返回處理完畢。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。
責任鏈模式實際上是一種處理請求的模式,它讓多個處理器(對象節點)都有機會處理該請求,直到其中某個處理成功為止。責任鏈模式把多個處理器串成鏈,然后讓請求在鏈上傳遞:
責任鏈模式
打個比喻:
假設你晚上去上選修課,為了可以走點走,坐到了最后一排。來到教室,發現前面坐了好幾個漂亮的小姐姐,於是你找張紙條,寫上:“你好, 可以做我的女朋友嗎?如果不願意請向前傳”。紙條就一個接一個的傳上去了,后來傳到第一排的那個妹子手上,她把紙條交給老師,聽說老師40多歲未婚...
2.3 責任鏈模式使用
責任鏈模式怎么使用呢?
- 一個接口或者抽象類
- 每個對象差異化處理
- 對象鏈(數組)初始化(連起來)
2.3.1 一個接口或者抽象類
這個接口或者抽象類,需要:
- 有一個指向責任下一個對象的屬性
- 一個設置下一個對象的set方法
- 給子類對象差異化實現的方法(如以下代碼的doFilter方法)
/**
* 關注公眾號:撿田螺的小男孩
*/
public abstract class AbstractHandler {
//責任鏈中的下一個對象
private AbstractHandler nextHandler;
/**
* 責任鏈的下一個對象
*/
public void setNextHandler(AbstractHandler nextHandler){
this.nextHandler = nextHandler;
}
/**
* 具體參數攔截邏輯,給子類去實現
*/
public void filter(Request request, Response response) {
doFilter(request, response);
if (getNextHandler() != null) {
getNextHandler().filter(request, response);
}
}
public AbstractHandler getNextHandler() {
return nextHandler;
}
abstract void doFilter(Request filterRequest, Response response);
}
2.3.2 每個對象差異化處理
責任鏈上,每個對象的差異化處理,如本小節的業務場景,就有參數校驗對象、安全校驗對象、黑名單校驗對象、規則攔截對象
/**
* 參數校驗對象
**/
@Component
@Order(1) //順序排第1,最先校驗
public class CheckParamFilterObject extends AbstractHandler {
@Override
public void doFilter(Request request, Response response) {
System.out.println("非空參數檢查");
}
}
/**
* 安全校驗對象
*/
@Component
@Order(2) //校驗順序排第2
public class CheckSecurityFilterObject extends AbstractHandler {
@Override
public void doFilter(Request request, Response response) {
//invoke Security check
System.out.println("安全調用校驗");
}
}
/**
* 黑名單校驗對象
*/
@Component
@Order(3) //校驗順序排第3
public class CheckBlackFilterObject extends AbstractHandler {
@Override
public void doFilter(Request request, Response response) {
//invoke black list check
System.out.println("校驗黑名單");
}
}
/**
* 規則攔截對象
*/
@Component
@Order(4) //校驗順序排第4
public class CheckRuleFilterObject extends AbstractHandler {
@Override
public void doFilter(Request request, Response response) {
//check rule
System.out.println("check rule");
}
}
2.3.3 對象鏈連起來(初始化)&& 使用
@Component("ChainPatternDemo")
public class ChainPatternDemo {
//自動注入各個責任鏈的對象
@Autowired
private List<AbstractHandler> abstractHandleList;
private AbstractHandler abstractHandler;
//spring注入后自動執行,責任鏈的對象連接起來
@PostConstruct
public void initializeChainFilter(){
for(int i = 0;i<abstractHandleList.size();i++){
if(i == 0){
abstractHandler = abstractHandleList.get(0);
}else{
AbstractHandler currentHander = abstractHandleList.get(i - 1);
AbstractHandler nextHander = abstractHandleList.get(i);
currentHander.setNextHandler(nextHander);
}
}
}
//直接調用這個方法使用
public Response exec(Request request, Response response) {
abstractHandler.filter(request, response);
return response;
}
public AbstractHandler getAbstractHandler() {
return abstractHandler;
}
public void setAbstractHandler(AbstractHandler abstractHandler) {
this.abstractHandler = abstractHandler;
}
}
運行結果如下:
非空參數檢查
安全調用校驗
校驗黑名單
check rule
3. 模板方法模式
3.1 業務場景
假設我們有這么一個業務場景:內部系統不同商戶,調用我們系統接口,去跟外部第三方系統交互(http方式)。走類似這么一個流程,如下:
一個請求都會經歷這幾個流程:
- 查詢商戶信息
- 對請求報文加簽
- 發送http請求出去
- 對返回的報文驗簽
這里,有的商戶可能是走代理出去的,有的是走直連。假設當前有A,B商戶接入,不少伙伴可能這么實現,偽代碼如下:
// 商戶A處理句柄
CompanyAHandler implements RequestHandler {
Resp hander(req){
//查詢商戶信息
queryMerchantInfo();
//加簽
signature();
//http請求(A商戶假設走的是代理)
httpRequestbyProxy()
//驗簽
verify();
}
}
// 商戶B處理句柄
CompanyBHandler implements RequestHandler {
Resp hander(Rreq){
//查詢商戶信息
queryMerchantInfo();
//加簽
signature();
// http請求(B商戶不走代理,直連)
httpRequestbyDirect();
// 驗簽
verify();
}
}
假設新加一個C商戶接入,你需要再實現一套這樣的代碼。顯然,這樣代碼就重復了,一些通用的方法,卻在每一個子類都重新寫了這一方法。
如何優化呢?可以使用模板方法模式。
3.2 模板方法模式定義
定義一個操作中的算法的骨架流程,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。它的核心思想就是:定義一個操作的一系列步驟,對於某些暫時確定不下來的步驟,就留給子類去實現,這樣不同的子類就可以定義出不同的步驟。
打個通俗的比喻:
模式舉例:追女朋友要先“牽手”,再“擁抱”,再“接吻”, 再“拍拍..額..手”。至於具體你用左手還是右手牽,無所謂,但是整個過程,定了一個流程模板,按照模板來就行。
3.3 模板方法使用
- 一個抽象類,定義骨架流程(抽象方法放一起)
- 確定的共同方法步驟,放到抽象類(去除抽象方法標記)
- 不確定的步驟,給子類去差異化實現
我們繼續那以上的舉例的業務流程例子,來一起用 模板方法優化一下哈:
3.3.1 一個抽象類,定義骨架流程
因為一個個請求經過的流程為一下步驟:
- 查詢商戶信息
- 對請求報文加簽
- 發送http請求出去
- 對返回的報文驗簽
所以我們就可以定義一個抽象類,包含請求流程的幾個方法,方法首先都定義為抽象方法哈:
/**
* 抽象類定義骨架流程(查詢商戶信息,加簽,http請求,驗簽)
*/
abstract class AbstractMerchantService {
//查詢商戶信息
abstract queryMerchantInfo();
//加簽
abstract signature();
//http 請求
abstract httpRequest();
// 驗簽
abstract verifySinature();
}
3.3.2 確定的共同方法步驟,放到抽象類
abstract class AbstractMerchantService {
//模板方法流程
Resp handlerTempPlate(req){
//查詢商戶信息
queryMerchantInfo();
//加簽
signature();
//http 請求
httpRequest();
// 驗簽
verifySinature();
}
// Http是否走代理(提供給子類實現)
abstract boolean isRequestByProxy();
}
3.3.3 不確定的步驟,給子類去差異化實現
因為是否走代理流程是不確定的,所以給子類去實現。
商戶A的請求實現:
CompanyAServiceImpl extends AbstractMerchantService{
Resp hander(req){
return handlerTempPlate(req);
}
//走http代理的
boolean isRequestByProxy(){
return true;
}
商戶B的請求實現:
CompanyBServiceImpl extends AbstractMerchantService{
Resp hander(req){
return handlerTempPlate(req);
}
//公司B是不走代理的
boolean isRequestByProxy(){
return false;
}
4. 觀察者模式
4.1 業務場景
登陸注冊應該是最常見的業務場景了。就拿注冊來說事,我們經常會遇到類似的場景,就是用戶注冊成功后,我們給用戶發一條消息,又或者發個郵件等等,因此經常有如下的代碼:
void register(User user){
insertRegisterUser(user);
sendIMMessage();
sendEmail();
}
這塊代碼會有什么問題呢?如果產品又加需求:現在注冊成功的用戶,再給用戶發一條短信通知。於是你又得改register方法的代碼了。。。這是不是違反了開閉原則啦。
void register(User user){
insertRegisterUser(user);
sendIMMessage();
sendMobileMessage();
sendEmail();
}
並且,如果調發短信的接口失敗了,是不是又影響到用戶注冊了?!這時候,是不是得加個異步方法給通知消息才好。。。
實際上,我們可以使用觀察者模式優化。
4.2 觀察者模式定義
觀察者模式定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被完成業務的更新。
觀察者模式屬於行為模式,一個對象(被觀察者)的狀態發生改變,所有的依賴對象(觀察者對象)都將得到通知,進行廣播通知。它的主要成員就是觀察者和被觀察者。
- 被觀察者(Observerable):目標對象,狀態發生變化時,將通知所有的觀察者。
- 觀察者(observer):接受被觀察者的狀態變化通知,執行預先定義的業務。
使用場景: 完成某件事情后,異步通知場景。如,登陸成功,發個IM消息等等。
4.3 觀察者模式使用
觀察者模式實現的話,還是比較簡單的。
- 一個被觀察者的類Observerable ;
- 多個觀察者Observer ;
- 觀察者的差異化實現
- 經典觀察者模式封裝:EventBus實戰
4.3.1 一個被觀察者的類Observerable 和 多個觀察者Observer
public class Observerable {
private List<Observer> observers
= new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
notifyAllObservers();
}
//添加觀察者
public void addServer(Observer observer){
observers.add(observer);
}
//移除觀察者
public void removeServer(Observer observer){
observers.remove(observer);
}
//通知
public void notifyAllObservers(int state){
if(state!=1){
System.out.println(“不是通知的狀態”);
return ;
}
for (Observer observer : observers) {
observer.doEvent();
}
}
}
4.3.2 觀察者的差異化實現
//觀察者
interface Observer {
void doEvent();
}
//Im消息
IMMessageObserver implements Observer{
void doEvent(){
System.out.println("發送IM消息");
}
}
//手機短信
MobileNoObserver implements Observer{
void doEvent(){
System.out.println("發送短信消息");
}
}
//EmailNo
EmailObserver implements Observer{
void doEvent(){
System.out.println("發送email消息");
}
}
4.3.3 EventBus實戰
自己搞一套觀察者模式的代碼,還是有點小麻煩。實際上,Guava EventBus
就封裝好了,它 提供一套基於注解的事件總線,api可以靈活的使用,爽歪歪。
我們來看下EventBus
的實戰代碼哈,首先可以聲明一個EventBusCenter類,它類似於以上被觀察者那種角色Observerable
。
public class EventBusCenter {
private static EventBus eventBus = new EventBus();
private EventBusCenter() {
}
public static EventBus getInstance() {
return eventBus;
}
//添加觀察者
public static void register(Object obj) {
eventBus.register(obj);
}
//移除觀察者
public static void unregister(Object obj) {
eventBus.unregister(obj);
}
//把消息推給觀察者
public static void post(Object obj) {
eventBus.post(obj);
}
}
然后再聲明觀察者EventListener
public class EventListener {
@Subscribe //加了訂閱,這里標記這個方法是事件處理方法
public void handle(NotifyEvent notifyEvent) {
System.out.println("發送IM消息" + notifyEvent.getImNo());
System.out.println("發送短信消息" + notifyEvent.getMobileNo());
System.out.println("發送Email消息" + notifyEvent.getEmailNo());
}
}
//通知事件類
public class NotifyEvent {
private String mobileNo;
private String emailNo;
private String imNo;
public NotifyEvent(String mobileNo, String emailNo, String imNo) {
this.mobileNo = mobileNo;
this.emailNo = emailNo;
this.imNo = imNo;
}
}
使用demo測試:
public class EventBusDemoTest {
public static void main(String[] args) {
EventListener eventListener = new EventListener();
EventBusCenter.register(eventListener);
EventBusCenter.post(new NotifyEvent("13372817283", "123@qq.com", "666"));
}
}
運行結果:
發送IM消息666
發送短信消息13372817283
發送Email消息123@qq.com
5. 工廠模式
5.1 業務場景
工廠模式一般配合策略模式一起使用。用來去優化大量的if...else...
或switch...case...
條件語句。
我們就取第一小節中策略模式那個例子吧。根據不同的文件解析類型,創建不同的解析對象
IFileStrategy getFileStrategy(FileTypeResolveEnum fileType){
IFileStrategy fileStrategy ;
if(fileType=FileTypeResolveEnum.File_A_RESOLVE){
fileStrategy = new AFileResolve();
}else if(fileType=FileTypeResolveEnum.File_A_RESOLV){
fileStrategy = new BFileResolve();
}else{
fileStrategy = new DefaultFileResolve();
}
return fileStrategy;
}
其實這就是工廠模式,定義一個創建對象的接口,讓其子類自己決定實例化哪一個工廠類,工廠模式使其創建過程延遲到子類進行。
策略模式的例子,沒有使用上一段代碼,而是借助spring的特性,搞了一個工廠模式,哈哈,小伙伴們可以回去那個例子細品一下,我把代碼再搬下來,小伙伴們再品一下吧:
/**
* @author 公眾號:撿田螺的小男孩
*/
@Component
public class StrategyUseService implements ApplicationContextAware{
private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();
//把所有的文件類型解析的對象,放到map,需要使用時,信手拈來即可。這就是工廠模式的一種體現啦
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IFileStrategy> tmepMap = applicationContext.getBeansOfType(IFileStrategy.class);
tmepMap.values().forEach(strategyService -> iFileStrategyMap.put(strategyService.gainFileType(), strategyService));
}
}
5.2 使用工廠模式
定義工廠模式也是比較簡單的:
- 一個工廠接口,提供一個創建不同對象的方法。
- 其子類實現工廠接口,構造不同對象
- 使用工廠模式
5.3.1 一個工廠接口
interface IFileResolveFactory{
void resolve();
}
5.3.2 不同子類實現工廠接口
class AFileResolve implements IFileResolveFactory{
void resolve(){
System.out.println("文件A類型解析");
}
}
class BFileResolve implements IFileResolveFactory{
void resolve(){
System.out.println("文件B類型解析");
}
}
class DefaultFileResolve implements IFileResolveFactory{
void resolve(){
System.out.println("默認文件類型解析");
}
}
5.3.3 使用工廠模式
//構造不同的工廠對象
IFileResolveFactory fileResolveFactory;
if(fileType=“A”){
fileResolveFactory = new AFileResolve();
}else if(fileType=“B”){
fileResolveFactory = new BFileResolve();
}else{
fileResolveFactory = new DefaultFileResolve();
}
fileResolveFactory.resolve();
一般情況下,對於工廠模式,你不會看到以上的代碼。工廠模式會跟配合其他設計模式如策略模式一起出現的。
6. 單例模式
6.1 業務場景
單例模式,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。I/O與數據庫的連接,一般就用單例模式實現de的。Windows里面的Task Manager(任務管理器)也是很典型的單例模式。
來看一個單例模式的例子
/**
* 公眾號:撿田螺的小男孩
*/
public class LanHanSingleton {
private static LanHanSingleton instance;
private LanHanSingleton(){
}
public static LanHanSingleton getInstance(){
if (instance == null) {
instance = new LanHanSingleton();
}
return instance;
}
}
以上的例子,就是懶漢式的單例實現。實例在需要用到的時候,才去創建,就比較懶。如果有則返回,沒有則新建,需要加下 synchronized
關鍵字,要不然可能存在線性安全問題。
6.2 單例模式的經典寫法
其實單例模式還有有好幾種實現方式,如餓漢模式,雙重校驗鎖,靜態內部類,枚舉等實現方式。
6.2.1 餓漢模式
public class EHanSingleton {
private static EHanSingleton instance = new EHanSingleton();
private EHanSingleton(){
}
public static EHanSingleton getInstance() {
return instance;
}
}
餓漢模式,它比較飢餓、比較勤奮,實例在初始化的時候就已經建好了,不管你后面有沒有用到,都先新建好實例再說。這個就沒有線程安全的問題,但是呢,浪費內存空間呀。
6.2.2 雙重校驗鎖
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instance;
private DoubleCheckSingleton() { }
public static DoubleCheckSingleton getInstance(){
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
雙重校驗鎖實現的單例模式,綜合了懶漢式和餓漢式兩者的優缺點。以上代碼例子中,在synchronized關鍵字內外都加了一層 if
條件判斷,這樣既保證了線程安全,又比直接上鎖提高了執行效率,還節省了內存空間。
6.2.3 靜態內部類
public class InnerClassSingleton {
private static class InnerClassSingletonHolder{
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
private InnerClassSingleton(){}
public static final InnerClassSingleton getInstance(){
return InnerClassSingletonHolder.INSTANCE;
}
}
靜態內部類的實現方式,效果有點類似雙重校驗鎖。但這種方式只適用於靜態域場景,雙重校驗鎖方式可在實例域需要延遲初始化時使用。
6.2.4 枚舉
public enum SingletonEnum {
INSTANCE;
public SingletonEnum getInstance(){
return INSTANCE;
}
}
枚舉實現的單例,代碼簡潔清晰。並且它還自動支持序列化機制,絕對防止多次實例化。