解決需求變化
June 8, 2022相信你一定常聽人家說,寫程式要有彈性,要有可維護性!那麼什麼叫有彈性?何謂可維護?老實說,這是有點抽象的問題,這邊從最簡單的定義開始:如果增加新的需求,原有的程式無需修改,只需針對新需求撰寫程式,那就是有彈性、具可維護性的程式。
開放/封閉原則
以〈定義行為外觀〉提到的需求為例,如果今天老闆突發奇想,想把海洋樂園變為海空樂園,有的東西會游泳,有的東西會飛,有的東西會游也會飛,那麼現有的程式可以應付這個需求嗎?
仔細想想,有的東西會飛,但不限於某種東西才有「飛」這個行為,有了先前的經驗,你使用 interface
定義了 Flyer
介面:
package cc.openhome;
public interface Flyer {
public abstract void fly();
}
Flyer
介面定義了 fly
方法,程式中想要飛的東西,可以實作 Flyer
介面,假設有台海上飛機具有飛行的行為,也可以在海面上航行,可以定義 Seaplane
實作、Swimmer
與 Flyer
介面:
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
就同時擁有 Swimmer
與 Flyer
的行為。
如果是會游也會飛的飛魚呢?飛魚是一種魚,可以繼承 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);
}
}
需求不斷變化,架構也有可能因此而修改,好的架構在修改時,其實也不會全部的程式碼都被牽動,這就是設計的重要性,不過像這位老闆無止境地在擴張需求,他說一個你改一個,也不是辦法,找個時間,好好跟老闆談談這個程式的需求邊界到底是什麼吧!