最近看了Head First 设计模式一书,开篇的故事讲述了设计模式的原则:封装变化与面向接口编程.
故事从编写一个模拟鸭子的游戏开始,游戏要求:
游戏里有许多鸭子,一边游泳戏水,一边呱呱叫…
该游戏内部使用面向对象设计,有一个鸭子的超类Duck:
public abstract class Duck{
public void swim(){
//游泳的方法
}
public void quack(){
//呱呱叫的方法
}
public abstract void display(){
//子类要实现的显示的方法
}
}
因为所有的鸭子都会游泳和叫,所以在超类中实现了swim()和qucak()方法,而具体显示出什么样和具体的鸭子有关,所以display()方法为抽象方法.
现在有种鸭子是红头鸭RedHeadDuck和绿头鸭MallardDuck.
红头RedHeadDuck代码:
public class RedHeadDuck extends Duck {
public void display() {
System.out.println("我是红头鸭...");
}
}
绿头鸭MallardDuck代码:
public class MallardDuck extends Duck {
public void display() {
System.out.println("我是绿头鸭...");
}
}
现在需求发生了变化,想要鸭子能飞行…那不是很简单嘛,给Duck类加个飞行的方法不就可以了,如下:
public abstract class Duck{
public void swim(){
//游泳的方法
}
public void quack(){
//呱呱叫的方法
}
public void fly(){
//飞行的方法
}
public abstract void display(){
//子类要实现的显示的方法
}
}
这样一来,确实绿头鸭和红头鸭都会飞行了.
由于公司业务需要,增加橡皮鸭这一角色RubberDuck,如下:
public class RubberDuck extends Duck {
public void display() {
System.out.println("我是橡皮鸭...");
}
}
等等,上面的橡皮鸭貌似不对啊,橡皮鸭不会飞啊!而且橡皮鸭是吱吱叫不是呱呱叫.这该怎么办呢?
这还不简单,直接覆盖方法不就行了.
public class RubberDuck extends Duck {
public void qucak(){
//吱吱叫...
}
public void fly(){
//什么也不做...
}
public void display() {
System.out.println("我是橡皮鸭...");
}
}
这样貌似是解决了,但是问题又来来,如果后来需要增加诱饵鸭DecoyDuck,诱饵鸭不会叫不会飞.怎么办?难道还要继续覆盖方法么?
既然无法确定以后的鸭子是什么类型,干脆抽取公共的部分,不同的写成接口.
比如会飞的实现Flyable接口,会叫的实现Qucakable接口.
//会飞的接口
public interface Flyable{
void fly();
}
//会叫的接口
public interface Quackable{
void quack();
}
//新的Duck类
public abstract class Duck{
public void swim(){
//游泳的方法
}
public abstract class display(){
//显示的方法
}
}
//新的绿头鸭
public class MallardDuck extend Duck implements Flyable, Qucakable {
public void fly(){
//我会飞...
}
public void quack(){
//我会呱呱叫...
}
public void display(){
//我是绿头鸭
}
}
//新的红头鸭类
public class RedHeadDuck extend Duck implements Flyable, Qucakable {
public void fly(){
//我会飞...
}
public void quack(){
//我会呱呱叫...
}
public void display(){
//我是红头鸭
}
}
//橡皮鸭
public class RubberDuck extend Duck implements Quackable {
public void quack(){
//我会吱吱叫...
}
public void display(){
//我是橡皮鸭
}
}
//诱饵鸭
public class DecoyDuck extends Duck {
public void display(){
//我是诱饵鸭
}
}
这样一来,问题就解决了.
上面的问题是解决了,好像代码有重复:
绿头鸭和红头鸭的会飞的方法和会呱呱叫的方法是重复的.
如果以后有更多类型的方法,重复的代码会更多,而且会埋下一个隐患:
如果以后飞行的动作有所改变,难道一个一个类的去修改?
如果需求还有变化,不是更难维护吗?
有没有好的方法解决这个问题呢?答案是肯定的.我们需要将代码中的变化的部分与不变的部分拆分出来.这就是封装变化的原则
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
下面就建立两组类,变化的和不会变化的.
上面的案例中什么是变化的呢?
飞行和叫声是变化的.那么就将飞行和叫声与Duck类分开.
如何设计鸭子的飞行行为和叫声行为呢?
我们希望一切有弹性,因为你无法确定以后的飞行行为会有什么变化,也无法确定以后的绿头鸭会有什么行为.
这就涉及到第二个原则:面向接口编程
针对接口编程,而不是针对实现编程
那么现在的需求有两个行为:飞和叫.
接口就为飞行行为接口和叫的行为接口:
//飞行行为接口
public interface FlyBehaviour{
void fly();
}
//叫的行为接口
public interface QuackBehaviour{
void quack();
}
现在飞行有种不同的行为:飞和不会飞.
//普通的飞
public class FlyWithWings implements FlyBehaviour {
public void fly(){
System.out.println("我会飞...");
}
}
//不会飞
public class FlyNoWay implements FlyBehaviour {
public void fly(){
//我不会飞...
}
}
现在叫也有三种行为:呱呱叫和吱吱叫和不会叫
//呱呱叫
public class Quack implements QuackBehaviour {
public void quack(){
System.out.println("我会呱呱叫...");
}
}
//吱吱叫
public class Squack implements QuackBehaviour {
public void quack(){
System.out.println("我会吱吱叫...");
}
}
//不会叫
public class MuteQuack implements QuackBehaviour {
public void quack(){
//我不会叫...
}
}
这样写的好处就在于,使用飞行行为时只需指定会飞行,不需绑定具体飞行的动作,弹性空间较大.而且此处的面向接口编程,并不是狭义上指Java中的接口,而是指超类型,可以是接口也可以是抽象类.
那么如何将行为和Duck类组合到一起呢?
将行为转为属性
即将飞行和叫的行转为鸭子的一个变量
public abstract class Duck {
//鸭子不处理飞的行为,将飞的行为委托给FlyBehaviour接口
FlyBehaviour flyBehaviour;
//鸭子不处理叫的行为,将飞的行为委托给QucakBehaviour接口
QucakBehaviour quackBehaviour;
public void performFly(){
flyBehaviour.fly();
}
public void performQuack(){
quackBehaviour.quack();
}
public void swim(){
System.out.println("我会游泳...");
}
public abstract void display();
}
再来看看绿头鸭,
public class MallardDuck extends Duck {
public MallardDuck(){
flyBehaviour = new FlyWithWings();
quackBehaviour = new Quack();
}
public class void display(){
System.out.println("我是绿头鸭...");
}
}
现在测试一下:
public class Client{
public static void main(String[] args){
Duck duck = new MallardDuck();
duck.display();
duck.performFly();
duck.performQuack();
duck.swim();
}
}
执行后结果如下:
我是绿头鸭...
我会飞...
我会呱呱叫...
我会游泳...
如何实现动态改变鸭子的行为呢?修改Duck类如下:
public abstract class Duck {
//鸭子不处理飞的行为,将飞的行为委托给FlyBehaviour接口
FlyBehaviour flyBehaviour;
//鸭子不处理叫的行为,将飞的行为委托给QucakBehaviour接口
QucakBehaviour quackBehaviour;
public void setFlyBehaviour(FlyBehaviour flyBehaviour){
this.flyBehaviour = flyBehaviour;
}
public void setQucakBehaviour(QucakBehaviour quackBehaviour){
this.quackBehaviour = quackBehaviour;
}
public void performFly(){
flyBehaviour.fly();
}
public void performQuack(){
quackBehaviour.quack();
}
public void swim(){
System.out.println("我会游泳...");
}
public abstract void display();
}
现在构建一个模型鸭ModelDuck
public class ModelDuck extends Duck{
public ModelDuck(){
flyBehaviour = new FlyNoWay(); //一开始不会飞
quackBehaviour = new Quack();
}
public void display(){
System.out.println("我是模型鸭...");
}
}
新建一个新的飞行行为:FlyRocketPowered
public class FlyRocketPowered implements FlyBehaviour{
public void fly(){
System.out.println("我能像火箭一样飞...");
}
}
现在测试一下动态改变飞行行为:
public class Client{
public static void main(String[] args){
Duck duck = new ModelDuck();
duck.display();
duck.performFly();
duck.setFlyBehaviour(new FlyRocketPowered());
duck.performFly();
}
}
测试结果:
我是模型鸭...
我会向火箭一样飞...
这样就实现了行为与类分开,及变化的部分与不变化的部分分开了.
变化的部分
飞行的行为和叫的行为
不变的部分
鸭子会有用,拥有飞行和叫的行为.
封装变化和面向接口编程能让代码有很大的弹性,在代码不变或者很小的改变的情况下满足需求的变化,也易于维护.