工厂模式与策略模式通常可以配合使用,成对出现,也是初学设计模式时常常分不清二者区别的原因。
业务场景需要根据传入的不同参数以获取对应的对象来处理各自的逻辑,很容易写出如下代码:
void doSomeThing(String name) {
if (name.equals("张三")) {
new ZhangSan();
} else if (name.equals("李四")) {
new LiSi();
} else if (name.equals("王五")) {
new WangWu();
} else if (name.equals("赵六")) {
// ...
}
// ...
execute();
}
显然这堆if…else…非核心代码,却占据了大量的篇幅,非常影响突出核心代码的可读性。
那么可使用简单工厂(并不算设计模式)将繁琐的判断逻辑单独封装起来,后续需求变更也只需要从工厂中修改代码即可:
public class Factory {
public static Object getBean(String name) {
if (name.equals("张三")) {
return new ZhangSan();
} else if (name.equals("李四")) {
return new LiSi();
} else if (name.equals("王五")) {
return new WangWu();
} else if (name.equals("赵六")) {
// ...
}
}
}
那么业务主体的代码就可以是:
void doSomeThing(String name) {
Factory.getBean(String name);
// ...
execute();
}
可以发现简化了代码逻辑,便于聚焦于核心代码。
但是上述也有新的问题。
对象创建是在工厂中根据参数固定了对应关系的,当应用中本来是是使用"张三",但是现在换成"李四",还是需要在这堆if…else…中修改对应关系。
那么我们可以创建对象的时机推迟到应用中,在工厂中只提供返回父类对象的抽象方法,各应用自行实现:
public abstract class Factory {
abstract Person getBean();
// ...
public void execute(){
Person person = getBean();
person.execute();
}
}
每个类都继承Factory并实现他的抽象方法:
public class ZhangSanFactory extends Factory{
public Person getBean(){
return new ZhangSan();
}
}
public class LiSiFactory extends Factory{
public Person getBean(){
return new new LiSi();
}
}
这样在应用中使用"张三"时:
void doSomeThing() {
Factory factory = new ZhangSanFactory();
Person person = factory.getBean();
// ...
person.execute();
}
如果有天需求变更要使用李四,只需要将 new ZhangSanFactory() 改为 new LiSiFactory(),而其他代码都不会影响。(就是利用多态)
void doSomeThing() {
Factory factory = new LiSiFactory();
Person person = factory.getBean();
// ...
person.execute();
}
某种程度上说工厂方法不能替代简单工厂,简单工厂是动态的接收参数以确定使用"张三"还是"李四",在同一时间"张三李四"都有可能在使用,是兼容动态参数变更;工厂方法是在上一层逻辑里已经明确了目前这段逻辑要么"张三"要么"李四",是兼容了需求角度变更。
java.util.Calendar#getInstance()中就有简单工厂:
private static Calendar createCalendar(TimeZone zone,Locale aLocale) {
// ...
switch (caltype) {
case "buddhist":
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese":
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "gregory":
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
java.text.NumberFormat#getInstance()也是简单工厂:
private static NumberFormat getInstance(LocaleProviderAdapter adapter,Locale locale, int choice) {
NumberFormatProvider provider = adapter.getNumberFormatProvider();
NumberFormat numberFormat = null;
switch (choice) {
case NUMBERSTYLE:
numberFormat = provider.getNumberInstance(locale);
break;
case PERCENTSTYLE:
numberFormat = provider.getPercentInstance(locale);
break;
case CURRENCYSTYLE:
numberFormat = provider.getCurrencyInstance(locale);
break;
case INTEGERSTYLE:
numberFormat = provider.getIntegerInstance(locale);
break;
}
return numberFormat;
}
工厂方法适用业务系统,根据需求的变更设计兼容性。(无法预料需求变更规则,只能将具体实现延迟)
简单工厂使用包括基础框架和业务系统,是将所有变化的情况都考虑并对应出相应的解决方案,以便下游程序取用所有场景。
应对"变与不变"中的不同角度的"变"。
抽象工厂看起来像工厂方法上多了一套模板方法,把多个工厂方法按模板方案组合起来,比工厂方法更加抽象,适应更复杂的业务场景。
其实Spring获取对象就是使用工厂模式。当需要使用某一个受Spring管理的对象,可以使用注解自动注入,那么Spring根据对象类型/名称,去Jvm里找到对应的这个对象的过程,就是一个工厂模式。
简单工厂是动态的接收参数以确定使用"张三"还是"李四",在同一时间"张三李四"都有可能在使用,是兼容动态参数变更;
工厂方法是在上一层逻辑里已经明确了目前这段逻辑要么"张三"要么"李四",是从需求角度兼容了变更;
策略模式是对简单工厂上对需求导致分支结构增加的兼容。
当项目某一天新需求需要增加"田七",那么就需要在这堆if…else…中修改;如果再增加"亢八"…
显然代码耦合度很强,而可读性和可扩展性弱,违反了开闭原则。
将这些不同的分支进一步抽象成一个一个不同的策略,并在容器加载时就把他们全部存到一个Map,再按需取用,如果需求需要增加,那么只用新增一个对应的策略类也会自动注册到Map,而不涉及在原代码上修改。(这不是策略模式啊…)
本质上就是利用Map对key值的hash计算来get出相应对象,来消除代码里的if…else…
继承 InitializingBean,实现afterPropertiesSet()方法。
那么张三的策略类:
@Component
public class ZhangSanHandler extends InitializingBean {
@Override
public void AAA(String name) {
// 业务逻辑A
System.out.println("张三来了");
}
// afterPropertiesSet()方法,在初始化bean的时候执行
@Override
public void afterPropertiesSet() throws Exception {
Factory.register("张三", this);
}
}
李四的策略类:
@Component
public class LiSiHandler extends InitializingBean {
@Override
public void AAA(String name) {
// 业务逻辑A
System.out.println("李四来了");
}
@Override
public void afterPropertiesSet() throws Exception {
Factory.register("李四", this);
}
}
同理王五、赵六、田七…
对应将上述工厂类中if…else…修改为:
public class Factory2 {
private static Map<String, Handler> strategyMap = Maps.newHashMap();
public static Handler getInvokeStrategy(String name) {
return strategyMap.get(name);
}
public static void register(String name, Handler handler) {
if (StringUtils.isEmpty(name) || null == handler) {
return;
}
strategyMap.put(name, handler);
}
}
通过不同的策略类在Spring初始化bean时自动调用了各自类中的afterPropertiesSet()方法,完成将对象put到工厂中准备好的strategyMap里,那么在使用时就可以直接根据Map的key值get到各自对应的策略类,消除了if…else…
继承ApplicationContextAware,实现setApplicationContext(ApplicationContext applicationContext)方法。
原理和方式一相同,也是利用Spring的生命周期,将所有策略类添加到准备好的Map容器中,在使用时根据key值获取到各自的策略类即可。
@Component
public class Factory3 implements ApplicationContextAware {
private static Map<String, AbstractHandler> strategyMap = Maps.newHashMap();
public static AbstractHandler getInvokeStrategy(String str) {
return strategyMap.get(str);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取到所有子类bean,全部加载到strategyMap中
Map<String, AbstractHandler> beans = applicationContext.getBeansOfType(AbstractHandler.class);
beans.values().forEach(bean -> strategyMap.put(bean.getName(),bean));
}
}
策略类:
@Component
public class ZhangSanHandler3 extends AbstractHandler{
@Override
public String getName() { // TODO key可以改为用枚举值
return "张三";
}
}
其他的策略类也是继承自AbstractHandler。才会被通过applicationContext.getBeansOfType(AbstractHandler.class)获取到所有子类Bean。
@Autowired
list<AbstractHandler> list
@postconstruct
public void register() {
// 遍历list,把每个handler放入map
}
在下篇责任链模式也见过这样的。
对于以上ZhangSanHandler、LiSiHandler、WangWuHandler…,他们同样继承自AbstractHandler,其实还能接着使用模版方法模式。
将不同策略共有的逻辑方法放到抽象父类中,消除重复代码,而各子类完成各自特有的逻辑,又降低了耦合。
工厂+模版方法通常也是可以配合使用的。
工厂模式与策略模式最大的区别:实例化一个对象的位置不同。
对工厂模式而言,实例化对象是放在服务端的,即放在了工厂类里面,同时固定了对象和这个对象被何总取用方式的逻辑;
策略模式是不对需要实例化的策略对象做实现,也是交给客户端,在运行时动态传入实例化对象。
有点类似代理模式与装饰者模式的区别。
策略模式的定义:
策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。是一种可插拔可替换的设计。
简单实现一个策略:(使用起来更像装饰者)
class CarContext {
private AudiCar audiCar;
public CarContext(AudiCar audiCar) {
this.audiCar = audiCar;
}
public void orderCar(){
this.audiCar.makeCar();
}
}
//客户 --客户端(这个客户是内行,直接拿来了A6的图纸就可以生产,销售部门不管是什么图纸,也不用懂)
public class SimplyFactoryAndStrategy2 {
public static void main(String[] args) {
//客户说我要什么什么样子的车子,销售人员才知道他要什么样子的车子
AudiCar car = new AudiA6("a6");
CarContext context = new CarContext(car);
context.orderCar();
}
}
new CarContext(new AudiA6(“a6”)) 代码看起来装饰者模式更像了,其实他们在客户端使用时的确区别不大;有区别的是在服务端,装饰着是对目标类的行为(方法)做扩展,装饰器是可变化的,策略不对目标类行为做变更而是直接用,但目标类是可替换的。
new BBB(new AAA()) 可能有天需求变更,于是new CCC(new AAA())
new CarContext(new AudiA6("a6")) 可能有天需求变更,于是new CarContext(new AudiA4("a4"))
其实策略模式用在上述简单的场景属实大材小用,只是便于理解。
复杂一点的场景,如植物大战僵尸中各种僵尸:
类型 | 外观 | 移动 | 攻击 |
---|---|---|---|
普通僵尸 | 普通 | 缓慢步行 | 咬 |
旗手僵尸 | 普通 + 举旗 | 缓慢步行 | 咬 |
大头僵尸 | 大头 | 缓慢步行 | 头撞 |
石膏僵尸 | 普通 + 打石膏 | 一瘸一拐 | 投掷物 |
其他僵尸xx | xxx | xxx | xx |
对僵尸对象,外观是他的一种属性(字段),移动和攻击是两种行为(方法)。
对每一种僵尸可以创建一个类:NormalZombie、FlagZombie、BigHeadZombie、LameZombie、XxxZombie…
这些类都用外观属性和移动攻击两个方法。都某一天需求变更了,产品经理说把旗手僵尸的攻击方法也变成投掷物,那么就要在FlagZombie中修改攻击方法的逻辑;或者把普通僵尸以外的所有缓慢步行的僵尸都换成小步快跑,这样就要在这些类里面去一个一个修改移动方法了。
抓住变与不变,变化的是外观、移动、攻击的不同组合,不变的是始终由这三方面组成一个僵尸对象。
那么就可以使用策略模式,将每一种行为的具体方式设计为一种算法并抽象为一类,按需装配。
public abstract class DisPlay {
public String display;
public abstract String getDisplay();
}
public abstract class Moveable {
public abstract void move();
}
public abstract class Attackable {
public abstract void attack();
}
针对外观我们定义一组多个策略:
public class FlagDisplay extends DisPlay{
public String getDisplay() {
System.out.println(display);
return display;
}
public FlagDisplay() {
this.display = "普通 + 举旗";
}
}
public class NormalDisplay extends DisPlay{
public String getDisplay() {
System.out.println(display);
return display;
}
public NormalDisplay() {
this.display = "普通";
}
}
针对移动我们定义一组多个策略:
public class RunMove extends Moveable {
@Override
public void move() {
System.out.println("我跑得飞快!!");
}
}
public class WalkMove extends Moveable {
@Override
public void move() {
System.out.println("我走得很慢..");
}
}
//...
针对攻击我们定义一组多个策略:
public class BiteAttack extends Attackable {
@Override
public void attack() {
System.out.println("我咬..");
}
}
public class BumpAttack extends Attackable {
@Override
public void attack() {
System.out.println("我头撞..");
}
}
public class ShootAttack extends Attackable {
@Override
public void attack() {
System.out.println("我砸过来...");
}
}
准备一个僵尸:
public class Zombie {
public DisPlay display;
public Moveable moveable;
public Attackable attackable;
public Zombie(DisPlay display, Moveable moveable, Attackable attackable) {
this.display = display;
this.moveable = moveable;
this.attackable = attackable;
}
public void start(){
System.out.println("我的外观是:");
display.getDisplay();
move();
attack();
}
void move() {
this.moveable.move();
}
void attack() {
this.attackable.attack();
}
}
测试类:
public static void main(String[] args) {
DisPlay disPlay = new FlagDisplay();
Moveable moveable = new RunMove();
Attackable attackable = new BiteAttack();
Zombie zombie = new Zombie(disPlay, moveable, attackable);
zombie.start();
}
输出:
我的外观是:
普通 + 举旗
我跑得飞快!!
我咬..
构造 Zombie对象时,需要根据它具体的行为算法,就能更具不同的业务需求组合出任意外观+任意移动+任意攻击的 Zombi。
例如今天想让这头僵尸移动方式改为缓慢步行,那么只需要将new RunMove() 改为 new WalkMove(),便组合出一个外观是举旗的、移动是走的很慢、攻击是咬的Zombie。
再例如后续要增加一种外观是长翅膀的僵尸,移动是飞过来… 那也分别继承DisPlay、Moveable创建新的算法即可,并不会对原来的代码做修改。
当然,有某些Zombie是固定了三种特征的,也可以将Zombie进一步抽象,再创建好NormalZombie、FlagZombie、BigHeadZombie…继承Zombie并各自定义出了外观、移动和攻击,客户端就可以绕过对外观、移动和攻击对象的创建而直接使用某种具象的Zombie(回到了与工厂模式结合)。
策略模式把各种行为抽象为一个一个独立的算法策略,客户端使用时可以灵活的选取、组合、替换不同的算法。–应对捉摸不定的需求变更。
客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。