Spring中實現策略模式示例

banq發表於2024-02-23

在本教程中,將探索 Spring 框架中的各種策略模式實現,例如列表注入、對映注入和方法注入。

什麼是策略模式?
策略模式是一種設計原則,允許您在執行時切換不同的演算法或行為。它允許您在不改變應用程式核心邏輯的情況下插入不同的策略,從而使您的程式碼具有靈活性和適應性。

這種方法適用於為特定功能任務提供不同實現方式,並使系統更能適應變化的情況。它透過將演算法細節與應用程式的主要邏輯分離,促進了更模組化的程式碼結構。

步驟 1:實施策略
把自己想象成一個黑暗巫師,努力與春天一起掌握不可饒恕詛咒的力量。我們的任務是實現所有三種詛咒--Avada Kedavra、Crucio 和 Imperio。之後,我們將在執行時切換不同的詛咒(策略)。

讓我們從策略介面開始:

public interface CurseStrategy {  

    String useCurse();

    String curseName();
}

下一步,我們需要執行所有 "不可饒恕的詛咒":

@Component  
public class CruciatusCurseStrategy implements CurseStrategy {  
  
    @Override  
    public String useCurse() {  
        return <font>"Attack with Crucio!";  
    }  
  
    @Override  
    public String curseName() {  
        return
"Crucio";  
    }  
}


@Component  
public class ImperiusCurseStrategy implements CurseStrategy {  
  
    @Override  
    public String useCurse() {  
        return
"Attack with Imperio!";  
    }  
  
    @Override  
    public String curseName() {  
        return
"Imperio";  
    }  
}

@Component  
public class KillingCurseStrategy implements CurseStrategy {  
  
    @Override  
    public String useCurse() {  
        return
"Attack with Avada Kedavra!";  
    }  
  
    @Override  
    public String curseName() {  
        return
"Avada Kedavra";  
    }  
}

第 2 步:將詛咒注入 List
Spring 提供了一個神奇的功能,允許我們以 List 的形式注入一個介面的多個實現,這樣我們就可以用它來注入策略並在它們之間切換。

但讓我們先建立基礎:Wizard介面。

public interface Wizard {  
    String castCurse(String name); 
}

我們可以在嚮導中注入我們的詛咒(策略),並篩選出所需的詛咒。

@Service  
public class DarkArtsWizard implements Wizard {  
  
    private final List<CurseStrategy> curses;  
  
    public DarkArtsListWizard(List<CurseStrategy> curses) {  
        this.curses = curses;  
    }  
  
    @Override  
    public String castCurse(String name) {  
        return curses.stream()  
            .filter(s -> name.equals(s.curseName()))  
            .findFirst()  
            .orElseThrow(UnsupportedCurseException::new)  
            .useCurse();  
    }  
}

如果請求的詛咒不存在,也會產生 UnsupportedCurseException。

public class UnsupportedCurseException extends RuntimeException {  
}

測試
我們可以驗證詛咒施放是否有效:

@SpringBootTest  
class DarkArtsWizardTest {  
  
    @Autowired  
    private DarkArtsWizard wizard;  
  
    @Test  
    public void castCurseCrucio() {  
        assertEquals(<font>"Attack with Crucio!", wizard.castCurse("Crucio"));  
    }  
  
    @Test  
    public void castCurseImperio() {  
        assertEquals(
"Attack with Imperio!", wizard.castCurse("Imperio"));  
    }  
  
    @Test  
    public void castCurseAvadaKedavra() {  
        assertEquals(
"Attack with Avada Kedavra!", wizard.castCurse("Avada Kedavra"));  
    }  
  
    @Test  
    public void castCurseExpelliarmus() {  
        assertThrows(UnsupportedCurseException.class, () -> wizard.castCurse(
"Abrakadabra"));  
    }  
}


另一種流行的方法是定義 canUse 方法,而不是 curseName。這將返回布林值,並允許我們使用更復雜的過濾功能,例如

public interface CurseStrategy {  

    String useCurse();

    boolean canUse(String name, String wizardType);
}

@Component  
public class CruciatusCurseStrategy implements CurseStrategy {  
  
    @Override  
    public String useCurse() {  
        return <font>"Attack with Crucio!";  
    }  
  
    @Override  
    public boolean canUse(String name, String wizardType) {  
        return
"Crucio".equals(name) && "Dark".equals(wizardType);  
    }  
}

@Service  
public class DarkArtstWizard implements Wizard {  
  
    private final List<CurseStrategy> curses;  
  
    public DarkArtsListWizard(List<CurseStrategy> curses) {  
        this.curses = curses;  
    }  
  
    @Override  
    public String castCurse(String name) {  
        return curses.stream()  
            .filter(s -> s.canUse(name,
"Dark")))  
            .findFirst()  
            .orElseThrow(UnsupportedCurseException::new)  
            .useCurse();  
    }  
}

