这个话题网上已经有很多解答,这里要做的仅仅是稍微归纳下。如果想略过推理的部分,请直接看向文章末尾。
当我们针对不同的情况,需要使用不同的逻辑时,就需要使用到策略模式。其本质是一种模式匹配:将模式映射到逻辑。伪代码如下所示:
interface Strategy {
void doWork()
}
class A implements Strategy {..}
class B implements Strategy {..}
// 默认策略
class C implements Strategy {..}
swith(code) {
case "A":
// 一种实现
a.doWork()
case "B":
// 另一种实现
b.doWork()
default:
c.doWork()
}
显然,如果每新增一种策略,匹配的代码将不断增加,这是一种重复,同时也不符合“对修改关闭,对拓展开放”的开闭原则。
我们首先使用重构手法,消除上述代码的重复:
Map<String, Strategy> map = new HashMap<>()
map.put("A", a)
map.put("B", b)
// 使用
Strategy s = MapUtils.getOrDefault(map, condition, c)
s.doWork()
这里可以看到,重复还是存在的(map的put会不断增加),但我们成功地干掉了swich,或是更恐怖的if。
更进一步,我们应该灵光一闪,让程序干他最擅长的事情:循环。于是,提取重复的部分到for循环中:
Map<String, Strategy> stategies = new HashMap<>()
// 默认策略
C c = new C()
void init() {
List<Strategy> allStrategies = Arrays.asList(a, b)
for(s in allStrategies) {
map.put(s.getCode(), s)
}
}
Strategy getStrategy(String code) {
MapUtils.getOrDefault(map, code, c)
}
很自然的,我们需要给接口添加getCode方法,让实现类知道更多的细节,承担更多的职责,毕竟面向对象的一大抉择就是该谁承担责任!
现在重复解决了,我们只需要不断延长allStrategies列表即可,但是开闭原则的问题还没解决。究其原因,是因为我们自己管理了allStrategies这个依赖,解决方案是“控制反转”,将依赖交给容器注入。
在经典的spring中,我们在xml里配置一个list,并将依赖bean都添加进去,再将list整体注入。现在我们早已远离配置,网上给出了解决方案:
@Autowired
ApplicationContext context
List<Strategy> stategies = context.getBeansOfType(Strategy.class)
这很好,但是我们依赖了spring的api,这违背了spring设计的初衷:一种非侵入式的容器实现。查看spring的源码,DefaultListableBeanFactory,我们还可以进一步简化:
@Autowired
List<Strategy> stategies;
此外,还可以直接注入Map形式的bean:
@Autowired
Map<String, Strategy> strategies;
将以bean的名字为键进行注入。这样可以助我们删掉getCode方法!
有时我们需要注入有某些注解的bean,怎么办呢?这个时候只能使用spring api了:
@Autowired
ApplicationContext context
List<Strategy> strategies = context.getBeansWithAnnotation(Strategy.class)
这里有读者会觉得,策略模式有必要写这么多过程么?又不是做重构!没错,我们就是在重构。在实际编码中,你面对的可能就是最初的那版if…else的写法,为了自身安全,不影响原有逻辑,需要这种小步快跑的安全模式。
另外,从重构开始,到模式结束,这是个很自然的过程,重构的终极用途就在于此。
附:
控制反转的理解
网上基本没有解释清楚这个概念的,有的说用了容器就是控制反转,有的说依赖注入就是控制反转,让人深感头疼。这里综述一下,对各种说法来一个总结,彻底说清楚到底谁控制了谁,哪里被反转。
按照传统的模式,我们有三个类,A依赖于B,B依赖于C,那么在类B中,一般会new一个C的对象,同理,A中会new一个B的对象,配合完成逻辑。
这样,依赖就是自上而下的,控制也是A -> B -> C。从实现上看,是B负责了C的创建工作(控制了C)。
假如我们换一种方式,在A中new好B和C,并且将C注入到B中,例如,使用B的setB方法,这就是setter依赖注入。这里要注意,依赖注入只是控制反转的一种实现方式。
更进一步,我们将构建B和C的意图,以及B依赖于C的意图,都存入到一个配置文件中,然后由A按照要求构造和注入。例如,配置文件可以是一个xml形式的bean定义文件,正如spring所做的那样。
那么,从控制流来看,是不是有:
B让A构造自己;
C让A构造自己;
B让A将C注入到自身。
由此我们是不是就有B -> A,以及C -> A。从效果来看,B和C间接控制了A的行为,控制流得到了反转。或者也可以说:Dont call me, i will call you。
如果将A当做容器,这不就是spring了么。
再进一步,如果C是一个接口,那么,B就与C的具体实现解耦了。这就符合了面向接口编程的原则,也能进一步实现开闭原则。