前言
本系列文章參考《設(shè)計模式之禪》、菜鳥教程網(wǎng)以及網(wǎng)上的一些文章進(jìn)行歸納總結(jié),并結(jié)合自身開發(fā)應(yīng)用。設(shè)計模式的命名以《設(shè)計模式之禪》為準(zhǔn)衔蹲。
設(shè)計模式僅是一些開發(fā)者在日常開發(fā)中的編碼技巧的匯總并非固定不變离赫,可根據(jù)項目業(yè)務(wù)實際情況進(jìn)行擴(kuò)展和應(yīng)用芭逝,切不可被這個束縛。更不要為了使用而使用渊胸,設(shè)計模式是一把雙刃劍旬盯,過度的設(shè)計會導(dǎo)致代碼的可讀性下降,代碼的體積增加翎猛。
系列文章不會詳細(xì)介紹設(shè)計模式的《七大原則》胖翰,也不會對設(shè)計模式進(jìn)行分類。這樣只會增加學(xué)習(xí)和記憶的成本切厘,也會導(dǎo)致使用時的思想固化萨咳,總在想這么設(shè)計是否符合xx原則,是否是xx設(shè)計模式疫稿,xx模式是什么類型等等培他,不是本系列文章的所希望看到的,目標(biāo)只有一個遗座,結(jié)合日常開發(fā)分享自身的應(yīng)用舀凛,以提供一種代碼優(yōu)化的思路。
學(xué)習(xí)然后忘記员萍,也許是一種最好的方式腾降。
就像俗話說的那樣:天下本沒有路,走的人多了碎绎,就變成了路螃壤。在我看來,設(shè)計模式也一樣筋帖,它并非是一種定律奸晴,而是前輩們總結(jié)下來的經(jīng)驗,我們學(xué)習(xí)并結(jié)合實際加以利用日麸,而不是生搬硬套寄啼。
定義
官腔:定義一組算法,將每個算法都封裝起來代箭,并且使它們之間可以互換墩划。
人話:有一個公共的邏輯接口或抽象類,且有一個控制類對實現(xiàn)了公共邏輯接口的類進(jìn)行控制嗡综,在引用時知道該調(diào)用哪一個接口的策略類乙帮。
應(yīng)用場景
樣例1
為了方便沒有了解過設(shè)計模式的小白快速了解,這里放一個網(wǎng)上到處都是的計算器模型极景。
在一個簡單的計算器中我們需要關(guān)注計算器的運算符即可察净。我們假定這個計算器非常非常簡單驾茴,僅支持加減乘除。
先定義一個運算符接口氢卡。
public interface IOperator {
/**
* 計算邏輯
* @return 結(jié)果
*/
int doOperator(int num1, int num2);
}
代碼比較簡單锈至,只做整形的計算。
加法實現(xiàn):
public class AddOperator implements IOperator{
@Override
public int doOperator(int num1, int num2) {
return num1 + num2;
}
}
乘法實現(xiàn):
public class MultiplyOperator implements IOperator{
@Override
public int doOperator(int num1, int num2) {
return num1 * num2;
}
}
減法和除法就不寫了译秦,參考上面即可峡捡。
上面這些IOperator的實現(xiàn)即為不同的運算策略。光有策略還不行诀浪,我們需要一個控制類棋返,對其進(jìn)行統(tǒng)一管理。所有的引用通過這個管理類來實現(xiàn)雷猪。管理類的實現(xiàn)方式有很多種,千萬別局限于網(wǎng)上的一兩個demo晰房。
方式一:
通過構(gòu)造函數(shù)將策略(IOperator)注入到管理類屬性中求摇,然后直接調(diào)用exec執(zhí)行即可。
demo中相當(dāng)于構(gòu)建了一個加法的控制類殊者,然后再執(zhí)行傳參和運算与境。
//控制類
public class StrategyManager_1 {
private final IOperator operator;
public StrategyManager_1(IOperator operator) {
this.operator = operator;
}
public int exec(int num1, int num2){
return operator.doOperator(num1, num2);
}
//引用方式
public static void main(String[] args) {
AddOperator operator = new AddOperator();
StrategyManager_1 manager_1 = new StrategyManager_1(operator);
int exec = manager_1.exec(1, 2);
System.out.println(exec);
}
}
方式二:
還可以使用switch語句,通過輸入的運算符進(jìn)行匹配計算猖吴。
public class StrategyManager_2 {
public static int exec(int num, int num1, String operator) {
switch (operator) {
case "+" :
return new AddOperator().doOperator(num, num1);
case "*" :
return new MultiplyOperator().doOperator(num, num1);
default:
return 0;
}
}
public static void main(String[] args) {
StrategyManager_2.exec(1, 2, "+");
}
}
這種方式不需要使用者去構(gòu)建策略對象摔刁,有點類似于簡單的工廠模式,只需要傳入一個標(biāo)識即可海蔽,且調(diào)用方式更加簡單共屈,個人更推薦這種方式。實際開發(fā)中也不建議讓使用者自己去封裝策略對象党窜,即增加了調(diào)用者的學(xué)習(xí)成本拗引,也可能會引起各種意想不到的錯誤。要考慮后人是在你的基礎(chǔ)上開發(fā)的幌衣,他們不知道也不需要知道你的策略對象矾削,因為策略對象是你的業(yè)務(wù)的封裝,而使用者并不知道豁护。
真實情況下的計算器哼凯,難道要用戶去創(chuàng)建一個處理加法的策略對象AddOperator嗎?顯然不可能楚里,用戶只關(guān)系輸入的數(shù)字断部、運算符和結(jié)果。
實現(xiàn)的方式可能還有很多腻豌,待各位自行發(fā)現(xiàn)家坎。
樣例2
跟大家分享一下嘱能,在一個springboot項目中,如何編寫一個策略模式的接口虱疏。
最近正好寫過一個對外開放的接口惹骂,用于接受其他項目同步來的用戶信息。但是不同的項目同步來的用戶信息的結(jié)構(gòu)和內(nèi)容是不同的做瞪,有些需要進(jìn)行定制業(yè)務(wù)開發(fā)对粪。
以此背景為例,假設(shè)有兩個項目(A 和 B)需要同步數(shù)據(jù)給我装蓬。
條件:
1.A項目傳入的僅有用戶姓名和手機(jī)號著拭,B項目傳入的僅有身份證和姓名。
2.A項目需要接受完成后同步發(fā)送短信牍帚,B項目需要同步的時候根據(jù)身份證計算年齡和生日儡遮。
3.任何一種情況同步前和完成后都需要記錄日志。
1.創(chuàng)建入?yún)ο?/strong>:
//剝離出來公共的參數(shù)
@Data
public class ReceiveBean {
private String name;
/**
* 來源
*/
private int source;
}
//B項目同步來的參數(shù)
@EqualsAndHashCode(callSuper = true)
@Data
public class BReceiveBean extends ReceiveBean{
private String idNo;
}
//A項目同步來的參數(shù)
@EqualsAndHashCode(callSuper = true)
@Data
public class AReceiveBean extends ReceiveBean{
private String phone;
}
實體bean中使用了lombok相關(guān)的注解代替了顯示的get/set方法暗赶。
在ReceiveBean中有一個關(guān)鍵的source字段用來區(qū)別渠道的來源毙驯,可用其他的方式代替锌雀。
2.定義頂層的策略接口
public interface IReceiveService {
/**
* 接受的參數(shù)為json格式的字符串
* 由調(diào)用者進(jìn)行轉(zhuǎn)換
* @param bean入?yún)? */
void receive(ReceiveBean bean);
}
使用字符串或Map對象能提供更好的兼容性蕴茴,也可以在內(nèi)部進(jìn)行轉(zhuǎn)換玉罐,這里為了方便外部的引用將轉(zhuǎn)換的過程轉(zhuǎn)交給調(diào)用者。
3.抽象層面接口實現(xiàn)
@SuppressWarnings("unused")
public abstract class AbstractReceiveService implements IReceiveService{
//可通過覆蓋這個方法定制接受之前的處理岳锁,替換bean的屬性
protected void receivePre(ReceiveBean bean) {
//記錄日志
}
//通過覆蓋這個方法定制接受之后的處理
//這個可能會不滿足绩衷,若需要的話可以添加一個map類型的參數(shù),用來保存處理過程中產(chǎn)生數(shù)據(jù)
protected void receiveAfter(ReceiveBean bean) {
//記錄日志
}
//需要被重寫,各類型的定制服務(wù)
protected abstract void doAction(ReceiveBean bean);
@Override
public void receive(ReceiveBean bean) {
//業(yè)務(wù)開始處理之前的操作
receivePre(bean);
//業(yè)務(wù)開始處理
doAction(bean);
//業(yè)務(wù)處理之后的操作
receiveAfter(bean);
}
}
抽象類內(nèi)部實現(xiàn)了receive方法激率,這里要看具體的業(yè)務(wù)咳燕,雖然有定制的服務(wù),但總體邏輯相同的可以這么實現(xiàn)柱搜,若差別較大迟郎,可在子類中覆蓋receive方法,自行實現(xiàn)聪蘸,否則就默認(rèn)走相同的流程宪肖。
整體還算比較簡單,receive方法內(nèi)進(jìn)行了參數(shù)的轉(zhuǎn)換健爬,并且調(diào)用了receivePre和receiveAfter進(jìn)行業(yè)務(wù)前后的處理控乾,兩個方法內(nèi)部都做了日志保存,這屬于默認(rèn)操作娜遵,也可以通過覆蓋這兩個方法進(jìn)行業(yè)務(wù)和參數(shù)的定制蜕衡,可以影響后續(xù)的處理。doAction方法可以酌情處理设拟,我將其作為一個抽象方法慨仿,要求子類必須去實現(xiàn)各自的業(yè)務(wù)處理操作久脯,也可以和receivePre和receiveAfter連個方法一樣提供默認(rèn)實現(xiàn)。
拿其中一個A項目的實現(xiàn)類看一下:
@Service
public class AReceiveService extends AbstractReceiveService{
@Override
protected void doAction(ReceiveBean bean) {
//保存記錄
// 使用的時候需要對bean進(jìn)行強(qiáng)轉(zhuǎn)
AReceiveBean ab = (AReceiveBean) bean;
}
@Override
protected void receivePre(ReceiveBean bean) {
super.receivePre(bean);
//記錄日志的同時進(jìn)行其他操作
// 使用的時候需要對bean進(jìn)行強(qiáng)轉(zhuǎn)
AReceiveBean ab = (AReceiveBean) bean;
}
@Override
protected void receiveAfter(ReceiveBean bean) {
super.receiveAfter(bean);
//發(fā)送短信
// 使用的時候需要對bean進(jìn)行強(qiáng)轉(zhuǎn)
AReceiveBean ab = (AReceiveBean) bean;
}
}
內(nèi)部可選擇性的覆蓋方法镰吆,若非必要的話帘撰。
4.控制類
@SuppressWarnings("unused")
public enum ReceiveSourceEnum {
A(1, "aReceiveService") {
@Override
public ReceiveBean coverJson(String json) {
return JSON.parseObject(json, AReceiveBean.class);
}
},
B(2, "bReceiveService") {
@Override
public ReceiveBean coverJson(String json) {
return JSON.parseObject(json, BReceiveBean.class);
}
};
private final int source;
private final String serviceName;
/**
* 在此定制轉(zhuǎn)換成不同類型的bean入?yún)? *
* @param json json格式入?yún)? * @return 結(jié)果
*/
public abstract ReceiveBean coverJson(String json);
//獲取枚舉資源
public static ReceiveSourceEnum getWithSource(int source) {
for (ReceiveSourceEnum sourceEnum : ReceiveSourceEnum.values()) {
if (sourceEnum.getSource() == source) {
return sourceEnum;
}
}
throw new DemoException("source類型不存在!");
}
//獲取服務(wù)
public static IReceiveService getReceiveService(int source) {
ReceiveSourceEnum sourceEnum = getWithSource(source);
Assert.notNull(sourceEnum, "source is null");
IReceiveService receiveService = (IReceiveService)SpringContextUtil.getBean(sourceEnum.getServiceName());
Assert.notNull(sourceEnum, "service is null");
return receiveService;
}
ReceiveSourceEnum(int source, String serviceName) {
this.source = source;
this.serviceName = serviceName;
}
public int getSource() {
return source;
}
public String getServiceName() {
return serviceName;
}
}
將ReceiveSourceEnum作為控制類万皿,內(nèi)部提供兩個靜態(tài)的方法getReceiveService和getWithSource摧找,獲取服務(wù)和枚舉類型。
枚舉類中定義了一個用于將json格式字符串轉(zhuǎn)換成對象的抽象方法coverJson牢硅。因為接受的參數(shù)為json格式字符串蹬耘,這樣可以提高接口的擴(kuò)展性,內(nèi)部轉(zhuǎn)換成對應(yīng)的參數(shù)實體類方便后續(xù)操作减余。
getReceiveService內(nèi)部通過服務(wù)名稱從spring容器中換取bean综苔。這種方式可以靜態(tài)的調(diào)用服務(wù)且方法簡單。
換取的方法網(wǎng)上有很多佳励,這里只提供一種:
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
//獲取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通過name獲取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
//通過class獲取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
//通過name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
5.引用方法
使用springboot的單元測試休里。
注意:
不能使用main函數(shù)直接引用,否則會發(fā)現(xiàn)SpringContextUtil中的ApplicationContext永遠(yuǎn)都是null赃承,原因是classloader導(dǎo)致的。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestBean {
@Test
public void testContextUtil() {
ReceiveBean bean = new ReceiveBean();
bean.setName("123");
bean.setSource(1);
String paramStr = JSON.toJSONString(bean);
ReceiveBean receiveBean = coverJsonToBean(paramStr);
ReceiveSourceEnum.getReceiveService(receiveBean.getSource()).receive(receiveBean);
}
private static ReceiveBean coverJsonToBean(String json){
JSONObject jo = JSONObject.parseObject(json);
if (!jo.containsKey("source")) {
throw new DemoException("傳入?yún)?shù)缺失:source");
}
//存在強(qiáng)轉(zhuǎn)錯誤的風(fēng)險
int source = (int)jo.get("source");
ReceiveSourceEnum sourceEnum = ReceiveSourceEnum.getWithSource(source);
return sourceEnum.coverJson(json);
}
}
不過這樣也可以看出來對于使用者來說悴侵,調(diào)用非常簡單瞧剖。若后續(xù)對接了C項目或其他項目,僅需擴(kuò)展AbstractReceiveService類和ReceiveSourceEnum的類型即可可免。
這里僅是我的一種思路抓于,一種參考,肯定還有其他更好的實現(xiàn)方式浇借。
UML圖
圖片來自于菜鳥教程捉撮。(偷懶中= =)
小結(jié)
最早了解策略模式是為了優(yōu)化代碼中大量的if-else語句,雖然一些情況下可以通過switch語句和enum類去實現(xiàn)妇垢,但擴(kuò)展性并不好巾遭,且代碼會分散在當(dāng)前業(yè)務(wù)類中,當(dāng)對接方比較少時還可以勉強(qiáng)看闯估。比較多的時候灼舍,就會發(fā)現(xiàn),一個入口方法內(nèi)涨薪,整合了大量的定制業(yè)務(wù)骑素,閱讀和維護(hù)都時分困難,并且業(yè)務(wù)類變得時分龐大刚夺。加之缺少注釋說明献丑,后人維護(hù)起來簡直是噩夢級的末捣。
策略模式下,將不同的渠道提取公共的方法置于父類中创橄,將定制的業(yè)務(wù)在各自的子類中實現(xiàn)箩做,使得整體流程時分清晰,且業(yè)務(wù)類不會無限膨脹筐摘。修改和擴(kuò)展只要不涉及公共部分卒茬,可放心變更,不用擔(dān)心牽一發(fā)而動全身咖熟,每種渠道的定制業(yè)務(wù)相對獨立圃酵。
若對接方完全無法適用公共的部分,也不用擔(dān)心馍管,大可直接覆蓋策略方法郭赐,重寫業(yè)務(wù)邏輯,依然可以保證同一個入口确沸。