解決需求變化

June 8, 2022

相信你一定常聽人家說,寫程式要有彈性,要有可維護性!那麼什麼叫有彈性?何謂可維護?老實說,這是有點抽象的問題,這邊從最簡單的定義開始:如果增加新的需求,原有的程式無需修改,只需針對新需求撰寫程式,那就是有彈性、具可維護性的程式。

開放/封閉原則

以〈定義行為外觀〉提到的需求為例,如果今天老闆突發奇想,想把海洋樂園變為海空樂園,有的東西會游泳,有的東西會飛,有的東西會游也會飛,那麼現有的程式可以應付這個需求嗎?

仔細想想,有的東西會飛,但不限於某種東西才有「飛」這個行為,有了先前的經驗,你使用 interface 定義了 Flyer 介面:

package cc.openhome;

public interface Flyer {
    public abstract void fly();
}

Flyer 介面定義了 fly 方法,程式中想要飛的東西,可以實作 Flyer 介面,假設有台海上飛機具有飛行的行為,也可以在海面上航行,可以定義 Seaplane 實作、SwimmerFlyer 介面:

package cc.openhome;

public class Seaplane implements Swimmer, Flyer {
    private String name;
    
    public Seaplane(String name) {
        this.name = name;
    }
    
    @Override
    public void fly() {
        System.out.printf("海上飛機 %s 在飛%n", name);
    }

    @Override
    public void swim() {
        System.out.printf("海上飛機 %s 航行海面%n", name);
    }
}

在 Java 中,類別可以實作兩個以上的類別,也就是擁有兩種以上的行為。例如 Seaplane 就同時擁有 SwimmerFlyer 的行為。

如果是會游也會飛的飛魚呢?飛魚是一種魚,可以繼承 Fish 類別,飛魚會飛,可以實作 Flyer 介面:

package cc.openhome;

public class FlyingFish extends Fish implements Flyer {
    public FlyingFish(String name) {
        super(name);
    }
    
    @Override
    public void swim() {
        System.out.println("飛魚游泳");
    }

    @Override
    public void fly() {
        System.out.println("飛魚會飛");
    } 
}

在 Java 中,類別可以同時繼承某個類別,並實作某些介面。例如 FlyingFish 是一種魚,也擁有 Flyer 的行為。如果現在要讓所有會游的東西游泳,那麼〈定義行為外觀〉中的 doSwim 方法就可以滿足需求了,因為 Seaplane 擁有 Swimmer 的行為,而 FlyingFish 也擁有 Swimmer 的行為:

package cc.openhome;

public class Ocean {
    public static void main(String[] args) {
...
        doSwim(new Seaplane("空軍零號"));
        doSwim(new FlyingFish("甚平"));
    }

    static void doSwim(Swimmer swimmer) {
        swimmer.swim();
    }
}

就滿足目前需求來說,所作的就是新增程式碼來滿足需求,但沒有修改舊有既存的程式碼,這符合開放/封閉(open-closed)原則,對擴充開放,對修改封閉,你的程式確實擁有某種程度的彈性與可維護性。

解決需求變化

調整架構的代價

當然需求是無止盡的,原有程式架也許確實可滿足某些需求,但有些需求也可能超過了原有架構預留之彈性,一開始要如何設計才會有彈性,是必須靠經驗與分析判斷,不用為了保有程式彈性的彈性而過度設計,因為過大的彈性表示過度預測需求,有的設計也許從不會遇上事先假設的需求。

例如,也許你預先假設會遇上某些需求而設計了一個介面,但從程式開發至生命週期結束,該介面從未被實作過,或者有一個類別實作過該介面,那麼該介面也許就不必存在,你事先的假設也許就是過度預測需求。

事先的設計也有可能因為需求不斷增加,而超出原本預留之彈性。例如老闆又開口了:不是所有的人都會游泳啊!有的飛機只會飛,不能停在海上啊!

好吧!並非所有的人都會游泳,所以不再讓 Human 實作 Swimmer

