作者案:本文介紹的是 Strategy Pattern (策略模式)。干貨滿滿,希望閱讀后你能有所收獲~
目的
做一件事情有不同的實(shí)現(xiàn)方式,可以將變化的部分和不變的部分剝離開,去除大量的 if/else,提供高擴(kuò)展性。
例子代碼
比如我們想要帶妹吃雞,就要成為一個(gè)神槍手。在各種槍戰(zhàn)游戲中,有各種不同的槍,我們要根據(jù)射程的不同選擇不同的槍進(jìn)行射擊。
如果槍的子彈數(shù)量都不富裕,我們要用最少的子彈,最合適的方法達(dá)到最強(qiáng)傷害,最終大吉大利。
當(dāng)我們距離對(duì)手:
- 1米以內(nèi),使用平底鍋(想我當(dāng)時(shí)三級(jí)頭三級(jí)甲,手持 AKM,滿血滿狀態(tài),三級(jí)包里藥包無(wú)數(shù),到了決賽圈被平底鍋堵在墻角打死啦?? );
- 100 米左右,使用沖鋒槍;
- 超過(guò) 1000 米,使用狙擊槍(對(duì)于我這樣的小菜雞,基本流程是開一槍沒(méi)打中,暴露位置,被別人一狙打死...囧)。
/**
* 面條式代碼判斷最強(qiáng)武器
*/
public class NoodlesKillProcessor {
/**
* 根據(jù)距離判斷最好的武器擊殺對(duì)手
* @param distance
*/
@BadSmell
public static void killByDistance(int distance) {
if(distance < 0) {
throw new RuntimeException("距離咋還能是負(fù)數(shù)呢?");
}
if(distance >= 0 && distance < 1) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("兩步快速走過(guò)去");
System.out.println("掏出平底鍋呼他");
return;
}
if(distance >= 1 && distance < 10) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("快速走過(guò)去");
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;
}
}
}
問(wèn)題分析
我覺(jué)得這有 3 個(gè)問(wèn)題,具體分析如下:
01 可讀性問(wèn)題
我看這么多 if/else 語(yǔ)句,里面的 sout 語(yǔ)句目前三四行也還好,如果我們有上百行的語(yǔ)句,里面也有很多 if/else,這樣都不知道下個(gè)主 if 跑哪去啦 ??
02 重復(fù)性問(wèn)題
全都需要發(fā)現(xiàn)敵人,如果發(fā)現(xiàn)敵人是個(gè)成百上千行代碼,就很麻煩啦。
03 可維護(hù)性問(wèn)題
如果這時(shí)候我們新增了一種槍,比如是霰彈槍,適用 10 到 20 的時(shí)候使用,這時(shí)候我們就需要在加一個(gè) if 語(yǔ)句如下:
/**
* 面條式代碼判斷最強(qiáng)武器
*/
public class NoodlesKillProcessor {
/**
* 根據(jù)距離判斷最好的武器擊殺對(duì)手
* @param distance
*/
@BadSmell
public static void killByDistance(int distance) {
if(distance < 0) {
throw new RuntimeException("距離咋還能是負(fù)數(shù)呢?");
}
if(distance >= 0 && distance < 1) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("兩步快速走過(guò)去");
System.out.println("掏出平底鍋呼他");
return;
}
if(distance >= 1 && distance < 10) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("快速走過(guò)去");
System.out.println("掏出手槍打他");
return;
}
if(distance >= 10 && distance < 20) {
System.out.println("發(fā)現(xiàn)敵人");
System.out.println("身體站直, 瞄準(zhǔn)");
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;
}
}
}
這個(gè)看著也沒(méi)啥大問(wèn)題的樣子,不就是加了個(gè) if 么,但是由于我們改動(dòng)了這個(gè)文件,測(cè)試同學(xué)問(wèn)我們需要測(cè)試哪些功能,說(shuō)是測(cè)一種槍需要 5 天。
問(wèn)題來(lái)啦,本來(lái)說(shuō)是你增加一種槍, 需要測(cè) 5 天,但是現(xiàn)在你說(shuō)改了這文件,上下可能有些局部變量共享的,或者有些方法可能改了入?yún)⒌闹?,這些有負(fù)作用的方法被調(diào)用啦,所以可能狙擊槍也得測(cè)一測(cè),可能手槍也得測(cè)一測(cè)。
測(cè)試同學(xué)崩了,本來(lái) 5 天的工作量,搞成了 5 * 6 天,一個(gè)月都在測(cè)槍。
初步嘗試解決
我們先定義好一個(gè)基礎(chǔ)類,解決一下可讀性問(wèn)題和重復(fù)性問(wèn)題。
定義一個(gè)基礎(chǔ)武器類:
/**
* 抽象的槍
*/
public abstract class Weapon {
/**
* 發(fā)現(xiàn)敵人
*/
protected void findEnemy() {
System.out.println("發(fā)現(xiàn)敵人");
}
/**
* 開槍前的動(dòng)作
*/
protected abstract void preAction();
/**
* 開槍
*/
protected abstract void shoot();
/**
* 整體的動(dòng)作
*/
public void kill() {
findEnemy();
preAction();
shoot();
}
}
逐個(gè)實(shí)現(xiàn)武器的具體類、平底鍋、沖鋒槍、步槍等類如下:
/**
* 平底鍋
*/
public class Pan extends Weapon {
@Override
protected void preAction() {
System.out.println("兩步快速走過(guò)去");
}
@Override
protected void shoot() {
System.out.println("掏出平底鍋呼他");
}
}
/**
* 手槍類
*/
public class Pistol extends Weapon {
@Override
protected void preAction() {
System.out.println("快速走過(guò)去");
}
@Override
protected void shoot() {
System.out.println("掏出手槍打他");
}
}
/**
* 霰彈槍
*/
public class Shotgun extends Weapon {
@Override
protected void preAction() {
System.out.println("身體站直, 瞄準(zhǔn)");
}
@Override
protected void shoot() {
System.out.println("打一槍算一槍");
}
}
/**
* 狙擊槍
*/
public class SniperRifle extends Weapon {
@Override
protected void preAction() {
System.out.println("趴在草叢里茍著");
System.out.println("掏出狙擊槍");
System.out.println("打開 8 倍鏡");
}
@Override
protected void shoot() {
System.out.println("開槍射擊");
}
}
/**
* 沖鋒槍
*/
public class SubmachineGun extends Weapon {
@Override
protected void preAction() {
System.out.println("身體站直, 心態(tài)穩(wěn)住");
}
@Override
protected void shoot() {
System.out.println("掏出沖鋒槍打他");
}
}
我們的方法就可以改動(dòng)得更清晰啦。
/**
* 抽象出類代碼判斷最強(qiáng)武器
*/
public class WeaponKillProcessor {
/**
* 根據(jù)距離判斷最好的武器擊殺對(duì)手
* @param distance
*/
@BadSmell
public static void killByDistance(int distance) {
if (distance < 0) {
throw new RuntimeException
("距離咋還能是負(fù)數(shù)呢?");
}
Weapon weapon = null;
if (distance >= 0 && distance < 1) {
weapon = new Pan();
} else if (distance >= 1 && distance < 10) {
weapon = new Pistol();
} else if (distance > 10 && distance < 20) {
weapon = new Shotgun();
} else if (distance >= 20 && distance < 100) {
weapon = new SubmachineGun();
} else if (distance >= 100 && distance < 1000) {
weapon = new Rifle();
} else if (distance >= 1000) {
weapon = new SniperRifle();
}
weapon.kill();
}
}
類圖如下:
使用策略模式
上面的代碼沒(méi)有解決最根本的問(wèn)題,也就是去除 if/else,所用的方法其實(shí)就是將 if else 轉(zhuǎn)換為 for,這樣的代碼后續(xù)添加槍就不需要再增加新的類型啦。
我們先定義一個(gè)通用的策略模式接口如下:
/**
* 策略模式
*/
public interface Strategy
<T extends AbstractStrategyRequest,
R extends AbstractStrategyResponse> {
/*
* 執(zhí)行策略
* @param request
* @return
*/
R executeStrategy(T request);
}
入?yún)⒑统鰠⒍际腔镜某橄箢悾?/p>
/**
* 策略模式抽象入?yún)?br> */
public abstract class AbstractStrategyRequest {
}
/**
* 策略模式抽象出參
*/
public abstract class AbstractStrategyResponse {
}
實(shí)現(xiàn)一個(gè)武器抽象類實(shí)現(xiàn)接口:
public abstract class WeaponStrategy implements
Strategy<WeaponStrategyRequest,
AbstractStrategyResponse> {
/**
* 發(fā)現(xiàn)敵人
*/
protected void findEnemy() {
System.out.println("發(fā)現(xiàn)敵人");
}
/**
* 開槍前的動(dòng)作
*/
protected abstract void preAction();
/**
* 開槍
*/
protected abstract void shoot();
/**
* 獲取距離范圍
* @return
*/
protected abstract Range<Integer> queryDistanceRange();
/**
* 整體的動(dòng)作
*/
public void kill() {
findEnemy();
preAction();
shoot();
}
@Override
public AbstractStrategyResponse
executeStrategy(WeaponStrategyRequest request) {
System.out.println("距離敵人 " + request.getDistance());
kill();
return null;
}
}
其中的 Range 類實(shí)現(xiàn)如下:
/**
* 范圍類
* @param <T>
*/
@Data
@AllArgsConstructor
public class Range<T extends Comparable<T>> {
private T start;
private T end;
public Range(T start, T end) {
this.start = start;
this.end = end;
}
private boolean isIncludeStart = true;
private boolean isIncludeEnd = false;
/**
* 判斷是否在范圍內(nèi)
* @param target
* @return
*/
public boolean inRange(T target) {
if(isIncludeStart) {
if(start.compareTo(target) > 0) {
return false;
}
} else {
if(start.compareTo(target) >= 0) {
return false;
}
}
if(isIncludeEnd) {
if(end.compareTo(target) < 0) {
return false;
}
} else {
if(end.compareTo(target) <= 0) {
return false;
}
}
return true;
}
}
依次實(shí)現(xiàn)這個(gè)抽象武器策略類:
/**
* 平底鍋
*/
public class PanStrategy extends WeaponStrategy {
@Override
protected void preAction() {
System.out.println("二步快速走過(guò)去");
}
@Override
protected void shoot() {
System.out.println("掏出平底鍋呼他");
}
@Override
protected Range<Integer> queryDistanceRange() {
return new Range<>(0, 1);
}
}
/**
* 手槍類
*/
public class PistolStrategy extends WeaponStrategy {
@Override
protected void preAction() {
System.out.println("快速走過(guò)去");
}
@Override
protected void shoot() {
System.out.println("掏出手槍打他");
}
@Override
protected Range<Integer> queryDistanceRange() {
return new Range<>(1, 10);
}
}
/**
* 步槍
*/
public class RifleStrategy extends WeaponStrategy {
@Override
protected void preAction() {
System.out.println("身體蹲下降低后坐力");
System.out.println("掏出步槍");
System.out.println("打開 3 倍鏡");
}
@Override
protected void shoot() {
System.out.println("開槍射擊");
}
@Override
protected Range<Integer> queryDistanceRange() {
return new Range<>(100, 1000);
}
}
/**
* 霰彈槍
*/
public class ShotgunStrategy extends WeaponStrategy {
@Override
protected void preAction() {
System.out.println("身體站直, 瞄準(zhǔn)");
}
@Override
protected void shoot() {
System.out.println("打一槍算一槍");
}
@Override
protected Range<Integer> queryDistanceRange() {
return new Range<>(10, 20);
}
}
/**
* 狙擊槍
*/
public class SniperRifleStrategy extends WeaponStrategy {
@Override
protected void preAction() {
System.out.println("趴在草叢里茍著");
System.out.println("掏出狙擊槍");
System.out.println("打開 8 倍鏡");
}
@Override
protected void shoot() {
System.out.println("開槍射擊");
}
@Override
protected Range<Integer> queryDistanceRange() {
return new Range<>(1000, Integer.MAX_VALUE);
}
}
/**
* 沖鋒槍
*/
public class SubmachineGunStrategy extends WeaponStrategy {
@Override
protected void preAction() {
System.out.println("身體站直, 心態(tài)穩(wěn)住");
}
@Override
protected void shoot() {
System.out.println("掏出沖鋒槍打他");
}
@Override
protected Range<Integer> queryDistanceRange() {
return new Range<>(20, 100);
}
}
定義一個(gè)上下文類來(lái)對(duì)入?yún)⑦M(jìn)行路由:
/**
* 策略上下文, 用來(lái)路由策略
*/
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)用就好啦:
public class App {
public static void main(String[] args) {
StrategyContext.execute(89);
}
}
結(jié)果如下:
距離敵人 89
發(fā)現(xiàn)敵人
身體站直,心態(tài)穩(wěn)住
掏出沖鋒槍打他
類圖如下: