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();
    }
}

測試結果:

我是模型鴨...

我會向火箭一樣飛...

這樣就實現了行爲與類分開,及變化的部分與不變化的部分分開了.

小總結

變化的部分
飛行的行爲和叫的行爲

不變的部分
鴨子會有用,擁有飛行和叫的行爲.

總結

封裝變化和麪向接口編程能讓代碼有很大的彈性,在代碼不變或者很小的改變的情況下滿足需求的變化,也易於維護.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章