Spring中实现策略模式示例

在本教程中,将探索 Spring 框架中的各种策略模式实现,例如列表注入、映射注入和方法注入。

什么是策略模式?
策略模式是一种设计原则,允许您在运行时切换不同的算法或行为。它允许您在不改变应用程序核心逻辑的情况下插入不同的策略,从而使您的代码具有灵活性和适应性。

这种方法适用于为特定功能任务提供不同实现方式,并使系统更能适应变化的情况。它通过将算法细节与应用程序的主要逻辑分离,促进了更模块化的代码结构。

步骤 1:实施策略
把自己想象成一个黑暗巫师,努力与春天一起掌握不可饶恕诅咒的力量。我们的任务是实现所有三种诅咒--Avada Kedavra、Crucio 和 Imperio。之后,我们将在运行时切换不同的诅咒(策略)。

让我们从策略接口开始:

public interface CurseStrategy {  

    String useCurse();

    String curseName();
}

下一步,我们需要执行所有 "不可饶恕的诅咒":

@Component  
public class CruciatusCurseStrategy implements CurseStrategy {  
  
    @Override  
    public String useCurse() {  
        return "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("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 "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("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 功能的优化解决方案。