Java设计原则---封装变化和面向接口编程

最近看了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();
    }
}

测试结果:

我是模型鸭...

我会向火箭一样飞...

这样就实现了行为与类分开,及变化的部分与不变化的部分分开了.

小总结

变化的部分
飞行的行为和叫的行为

不变的部分
鸭子会有用,拥有飞行和叫的行为.

总结

封装变化和面向接口编程能让代码有很大的弹性,在代码不变或者很小的改变的情况下满足需求的变化,也易于维护.

你可能感兴趣的:(Android,Java,Java设计模式)