在本例中,我们将学习中的策略模式春天。我们将介绍注入策略的不同方法,从简单的基于列表的方法到更有效的基于地图的方法。为了说明这个概念,我们将使用《哈利·波特》系列中的三个不可饶恕咒——阿瓦达·凯达维拉、克鲁西奥和因佩里奥。
战略模式是什么?
战略模式是一种设计原则,允许您在运行时在不同的算法或行为之间切换。它允许您在不改变应用程序核心逻辑的情况下插入不同的策略,从而使您的代码具有灵活性和适应性。
这种方法在特定功能任务有不同实现并希望系统更能适应变化的情况下非常有用。它通过将算法细节从应用程序的主要逻辑中分离出来,促进了更模块化的代码结构。
步骤1:实施战略
想象自己是一个黑巫师,努力掌握不可饶恕咒的力量春天。我们的任务是实施所有三个诅咒——阿瓦达·凯达维拉、克鲁西奥和英佩里奥。之后,我们将在运行时切换诅咒(策略)。
让我们从我们的策略界面开始:
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";
}
}
第二步:注入诅咒列表
春天带来了一点魔力,允许我们将一个接口的多个实现作为一个列表注入,这样我们就可以使用它来注入策略并在它们之间切换。
但是让我们首先创建基础:向导界面。
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。这将会回来boolean并允许我们使用更复杂的过滤方法,如:
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:将策略作为映射注入
我们可以很容易地解决上一节的缺点。Spring让我们用bean名称和实例。它简化了代码并提高了效率。
@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,则很容易消除:
@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而不是春天的豆类名称Map关键字(策略名称)。
步骤5:@界面中的自动连线
弹簧支架自动布线转化为方法。自动连接到方法的简单示例是通过setter注入。该功能允许我们使用@Autowired在一个default方法,这样我们就可以注册每个CurseStrategy在……里Wizard接口,而无需在每个策略实现中实现注册方法。
让我们更新Wizard接口添加一个registerCurse方法:
public interface Wizard {
String castCurse(String name);
void registerCurse(String curseName, CurseStrategy curse)
}
这是Wizard实施:
@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);
}
}
现在,让我们更新CurseStrategy接口,方法是添加一个带有@Autowired注释:
public interface CurseStrategy {
String useCurse();
String curseName();
@Autowired
default void registerMe(Wizard wizard) {
wizard.registerCurse(curseName(), this);
}
}
在注入依赖项的时候,我们将我们的诅咒注册到Wizard.
优点:没有循环,也不依赖内部Spring bean名称。
缺点:没有骗局,纯粹的黑魔法。
结论
在本文中,我们探讨了Spring背景下的策略模式。我们评估了不同的策略注入方法,并使用Spring的功能演示了一个优化的解决方案。