我曾经非常喜欢用继承解决问题,一来继承是Java的四大特性之一,经常性的使用可令我更熟知其真意;二来也确实吃了继承的红利,少写了许多重复代码,使我更加乐此不疲。但继承不是银弹,一味地使用也确实会暴露些许问题,例如继承了本不该具有的内容… …
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午6:24
* @Description: 人类
*/
public class Human {
/**
* 行走
*/
public void walk() {
System.out.println("行走。");
}
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:18
* @Description: 真人类
*/
public class RealHuman extends Human {
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:24
* @Description: 假人类
*/
public class FakeHuman extends Human {
}
RealHuman与FakeHuman都是Human的子类,并继承了walk方法,这看似正确实则内涵隐患。
继承在复用的同时,往往也使子类获取了不合理(不该有/仅限于单个)的行为。这是继承一个很大的缺点,对此开发者往往会重写子类方法,这也是最直接的处理方案。
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:18
* @Description: 真人类
*/
public class RealHuman extends Human {
/**
* 行走
*/
@Override
public void walk() {
System.out.println("优雅行走。");
}
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:24
* @Description: 假人类
*/
public class FakeHuman extends Human {
/**
* 行走
*/
@Override
public void walk() {
System.out.println("不会行走。");
}
}
重写无法掩盖子类拥有行为的事实。通过重写子类方法,我们看似完美解决了子类继承行为不合理的问题… …RealHuman开拓了新的walk行为,FakeHuman关闭了walk行为… …但这种完美是虚假的,重写无法掩盖子类拥有行为的事实,“不会行走”本身也是walk行为的具体化实现。而我们期望的是FakeHuman不具备walk行为(即没有walk方法),这是一种没有和残疾的区别。
重写无法很好的应对子类数目十分庞大的情况。重写除了无法掩盖子类拥有行为的事实外还有一个缺陷,当子类数目十分庞大时,重写依然还能很好的处理妥当吗?答案是否定的… …重写的本质是硬编码,在子类数目十分庞大的情况下,无论是维护重写还是重写本身都是庞大的工作量。这一点尤为体现在维护上,例如我想将所有“优雅行走”改为“大步行走”… …我并不认为找到所有的“优雅行走”并修改是一件轻松的事情。
简而言之,继承有利有弊… … 利处发扬,弊处弥补… …而策略模式就很好的弥补了这一点。
策略模式遵循了【封装变化】【多用组合,少用继承】【面向接口编程,而非面向实现编程】三条设计原则。所谓的设计原则是指设计模式通用的一系列指标,事实证明无论是何种风格的设计模式想要实现目的都要达成相应的部分指标,因此一个合格的设计模式或多或少都会遵循其中的若干条,这些设计原则会在后期慢慢提及并讲述。
封装变化。
所谓的封装变化是指找出程序中变化的部分,并将之与不变的部分进行分离并封装。要达成该目的的第一步便是找出程序中变化的部分,这要求你对代码有着一定结构性的认知,是一位有思想的程序员而非机械式敲打键盘的码农… …当然寻找变化也有方法,即专注于寻找程序中需要频繁硬编码的部分。而在本文的案例中,变化的部分显然是身为继承行为walk方法,那么我们如何分离封装他们呢?
将继承行为从父(超)类中剥离。我们试着将walk方法从父(超)类中剪切出来放置一边不顾,你会发现程序没有任何需要变化的地方,这就说明我们找对了位置,分离已经实现,紧接着要向封装迈进。
将继承行为形成自身的类家族。我们需要设计一个行为接口作为整个行为类家族的父(超)类型,并以此为基柱向下延伸各子类型的实现,完成对行为的封装。
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午11:08
* @Description: 行走能力接口
*/
public interface Walkable {
/**
* 行走
*/
void walk();
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午11:11
* @Description: 优雅行走类
*/
public class ElegantWalk implements Walkable {
/**
* 行走
*/
public void walk() {
System.out.println("优雅行走。");
}
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午11:12
* @Description: 大步行走类
*/
public class StrideWalk implements Walkable {
/**
* 行走
*/
public void walk() {
System.out.println("大步行走。");
}
}
多用组合,少用继承。
通过组合获取行为。继承行为被剥离出父(超)类后,子类也同步失去相关行为。策略模式推荐使用组合来获取行为,以解决子类继承不合理(不该有/仅限于单个)行为的问题 —— 只让应该拥有行为的类获取到匹配的行为。
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午6:24
* @Description: 人类
*/
public class Human {
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:18
* @Description: 真人类
*/
public class RealHuman extends Human {
/**
* 优雅行走类对象
*/
private ElegantWalk elegantWalk;
public ElegantWalk getElegantWalk() {
return elegantWalk;
}
public void setElegantWalk(ElegantWalk elegantWalk) {
this.elegantWalk = elegantWalk;
}
}
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:24
* @Description: 假人类
*/
public class FakeHuman extends Human {
/**
* 假人不具备行走行为
*/
}
面向接口编程,而非面向实现编程。
“有一个”可能比“是一个”更好。上述代码中我们通过组合实现了“只让应该拥有行为的类获取到匹配的行为”,我们可以发现这个行为是固定的,但在实际环境中我们往往需要行为可以发生变化,例如从ElegantWalk切换为StrideWalk,当前代码显然无法做到这一点,这就迫使我们必须去修改源代码(具体如下),这和重写一样属于硬编码… …我们真正希望的是“有一个”可变化的行为,而非“是一个”固定的行为… …那有没有更好的方案,可以在不修改源代码的前提下完成对行为的切换呢?有的,就是所谓的面向接口编程。
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:18
* @Description: 真人类
*/
public class RealHuman extends Human {
/**
* 大步行走类对象
*/
private StrideWalk strideWalk;
public StrideWalk getStrideWalk() {
return strideWalk;
}
public void setStrideWalk(StrideWalk strideWalk) {
this.strideWalk = strideWalk;
}
}
所谓面向接口编程的即面向父(超)类编程,具体的表现为使用父(超)类来承接具体的子类实例。面向接口编程最大的优点在于其天生带有“松耦合”的特性,由于使用父(超)类来承接具体的子类实例的缘故,承接类型不再局限于某个具体的子类,而是可以在整个父(超)类家族中随意选择,这就为实现动态行为切换提供了可能… …让我们修改代码。
/**
* @Author: 说淑人
* @Date: 2022/3/27 下午7:18
* @Description: 真人类
*/
public class RealHuman extends Human {
/**
* 行走能力接口对象
*/
private Walkable walkable;
public Walkable getWalkable() {
return walkable;
}
public void setWalkable(Walkable walkable) {
this.walkable = walkable;
}
}
现在让我们体会下面向接口编程的好处。
/**
* @Author: 说淑人
* @Date: 2022/3/29 下午5:31
* @Description: 主类
*/
public class Main {
public static void main(String[] args) {
RealHuman realHuman = new RealHuman();
ElegantWalk elegantWalk = new ElegantWalk();
// 设置为优雅走路。
realHuman.setWalkable(elegantWalk);
realHuman.doSomething();
StrideWalk strideWalk = new StrideWalk();
// 设置为大步走路。
realHuman.setWalkable(strideWalk);
realHuman.doSomething();
// 如果说上述代码仍然有硬编码的嫌疑,那可以参考这一段随机设置。
List<Walkable> walkables = new ArrayList<Walkable>();
walkables.add(elegantWalk);
walkables.add(strideWalk);
Random random = new Random();
realHuman.setWalkable(walkables.get(random.nextInt(2)));
realHuman.doSomething();
}
}
不一定是接口。面向接口编程并不要求父(超)类一定是接口,这表示无论是接口、抽象类或一般类,只要作为父(超)类使用就都符合面向接口编程的思想。只是在此之中我们首推接口,因为Java语言中接口允许多实现,然类只允许单继承。
说到这里,有关策略模式的内容基本上已经全部讲完了,这个过程中我们封装了变化,使用了组合以及使用了面向对象编程,那这一切都是为了什么呢?或者换一种问法… …策略模式的核心目标是什么呢?答案是运行时拓展。
运行时拓展是一种不依赖静态编译,通过在运行期间动态的变化来完成功能的编程思想。相信在学习了策略模式之后对于该描述多多少少能够有所体会… …文中提及的继承重写及组合改写就属于典型依赖静态编译的场景,这种通过编译硬编码后的代码来完成功能的方式是不被主流所提倡的… …而我们最终也借助【封装变化】【多用组合,少用继承】【面向接口编程,而非面向实现编程】三条设计原则实现了其向运行时拓展的转变。以运行时拓展为核心思想的设计模式并不少,包括后期要讲述的观察者模式、装饰者模式也都是如此。
策略模式是为了拓展继承,而不是替代继承。学习了策略模式后,不要有“继承可有可无”的错觉,原因有以下几点:
继承与策略模式相辅相成,一个复用性强,一个更加灵活。因此在适当的场景下,请“放肆”的使用继承。
【上篇】《设计模式【1】简介》
【下篇】《设计模式【3】观察者模式》