《設(shè)計(jì)模式之策略模式》

一尉桩、策略模式簡介

1.1 簡介

策略模式(Strategy)蜘犁,定義了一組算法屈扎,將每個算法都封裝起來墨叛,并且使它們之間可以互換漠趁。從而導(dǎo)致客戶端程序獨(dú)立于算法的改變甥绿。

UML結(jié)構(gòu)圖如下


image.png

主要解決:在有多種算法相似的情況下妹窖,使用 if...else 所帶來的復(fù)雜和難以維護(hù)蜓萄。
使用場景:一個系統(tǒng)有許多許多類绸硕,而區(qū)分它們的只是他們直接的行為出嘹。
如何解決:將這些算法封裝成一個一個的類,任意地替換。
關(guān)鍵代碼:實(shí)現(xiàn)同一個接口。
設(shè)計(jì)原則:找出應(yīng)用中需要變化的部分游桩,把他們獨(dú)立出來雀彼,不要和那些不需要變化的代碼混合在一起。

1.2 使用場景

使用用場景:
a) 如果在一個系統(tǒng)里面有許多類,它們之間的區(qū)別僅在于它們的行為,那么使用策略模式可以動態(tài)地讓一個對象在許多行為中選擇一種行為。
b) 一個系統(tǒng)需要動態(tài)地在幾種算法中選擇一種扰肌。
c) 如果一個對象有很多的行為,如果不用恰當(dāng)?shù)哪J轿保@些行為就只好使用多重的條件選擇語句來實(shí)現(xiàn)狡耻。

如:
a) 諸葛亮的錦囊妙計(jì),每一個錦囊就是一個策略猴凹。
b) 出門選擇不同的交通工具夷狰。
c) 支付時選擇不同的支付通道。
d) JAVA AWT 中的 LayoutManager郊霎,選用不同的模板或皮膚進(jìn)行展示沼头。

  • 融山系統(tǒng)使用場景:
    a) 鑒權(quán)中心根據(jù)不同的token類型進(jìn)行 生成token, 校驗(yàn)token, 獲取token參數(shù)。
    b) 數(shù)據(jù)門戶數(shù)據(jù)填報, 三大業(yè)務(wù)線的數(shù)據(jù)結(jié)構(gòu)基本相同,都需要進(jìn)行5級菜單聯(lián)動进倍,數(shù)據(jù)過濾土至,操作權(quán)限檢查等。
    c) 合同參數(shù)校驗(yàn)服務(wù)猾昆,不同的合同都要進(jìn)行通用參數(shù)檢查和特殊參數(shù)檢查陶因。

注意事項(xiàng):如果一個系統(tǒng)的策略多于四個,就需要考慮使用混合模式垂蜗,解決策略類膨脹的問題楷扬。

二、策略模式如何實(shí)現(xiàn)

2.1 通過分離變化得出的策略接口Strategy

public interface TokenStrategy {

     String creatToken(BaseToken token);

     Boolean checkToken(String token);

     Map<String, Object> getParms(String token);
}

2.2 Strategy的實(shí)現(xiàn)類

  • OperationAdminUserToken
@Slf4j
@Component
public class OperationAdminUserToken implements TokenStrategy {
    @Override
    public String creatToken(BaseToken token) {
        log.info("adminUserToken生成成功");
        return "adminUserToken 生成成功";
    }

    @Override
    public Boolean checkToken(String token) {
        log.info("token ={}, adminUserToken 檢驗(yàn)成功", token);
        return true;
    }

    @Override
    public Map<String, Object> getParms(String  token) {
        log.info("adminUserToken 獲取參數(shù)");
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("tokenType", "adminUserToken");
        return hashMap;
    }
}

  • OperationApplicationToken
@Slf4j
@Component
public class OperationApplicationToken implements TokenStrategy{
    @Override
    public String creatToken(BaseToken token) {
        log.info("applicationToken生成成功");
        return "applicationToken 生成成功";
    }

