今天介紹的是?Strategy Pattern (策略模式)。同樣干貨滿滿弹惦,希望看到的你依舊能有所收獲~
? 目的??
做一件事情有不同的實現(xiàn)方式, 可以將變化的部分和不變的部分剝離開, 去除大量的 if/else, 提供高擴展性。
例子代碼
比如我們想要帶妹吃雞, 就要成為一個神槍手悄但。在各種槍戰(zhàn)游戲中, 有各種不同的槍, 我們要根據(jù)實際情況(比如肤频,射程)的不同選擇不同的槍進行射擊。
如果槍的子彈數(shù)量都不富裕, 我們要用最少的子彈算墨,最合適的方法達到最強傷害, 最終大吉大利。這樣汁雷,你想帶妹子去哪里吃雞就去哪里吃雞啦净嘀, 真的是哪里都可以哦报咳。
當我們距離對手:
1米以內(nèi),使用平底鍋( 想我當時三級頭三級甲, 手持 AKM, 滿血滿狀態(tài), 三級包里藥包無數(shù), 到了決賽圈被平底鍋堵在墻角打死啦?? )挖藏;
100 米左右暑刃,使用沖鋒槍;
超過1000 米膜眠,使用狙擊槍(對于想我醬紫的小菜雞岩臣,基本流程是開一槍沒打中, 暴露位置, 被別人一狙打死...囧)。
/**
* 面條式代碼判斷最強武器
*/
publicclassNoodlesKillProcessor{
/**
* 根據(jù)距離判斷最好的武器擊殺對手
*@paramdistance
*/
* 根據(jù)距離判斷最好的武器擊殺對手
*@paramdistance
*/
@BadSmell
publicstaticvoidkillByDistance(intdistance){
????????if(distance <0) {
????????????thrownewRuntimeException
("距離咋還能是負數(shù)呢? 我以為只要我能 -20 cm 呢");
? ? ? ?} ? ? ? ?
????????if(distance >=0&& distance <1) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("二步快速走過去");
System.out.println("掏出平底鍋呼死他");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=1&& distance <10) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("快速走過去");
System.out.println("掏出手槍打死他");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=10&& distance <100) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("身體站直, 心態(tài)穩(wěn)住");
System.out.println("掏出沖鋒槍打死他");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=100&& distance <1000) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("身體蹲下降低后坐力");
System.out.println("掏出步槍");
System.out.println("打開 3 倍鏡");
System.out.println("開槍射擊");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=1000) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("趴在草叢里茍著");
System.out.println("掏出狙擊槍");
System.out.println("打開 8 倍鏡");
System.out.println("開槍射擊");
????????????return;
}
}
}
問題分析
我們覺得這有3個問題宵膨,具體分析如下:
01
可讀性問題
我看這么多 if/else 語句, 里面的 sout 語句目前三四行也還好, 如果我們有上百行的語句, 里面也有很多 if/else, 這樣都不知道下個主 if 跑哪去啦??
02
重復性問題
全都需要發(fā)現(xiàn)敵人, 如果發(fā)現(xiàn)敵人是個成百上千行代碼, 就很麻煩啦架谎。
03
可維護性問題
如果這時候我們新增了一種槍, 比如是散彈槍, 適用10 到20 的時候使用, 這時候我們就需要在加一個 if 語句如下:
/**
* 面條式代碼判斷最強武器
*/publicclassNoodlesKillProcessor{
/**
* 根據(jù)距離判斷最好的武器擊殺對手
*@paramdistance
*/
* 根據(jù)距離判斷最好的武器擊殺對手
*@paramdistance
*/
@BadSmell
publicstaticvoidkillByDistance(intdistance){
????????if(distance <0) {
????????????thrownewRuntimeException
("距離咋還能是負數(shù)呢? 我以為只要我能 -20 cm 呢");
? ? ? ?} ? ? ? ?
????????if(distance >=0&& distance <1) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("二步快速走過去");
System.out.println("掏出平底鍋呼死他");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=1&& distance <10) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("快速走過去");
System.out.println("掏出手槍打死他");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=10&& distance <20) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("身體站直, 瞄準");
System.out.println("打一槍算一槍");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=20&& distance <100) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("身體站直, 心態(tài)穩(wěn)住");
System.out.println("掏出沖鋒槍打死他");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=100&& distance <1000) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("身體蹲下降低后坐力");
System.out.println("掏出步槍");
System.out.println("打開 3 倍鏡");
System.out.println("開槍射擊");
????????????return;
? ? ? ?} ? ? ? ?
????????if(distance >=1000) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("趴在草叢里茍著");
System.out.println("掏出狙擊槍");
System.out.println("打開 8 倍鏡");
System.out.println("開槍射擊");
????????????return;
}
}
}
這個看著也沒啥大問題的樣子, 不就是加了個 if 么, 但是由于我們改動了這個文件, 測試同學問我們需要測試哪些功能, 說是測一種槍需要 5 天???♂?
問題來啦, 本來說是你增加一種槍, 需要測 5 天, 但是現(xiàn)在你說改了這文件, 上下可能有些局部變量共享的, 或者有些方法可能改了入?yún)⒌闹? 這些有負作用的方法被調(diào)用啦, 所以可能狙擊槍也得測一測, 可能手槍也得測一測。
測試同學崩了, 本來 5 天的工作量, 搞成了 5 * 6 天, 一個月都在測槍??, KPI 都沒啦, OKR 都沒啦, 錢都沒啦, 老婆都跑啦, 自己都綠啦等等等, 不拿刀砍你就是真感情啦??
初步嘗試解決
我們先定義好一個基礎(chǔ)類, 解決一下可讀性問題和重復性問題辟躏。
定義一個基礎(chǔ)武器類:
/**
* 抽象的槍
*/
publicabstractclassWeapon{
/**
* 發(fā)現(xiàn)敵人
*/
* 發(fā)現(xiàn)敵人
*/
protectedvoidfindEnemy(){
System.out.println("發(fā)現(xiàn)敵人");
? ?} ? ?
/**
* 開槍前的動作
*/
* 開槍前的動作
*/
protectedabstractvoidpreAction();
/**
* 開槍
*/
* 開槍
*/
protectedabstractvoidshoot();
/**
* 殺人的動作
*/
* 殺人的動作
*/
publicvoidkill(){
findEnemy();
preAction();
shoot();
}
}
逐個實現(xiàn)武器的具體類, 平底鍋, 沖鋒槍, 步槍等類如下:
/**
* 平底鍋
*/
publicclassPanextendsWeapon{
????@Override
protectedvoidpreAction(){
System.out.println("二步快速走過去");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("掏出平底鍋呼死他");
}
}
/**
* 手槍類
*/
publicclassPistolextendsWeapon{
????@Override
protectedvoidpreAction(){
System.out.println("快速走過去");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("掏出手槍打死他");
}
}
/**
* 散彈槍
*/
publicclassShotgunextendsWeapon{
????@Override
protectedvoidpreAction(){
System.out.println("身體站直, 瞄準");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("打一槍算一槍");
}
}
/**
* 狙擊槍
*/
publicclassSniperRifleextendsWeapon{
????@Override
protectedvoidpreAction(){
System.out.println("趴在草叢里茍著");
System.out.println("掏出狙擊槍");
System.out.println("打開 8 倍鏡");
}@Override
protectedvoidshoot(){
System.out.println("開槍射擊");
}
}
/**
* 沖鋒槍
*/
publicclassSubmachineGunextendsWeapon{
????@Override
protectedvoidpreAction(){
System.out.println("身體站直, 心態(tài)穩(wěn)住");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("掏出沖鋒槍打死他");
}
}
我們的方法就可以改動得更清晰啦
/**
* 抽象出類代碼判斷最強武器
*/
publicclassWeaponKillProcessor{
* 根據(jù)距離判斷最好的武器擊殺對手
*
*@paramdistance
*/
@BadSmell
publicstaticvoidkillByDistance(intdistance){
????????if(distance <0) {
????????????thrownewRuntimeException
("距離咋還能是負數(shù)呢? 我以為只要我能 -20 cm 呢?");
? ? ? ?}
Weapon weapon =null;
????????if(distance >=0&& distance <1) {
weapon =newPan();
}elseif(distance >=1&& distance <10) {
weapon =newPistol();
}elseif(distance >10&& distance <20) {
weapon =newShotgun();
}elseif(distance >=20&& distance <100) {
weapon =newSubmachineGun();
}elseif(distance >=100&& distance <1000) {
weapon =newRifle();
}elseif(distance >=1000) {
weapon =newSniperRifle();
}
weapon.kill();
}
}
類圖如下:
使用策略模式
上面的代碼沒有解決最根本的問題, 也就是去除 if/else, 所用的方法其實就是將 if else 轉(zhuǎn)換為 for, 這樣的代碼后續(xù)添加槍就不需要再增加新的類型啦谷扣。
我們先定義一個通用的策略模式接口如下:
/**
* 策略模式
*/publicinterfaceStrategy
RextendsAbstractStrategyResponse>{
* 執(zhí)行策略
*@paramrequest
*@return
*/
RexecuteStrategy(T request);
}
入?yún)⒑统鰠⒍际腔镜某橄箢悾?/p>
/**
* 策略模式抽象入?yún)?/p>
*/
publicabstractclassAbstractStrategyRequest{
}
/**
* 策略模式抽象出參
*/
publicabstractclassAbstractStrategyResponse{
}
實現(xiàn)一個武器抽象類實現(xiàn)接口:
publicabstractclassWeaponStrategyimplements
Strategy
AbstractStrategyResponse>{
/**
* 發(fā)現(xiàn)敵人
*/
protectedvoidfindEnemy(){
System.out.println("發(fā)現(xiàn)敵人");
? ?} ? ?
/**
* 開槍前的動作
*/
protectedabstractvoidpreAction();
/**
* 開槍
*/
protectedabstractvoidshoot();
/**
* 獲取距離范圍
*@return
*/
protectedabstractRangequeryDistanceRange();
/**
* 殺人的動作
*/
publicvoidkill(){
findEnemy();
preAction();
shoot();
? ?} ? ?
????@Override
publicAbstractStrategyResponse
????????executeStrategy(WeaponStrategyRequest request){
System.out.println("距離敵人 "+
????????????????????????????????request.getDistance());
? ? ? ?kill(); ? ? ? ?
????????returnnull;
}
}
其中的 Range 類實現(xiàn)如下:
/**
* 范圍類
*@param
*/
@Data
@AllArgsConstructor
publicclassRange>{
????privateT start;
????privateT end;
????publicRange(T start, T end){
????????this.start = start;
????????this.end = end;
? ?} ? ?
????privatebooleanisIncludeStart =true;
????privatebooleanisIncludeEnd =false;
/**
* 判斷是否在范圍內(nèi)
*@paramtarget
*@return
*/
publicbooleaninRange(T target){
????????if(isIncludeStart) {
????????????if(start.compareTo(target) >0) {
????????????????returnfalse;
? ? ? ? ? ?}
}else{
????????????if(start.compareTo(target) >=0) {
????????????????returnfalse;
? ? ? ? ? ?}
? ? ? ?} ? ? ? ?
????????if(isIncludeEnd) {
????????????if(end.compareTo(target) <0) {
????????????????returnfalse;
? ? ? ? ? ?}
}else{
????????????if(end.compareTo(target) <=0) {
????????????????returnfalse;
? ? ? ? ? ?}
? ? ? ?} ? ? ? ?
????????returntrue;
}
}
依次實現(xiàn)這個抽象武器策略類:
/**
* 平底鍋
*/
publicclassPanStrategyextendsWeaponStrategy{@Override
protectedvoidpreAction(){
System.out.println("二步快速走過去");
}@Override
protectedvoidshoot(){
System.out.println("掏出平底鍋呼死他");
}@Override
protectedRangequeryDistanceRange(){returnnewRange<>(0,1);
}
}
/**
* 手槍類
*/
publicclassPistolStrategyextendsWeaponStrategy{
????@Override
protectedvoidpreAction(){
System.out.println("快速走過去");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("掏出手槍打死他");
? ?} ? ?
????@Override
protectedRangequeryDistanceRange(){
????????returnnewRange<>(1,10);
}
}
/**
* 步槍
*/
publicclassRifleStrategyextendsWeaponStrategy{
????@Override
protectedvoidpreAction(){
System.out.println("身體蹲下降低后坐力");
System.out.println("掏出步槍");
System.out.println("打開 3 倍鏡");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("開槍射擊");
? ?} ? ?
????@Override
protectedRangequeryDistanceRange(){
????????returnnewRange<>(100,1000);
}
}
/**
* 散彈槍
*/
publicclassShotgunStrategyextendsWeaponStrategy{
????@Override
protectedvoidpreAction(){
System.out.println("身體站直, 瞄準");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("打一槍算一槍");
? ?} ? ?
????@Override
protectedRangequeryDistanceRange(){
????????returnnewRange<>(10,20);
}
}
publicclassSniperRifleStrategyextendsWeaponStrategy{
????@Override
protectedvoidpreAction(){
System.out.println("趴在草叢里茍著");
System.out.println("掏出狙擊槍");
System.out.println("打開 8 倍鏡");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("開槍射擊");
? ?} ? ?
????@Override
protectedRangequeryDistanceRange(){
????????returnnewRange<>(1000, Integer.MAX_VALUE);
}
}
/**
* 沖鋒槍
*/
publicclassSubmachineGunStrategyextendsWeaponStrategy{
????@Override
protectedvoidpreAction(){
System.out.println("身體站直, 心態(tài)穩(wěn)住");
? ?} ? ?
????@Override
protectedvoidshoot(){
System.out.println("掏出沖鋒槍打死他");
? ?} ? ?
????@Override
protectedRangequeryDistanceRange(){
????????returnnewRange<>(20,100);
}
}
定義一個上下文類來對入?yún)⑦M行路由:
/**
* 策略上下文, 用來路由策略
*/
public class StrategyContext {
? ?public static final List<WeaponStrategy>
????????WEAPON_STRATEGYS = new ArrayList<>();
static {
WEAPON_STRATEGYS.add(new PanStrategy());
WEAPON_STRATEGYS.add(new PistolStrategy());
WEAPON_STRATEGYS.add(new RifleStrategy());
WEAPON_STRATEGYS.add(new ShotgunStrategy());
WEAPON_STRATEGYS.add(new SniperRifleStrategy());
WEAPON_STRATEGYS.add(new SubmachineGunStrategy());
}
public static void execute(Integer distance) {
? ? ? ?WEAPON_STRATEGYS.stream().
????????filter((weaponStrategy -> {
? ? ? ? ? ?Range<Integer> integerRange =
????????????????weaponStrategy.queryDistanceRange();
? ? ? ? ? ?return integerRange.inRange(distance);
? ? ? ?})).
????????findAny().
????????get().
????????executeStrategy(
????????????new WeaponStrategyRequest(distance));
}
}
最后在主方法里面調(diào)用就好啦:
publicclassApp{
publicstaticvoidmain(String[] args){
????????intdistance =89;
? ? ? ?StrategyContext.execute(89);
}
}
結(jié)果如下:
距離敵人 89
發(fā)現(xiàn)敵人
身體站直, 心態(tài)穩(wěn)住
掏出沖鋒槍打死他
類圖如下:
?其他
這個是比較常見的方法, 基于Spring 的實現(xiàn)和基于反射的實現(xiàn), 還有生產(chǎn)上的例子會在習題中給出。
策略者模式課后作業(yè)
1.了解使用反射和在 spring 框架下策略模式的使用, 將文中的例子轉(zhuǎn)為這二種情況下的代碼捎琐。
2. 比較利用運行時動態(tài)決策策略和初始化的時候就將策略整理到一個集合下的性能差異会涎。
3. 作為一個銀行開發(fā)人員, 有一天產(chǎn)品經(jīng)理需要你完成借貸優(yōu)惠卷計算的部分邏輯, 大致需求如下:
貸款分為本金利息, 輸入一種貸款屬性 (本金 + 利息) 和優(yōu)惠卷類型, 需要你計算用戶總共應(yīng)該還的金額從而給用戶推薦最省錢的優(yōu)惠卷
貸款屬性類定義如下:
/**
* 貸款金額屬性
*/
@DatapublicclassLoanAmtDetail{
/**
* 本金
*/
privateBigDecimal priceAmt;
/**
* 利息
*/
privateBigDecimal rateAmt;
}
優(yōu)惠卷類型如下:
一. 利息全免, 本金不變
二. 利息打 8 折, 本金不變
三. 利息固定抵扣抵扣 800 元, 本金不變
四. 本金加利息一起打 9 折
請使用策略模式實現(xiàn)。
這時候產(chǎn)品提出了一個新的優(yōu)惠卷, 要求本金抵扣 80 元, 利息全免, 我們的改動有多大,測試回歸測試的范圍有多大??