package cc.openhome;

public class Human {
    protected String name;
    
    public Human(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

假設只有游泳選手會游泳,游泳選手是一種人,並擁有 Swimmer 的行為:

package cc.openhome;

public class SwimPlayer extends Human implements Swimmer {
    public SwimPlayer(String name) {
        super(name);
    }
    
    @Override
    public void swim() {
        System.out.printf("游泳選手 %s 游泳%n", name);
    }   
}

有的飛機只會飛,所以設計一個 Airplane 類別作為 Seaplane 的父類別,Airplane 實作 Flyer 介面:

package cc.openhome;

public class Airplane implements Flyer {
    protected String name;
    
    public Airplane(String name) {
        this.name = name;
    }
    
    @Override
    public void fly() {
        System.out.printf("飛機 %s 在飛%n", name);
    }
}

Seaplane 會在海上航行,所以在繼承 Airplane 之後,必須實作 Swimmer 介面:

package cc.openhome;

public class Seaplane extends Airplane implements Swimmer {
    public Seaplane(String name) {
        super(name);
    }
    
    @Override
    public void fly() {
        System.out.print("海上");
        super.fly();
    }

    @Override
    public void swim() {
        System.out.printf("海上飛機 %s 航行海面%n", name);
    }
}

不過程式中的直昇機就只會飛:

package cc.openhome;

public class Helicopter extends Airplane {
    public Helicopter(String name) {
        super(name);
    }
    
    @Override
    public void fly() {
        System.out.printf("飛機 %s 在飛%n", name);
    }
}

這一連串的修改,都是為了調整程式架構,這只是個簡單的示範,想像一下,在更大規模的程式中調整程架構會有多麼麻煩,而且不只是修改程式很麻煩,沒有被修改到的地方,也有可能因此出錯:

解決需求變化

沒有動到這邊啊!怎麼出錯了?

程式架構很重要!這邊就是個例子,因為 Human 不再實作 Swimmer 介面了,因此不能再套用 doSwim 方法!應該改用 SwimPlayer 了!

不好的架構下要修改程式,很容易牽一髮而動全身,想像一下在更複雜的程式中,修改程式之後,到處出錯的窘境,也有不少人維護到架構不好的程式,抓狂到想砍掉重練的情況。

對於一些人來說,軟體看不到,摸不著,改程式似乎也不需成本,也因此架構這東西經常被漠視。曾經聽過一個比喻是這樣的:沒人敢在蓋十幾層高樓之後,要求修改地下室架構,但軟體業界常常在做這種事。

也許老闆又想到了:水裡的話,將淺海游泳與深海潛行分開好了!就算心裡再千百個不願意,你還是摸摸鼻子改了:

package cc.openhome;

public interface Diver extends Swimmer {
    public abstract void dive();
} 

在 Java 中,介面可以繼承自另一個介面,也就是繼承父介面行為,再於子介面中額外定義行為。假設一般的船可以在淺海航行:

package cc.openhome;

public class Boat implements Swimmer {
    protected String name;
    
    public Boat(String name) {
        this.name = name;
    }
    
    @Override
    public void swim() {
        System.out.printf("船在水面 %s 航行%n", name);
    }
} 

潛水航是一種船,可以在淺海游泳,也可以在深海潛行:

package cc.openhome;

public class Submarine extends Boat implements Diver {
    public Submarine(String name) {
        super(name);
    }
    
    @Override
    public void dive() {
        System.out.printf("潛水艇 %s 潛行%n", name);
    }
}      

需求不斷變化,架構也有可能因此而修改,好的架構在修改時,其實也不會全部的程式碼都被牽動,這就是設計的重要性,不過像這位老闆無止境地在擴張需求,他說一個你改一個,也不是辦法,找個時間,好好跟老闆談談這個程式的需求邊界到底是什麼吧!

解決需求變化

分享到 LinkedIn 分享到 Facebook 分享到 Twitter