    @Override
    public Boolean checkToken(String token) {
        log.info("token ={}, applicationToken 校驗(yàn)成功", token);
        return true;
    }

    @Override
    public Map<String, Object> getParms(String  token) {
        log.info("applicationToken 參數(shù)獲取成功");
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("tokenType", "application token");
        return hashMap;
    }
}

2.3 定義一個封裝了策略的Context

public class TokenContext {

    private TokenStrategy tokenStrategy;

    public TokenContext(TokenStrategy tokenStrategy) {
        this.tokenStrategy = tokenStrategy;
    }

    public String creatToken(BaseToken token) {
        return tokenStrategy.creatToken(token);
    }

    ;

    public Boolean checkToken(String token) {
        return tokenStrategy.checkToken(token);
    }

    ;

    public Map<String, Object> getParms(String token) {
        return tokenStrategy.getParms(token);
    }

    ;
}

2.4 在客戶程序中選擇

@Slf4j
public class MainTest {

    public static void main(String[] args) {
        TokenContext tokenContext = new TokenContext(new OperationApplicationToken());
        ApplicationToken applicationToken = new ApplicationToken();
        applicationToken.setApplicationCode("fyfq");
        applicationToken.setApplicationIp("0.0.0.1");
        applicationToken.setEnv("test");
        applicationToken.setTokenId(UUID.randomUUID().toString());
        tokenContext.creatToken(applicationToken);
        tokenContext.checkToken("applicationTokn.....");
        Map<String, Object> hashMap = tokenContext.getParms("applicationTokn.....");
        log.info("hashMap ={}", hashMap);



        tokenContext = new TokenContext(new OperationAdminUserToken());
        AdminUserToken adminUserToken = new AdminUserToken();
        adminUserToken.setAdminUserId("123456");
        adminUserToken.setVersion("v1");
        adminUserToken.setEnv("test");
        adminUserToken.setTokenId(UUID.randomUUID().toString());
        tokenContext.creatToken(applicationToken);
        tokenContext.checkToken("adminUserToken.....");
        hashMap = tokenContext.getParms("adminUserToken.....");
        log.info("hashMap ={}", hashMap);


        tokenContext = new TokenContext(new OperationCustomerToken());
        CustomerToken customerToken = new CustomerToken();
        customerToken.setCustomerId("customerId");
        customerToken.setNewTokenIntervalHours(9);
        customerToken.setTokenRefreshIntervalHours(10);
        customerToken.setCustomerId("123");
        customerToken.setEnv("prd");
        customerToken.setTokenId(UUID.randomUUID().toString());
        tokenContext.creatToken(applicationToken);
        tokenContext.checkToken("customerUserToken.....");

        hashMap = tokenContext.getParms("customerUserToken.....");
        log.info("hashMap ={}", hashMap);


        tokenContext = new TokenContext(new OperationOtherToken());
        OtherToken otherToken = new OtherToken();
        otherToken.setInfo("other token ");
        otherToken.setEnv("prd");
        otherToken.setTokenId(UUID.randomUUID().toString());
        tokenContext.creatToken(applicationToken);
        tokenContext.checkToken("other token.....");
        hashMap = tokenContext.getParms("other token.....");
        log.info("hashMap ={}", hashMap);
    }
}

三贴见、策略模式優(yōu)缺點(diǎn)

3.1 與繼承相比(在父類中提供實(shí)現(xiàn)方法烘苹,子類通過繼承獲取父類的某種功能)

繼承優(yōu)點(diǎn):簡單易用,已有應(yīng)用可以快速添加額外功能片部。
繼承缺點(diǎn):不具備靈活性镣衡,對未來變化支持差。

3.2 與抽象方法相比 (在父類中提供抽象方法档悠,強(qiáng)迫子類實(shí)現(xiàn)自己的某種功能)

抽象方法優(yōu)點(diǎn):足夠靈活
缺點(diǎn):每一個子類都要實(shí)現(xiàn)一遍代碼廊鸥,即使是相同的也不例外。

