設計程式時,當我們收到需求之後,要做出來很容易,要做得好的上下限卻差很多。
如果你能確保這軟體寫完用一次就不需要了,未來不支持更新,那你可能可以忽略這一切,也不需要設計模式,反正不需要擴展也不用維護XD;但這軟體就相當於死了,只要還活著,面對的就是不停的改變;擴展─需要新功能、要跨到新的平台,是改變;維護─環境變了,對接的其他工具變了,重構程式碼,也是改變;因此,程式的設計要考慮彈性,不能牽一髮動全身,改個用法就要整篇重寫,做好的測試也都要重來。
書中以一個充滿各種鴨子遊戲舉例,為了不要重複撰寫多個鴨子的類別,來個簡單明瞭的base class鴨子,其他鴨子則是繼承它,並覆寫(override)外觀部分:
(以下由C++撰寫)
#include <iostream>
using namespace std;
class Duck
{
public:
void quack()
{
cout << "quack!" << endl;
}
void swim()
{
cout << "swim in the water!" << endl;
}
void display()
{
cout << "a normal duck" << endl;
}
};
class MallardDuck: public Duck
{
public:
void display()
{
cout << "a green head duck" << endl;
}
};
class RedheadDuck: public Duck
{
public:
void display()
{
cout << "a red head duck" << endl;
}
};
int main()
{
Duck duck;
duck.quack();
duck.swim();
duck.display();
MallardDuck mallard;
mallard.quack();
mallard.swim();
mallard.display();
RedheadDuck redhead;
redhead.quack();
redhead.swim();
redhead.display();
return 0;
}
// -----output-----
quack!
swim in the water!
a normal duck
quack!
swim in the water!
a green head duck
quack!
swim in the water!
a red head duck
此時,若想為鴨子加一個feature:fly()
,定義為cout<<"fly in sky"<<endl;
,所有繼承的鴨子都會多了此feature;而若現在鴨子有10種,分成了兩類,有一半的鴨子不會飛,那就變成要逐一將不會飛的鴨子檢查並覆寫為 cout<<"cannot fly"<<endl;
;若此時又出現第三種情形,cout<<"fly in space<<endl;
,有好幾種鴨子屬於此類,那又得一一覆寫,不利於維護。
(皆用使用draw.io繪製)
於是,本書的第一個原則出現了:
把會變和不會變的部分隔開
把會變的部分封裝起來,就可以修改會變的部分,並不影響不變的部分。
回來看鴨子會變與不變的部分:
fly()
, quack()
(不是每隻鴨子都會有這些feature)display()
(所有鴨子都會有各自的外觀)然後,將會變的部分都抽出來到另外的類別中,達到「封裝」的效果,讓它不會影響其餘的部分。
這樣的話,結構就會變成"duck"一類,飛行行為與鳴叫行為各自一類。
此時可以暫停一下,自行想想這樣結構要怎麼設計比較好呢?
上半部是目前覺得可以合理使用繼承的部分(display),下面則是這幾隻鴨子各自飛行跟鳴叫行為,這個diagram可以怎麼長呢?
明天再繼續看下去,出現了第二個原則,還有策略模式給出的答案,跟自己的設計比較看看,也比較會有記憶點!