步驟 3:將策略注入Map
我們可以輕鬆解決上一節中的弊端。Spring 允許我們將 Bean 名稱和例項注入 Map。它簡化了程式碼並提高了效率。

@Service  
public class DarkArtsWizard implements Wizard {  
  
    private final Map<String, CurseStrategy> curses;  
  
    public DarkArtsMapWizard(Map<String, CurseStrategy> curses) {  
        this.curses = curses;  
    }  
  
    @Override  
    public String castCurse(String name) {  
        CurseStrategy curse = curses.get(name);  
        if (curse == null) {  
            throw new UnsupportedCurseException();  
        }  
        return curse.useCurse();  
    }  
}

這種方法有一個缺點:Spring 會注入 Bean 名稱作為 Map 的鍵,因此策略名稱與 Bean 名稱相同,如 cruciatusCurseStrategy。如果 Spring 的程式碼或我們的類名在未通知的情況下發生變化,這種對 Spring 內部 Bean 名稱的依賴可能會導致問題。

讓我們檢查一下,我們是否仍能施放這些詛咒:

@SpringBootTest  
class DarkArtsWizardTest {  
  
    @Autowired  
    private DarkArtsWizard wizard;  
  
    @Test  
    public void castCurseCrucio() {  
        assertEquals(<font>"Attack with Crucio!", wizard.castCurse("cruciatusCurseStrategy"));  
    }  
  
    @Test  
    public void castCurseImperio() {  
        assertEquals(
"Attack with Imperio!", wizard.castCurse("imperiusCurseStrategy"));  
    }  
  
    @Test  
    public void castCurseAvadaKedavra() {  
        assertEquals(
"Attack with Avada Kedavra!", wizard.castCurse("killingCurseStrategy"));  
    }  
  
    @Test  
    public void castCurseExpelliarmus() {  
        assertThrows(UnsupportedCurseException.class, () -> wizard.castCurse(
"Crucio"));  
    }  
}

  • 優點:無迴圈。
  • 缺點:依賴於 Bean 名稱,這使得程式碼的可維護性較差,並且在名稱更改或重構時更容易出錯。

步驟 4:注入 List 並將其轉換為 Map
如果我們注入 List 並將其轉換為 Map,就可以輕鬆消除 Map 注入的弊端:

@Service  
public class DarkArtsWizard implements Wizard {  
  
    private final Map<String, CurseStrategy> curses;  
  
    public DarkArtsMapWizard(List<CurseStrategy> curses) {  
        this.curses = curses.stream()  
            .collect(Collectors.toMap(CurseStrategy::curseName, Function.identity()));
    }  
  
    @Override  
    public String castCurse(String name) {  
        CurseStrategy curse = curses.get(name);  
        if (curse == null) {  
            throw new UnsupportedCurseException();  
        }  
        return curse.useCurse();  
    }  
}

有了這種方法,我們就可以使用 curseName 代替 Spring 的 Bean 名稱作為 Map 鍵(策略名稱)。

步驟 5:介面中的 @Autowire
Spring 支援在方法中自動佈線。自動連線到方法的簡單示例是透過設定器注入。此功能允許我們在介面的預設方法中使用 @Autowired,這樣我們就可以在嚮導介面中註冊每個 CurseStrategy,而無需在每個策略實現中實現註冊方法。

讓我們透過新增 registerCurse 方法來更新Wizard介面:

public interface Wizard {  
  
    String castCurse(String name);  
  
    void registerCurse(String curseName, CurseStrategy curse)
}
@Service  
public class DarkArtsWizard implements Wizard {  
  
    private final Map<String, CurseStrategy> curses = new HashMap<>();  
  
    @Override  
    public String castCurse(String name) {  
        CurseStrategy curse = curses.get(name);  
        if (curse == null) {  
            throw new UnsupportedCurseException();  
        }  
        return curse.useCurse();  
    }  
  
    @Override  
    public void registerCurse(String curseName, CurseStrategy curse) {  
        curses.put(curseName, curse);  
    }  
}


現在,讓我們透過新增帶有 @Autowired 註解的方法來更新 CurseStrategy 介面:

public interface CurseStrategy {  
  
    String useCurse();  
  
    String curseName();  
  
    @Autowired  
    default void registerMe(Wizard wizard) {  
        wizard.registerCurse(curseName(), this);  
    }  
}

在注入依賴項的同時,我們將詛咒註冊到嚮導中。

  • 優點:沒有迴圈,也不依賴內部 Spring Bean 名稱。
  • 缺點:沒有缺點,純粹的黑魔法。

結論
在本文中,我們探討了 Spring 環境中的策略模式。我們評估了不同的策略注入方法,並演示了使用 Spring 功能的最佳化解決方案。

 

相關文章