3.3 綜合對比

優(yōu)點(diǎn): 1站粟、算法可以自由切換黍图。 2、避免使用多重條件判斷奴烙。 3、擴(kuò)展性良好剖张。
缺點(diǎn): 1切诀、策略類會增多。 2搔弄、所有策略類都需要對外暴露幅虑。

  • 繼承是重用代碼的利器,但是繼承并不總是最好的工具顾犹。
  • Favor composition over inheritance. 復(fù)合優(yōu)先于繼承倒庵,多用組合,少用繼承炫刷。

What is Composition?
在類中增加一個私有域擎宝,引用另一個已有類的實(shí)例,通過調(diào)用引用實(shí)例的方法從而獲得新的功能浑玛,這種設(shè)計(jì)被稱作組合(復(fù)合)绍申。

四、進(jìn)一步優(yōu)化

問題描述:
在選擇策略的時候,通常需要根據(jù)不同的條件判斷對策略進(jìn)行選擇极阅,會產(chǎn)生多個if ......else胃碾,使用動態(tài)選擇執(zhí)行bean 的方式來選擇策略可進(jìn)一步優(yōu)化代碼

4.1 定義相關(guān)的枚舉映射

/**
 * @author: lipei
 * @Date:2020/7/2 2:43 下午
 */
public enum StrategyClassEnum {
    APPLICATION_TOKEN("app", OperationApplicationToken.class),
    ADMIN_USER_TOKEN("adminuser", OperationAdminUserToken.class),
    CUSTOMER_TOKEN("customer", OperationCustomerToken.class);


    private String tokenType;

    private Class clazz;

    StrategyClassEnum(String tokenType, Class clazz) {
        this.tokenType = tokenType;
        this.clazz = clazz;
    }

    public String getTokenType() {
        return tokenType;
    }

    public Class getClazz() {
        return clazz;
    }

    public static Class getClazz(String tokenType) {
        for (StrategyClassEnum ele : values()) {
            if(ele.getTokenType().equals(tokenType)) return ele.getClazz();
        }
        return null;
    }

}

4.2 定義相關(guān)的工具類,可以通過策略類的名稱從容器中獲取相關(guān)的bean

@Component
@Slf4j
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null) {
            SpringUtil.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);
    }

    //通過name,以及Clazz返回指定的Bean
    public static String[] getAllBens( ){
        return getApplicationContext().getBeanDefinitionNames();
    }

}

4.3 定義相關(guān)的接口并實(shí)現(xiàn)相關(guān)功能

  • IStrategyTokenService
public interface IStrategyTokenService {

    /**
     * 檢查token是否合法
     * @param token
     * @return
     */
    Boolean checkToken(String tokenType, String token);
}

  • StrategyTokenServiceImpl
@Slf4j
@Component
public class StrategyTokenServiceImpl implements IStrategyTokenService {

    @Override
    public Boolean checkToken( String tokenType, String token) {
        Class strategyClass = StrategyClassEnum.getClazz(tokenType);
        log.info("strategyClass ={}", strategyClass);

        TokenContext tokenContext = null;
        try {
            tokenContext = new TokenContext((TokenStrategy)strategyClass.newInstance());
            return tokenContext.checkToken(token);

        } catch (Exception e) {
            log.error("check token error , e={}", e);
            return false;
        }
    }
}

4.4 使用

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class GofStrategyTests {

    @Autowired
    IStrategyTokenService tokenService;

    @Test
    public void test01() {
        TestImplBeans testImplBeans = SpringUtil.getBean(TestImplBeans.class);
        testImplBeans.print();
    }

    @Test
    public void test02() {
        String tokenType = "app";
        String token = "客戶隨機(jī)輸入的token";
        Class strategyClass = StrategyClassEnum.getClazz(tokenType);
        log.info("strategyClass ={}", strategyClass);

        TokenContext tokenContext = null;
        try {
            tokenContext = new TokenContext((TokenStrategy)strategyClass.newInstance());
            ApplicationToken applicationToken = new ApplicationToken();
            applicationToken.setApplicationCode("fyfq");
            applicationToken.setApplicationIp("0.0.0.1");
            applicationToken.setEnv("test");
            applicationToken.setTokenId(UUID.randomUUID().toString());
            tokenContext.creatToken(applicationToken);
            tokenContext.checkToken("applicationTokn.....");
            Map<String, Object> hashMap = tokenContext.getParms("applicationTokn.....");
            log.info("hashMap ={}", hashMap);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test03(){
        String tokenType01 = "app";
        String token01 = "應(yīng)用所攜帶的token";
        tokenService.checkToken(tokenType01, token01);

        String tokenType02 = "adminuser";
        String token02 = "用戶登陸時所攜帶的token";
        tokenService.checkToken(tokenType02, token02);
    }
}

五筋搏、Spring 中的應(yīng)用

Spring 中策略模式使用有多個地方仆百,如 Bean 定義對象的創(chuàng)建以及代理對象的創(chuàng)建等。這里主要看一下代理對象創(chuàng)建的策略模式的實(shí)現(xiàn)奔脐。

Spring 中策略模式的實(shí)現(xiàn)

Spring 的代理方式有兩個 Jdk 動態(tài)代理和 CGLIB 代理俄周。這兩個代理方式的使用正是使用了策略模式。它的結(jié)構(gòu)圖如下所示:

Spring 中策略模式結(jié)構(gòu)圖

Spring 中策略模式結(jié)構(gòu)圖

在上面結(jié)構(gòu)圖中與標(biāo)準(zhǔn)的策略模式結(jié)構(gòu)稍微有點(diǎn)不同帖族,這里抽象策略是 AopProxy 接口栈源,Cglib2AopProxy 和 JdkDynamicAopProxy 分別代表兩種策略的實(shí)現(xiàn)方式,ProxyFactoryBean 就是代表 Context 角色竖般,它根據(jù)條件選擇使用 Jdk 代理方式還是 CGLIB 方式甚垦,而另外三個類主要是來負(fù)責(zé)創(chuàng)建具體策略對象,ProxyFactoryBean 是通過依賴的方法來關(guān)聯(lián)具體策略對象的涣雕,它是通過調(diào)用策略對象的 getProxy(ClassLoader classLoader) 方法來完成操作艰亮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挣郭,隨后出現(xiàn)的幾起案子迄埃,更是在濱河造成了極大的恐慌,老刑警劉巖兑障,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侄非,死亡現(xiàn)場離奇詭異,居然都是意外死亡流译,警方通過查閱死者的電腦和手機(jī)逞怨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來福澡,“玉大人叠赦,你說我怎么就攤上這事「镌遥” “怎么了除秀?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長算利。 經(jīng)常有香客問我册踩,道長,這世上最難降的妖魔是什么笔时? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任棍好,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘借笙。我一直安慰自己扒怖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布业稼。 她就那樣靜靜地躺著盗痒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪低散。 梳的紋絲不亂的頭發(fā)上俯邓,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音熔号,去河邊找鬼稽鞭。 笑死,一個胖子當(dāng)著我的面吹牛引镊,可吹牛的內(nèi)容都是我干的朦蕴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼弟头,長吁一口氣:“原來是場噩夢啊……” “哼吩抓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赴恨,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤疹娶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伦连,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雨饺,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年惑淳,在試婚紗的時候發(fā)現(xiàn)自己被綠了沛膳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡汛聚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出短荐,到底是詐尸還是另有隱情倚舀,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布忍宋,位于F島的核電站痕貌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏糠排。R本人自食惡果不足惜舵稠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哺徊,春花似錦室琢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至轿钠,卻和暖如春巢钓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疗垛。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工症汹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贷腕。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓背镇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親花履。 傳聞我的和親對象是個殘疾皇子芽世,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344