引子:面試受挫#
小菜大四在求職途中收到一份計算機面試題:
請用 C++、Java、C# 或 VB.NET 任意一種物件導向語言實現一個計算器控制台程式,要求輸入兩個數和運算符號,得到結果。
小菜十分鐘內就交了卷,自認為沒有錯誤,卻在半個月後仍未收到回覆。表哥大鳥看完他的程式後指出:
- 命名不規範(
A、B、C、D等) - 多個
if分支讓電腦做了無用功,應改為switch - 沒處理除數為 0、輸入非數字等錯誤情境
- 最關鍵:沒有用「物件導向」的方式去實現
「程式無錯就是優?」——能跑通的程式不等於好的程式。容易維護、容易擴展、容易複用,才是物件導向追求的品質。
活字印刷的啟示#
大鳥透過「曹操做詩」的故事,讓小菜理解物件導向:
- 曹操吟詩後屢次更改詞句,工匠每改一字就要重新刻一次整版
- 若採用活字印刷,每次只需更改要改的字,其餘工作不必白做
活字印刷對應物件導向的四大好處:
- 可維護:要改,只需更改要改的字
- 可複用:這些字在後來的印刷中可以重複使用
- 可擴展:要加字,只需另刻字加入即可
- 靈活性好:字可橫排可豎排,移動即可滿足排列需求
活字印刷不僅是技術進步,更是思想的成功,物件導向的勝利。
傳統刻版的問題在於「所有字都刻在同一版面上」造成耦合度太高。物件導向則透過封裝、繼承、多型把程式的耦合度降低。
第一次重構:業務與介面分離#
小菜先把運算邏輯封裝為一個 Operation 類別,將計算與顯示分離:
public class Operation
{
public static double GetResult(double numberA, double numberB, string operate)
{
double result = 0d;
switch (operate)
{
case "+": result = numberA + numberB; break;
case "-": result = numberA - numberB; break;
case "*": result = numberA * numberB; break;
case "/": result = numberA / numberB; break;
}
return result;
}
}這樣不論是 Windows、Web、PDA 或手機程式,都能複用 Operation 類別。
這只是用到了物件導向三大特性中的「封裝」,還沒有用到繼承與多型。
第二次重構:繼承與多型#
問題:若要新增「平方根」運算,得修改 Operation 類別,連帶讓加減乘除被重新編譯,風險高。
解決:把 Operation 抽象化,每種運算各自繼承並覆寫 GetResult:
public class Operation
{
public double NumberA { get; set; }
public double NumberB { get; set; }
public virtual double GetResult() => 0;
}
class OperationAdd : Operation
{
public override double GetResult() => NumberA + NumberB;
}
class OperationDiv : Operation
{
public override double GetResult()
{
if (NumberB == 0) throw new Exception("除數不能為 0。");
return NumberA / NumberB;
}
}修改任一運算不會影響其他運算;新增運算只需擴展子類別。但問題隨之而來:客戶端如何知道該實例化哪一個子類別?
簡單工廠模式#
把「實例化哪一個子類別」這個容易變化的決定,封裝到一個獨立的工廠類別中:
public class OperationFactory
{
public static Operation createOperate(string operate)
{
Operation oper = null;
switch (operate)
{
case "+": oper = new OperationAdd(); break;
case "-": oper = new OperationSub(); break;
case "*": oper = new OperationMul(); break;
case "/": oper = new OperationDiv(); break;
}
return oper;
}
}客戶端只需傳入運算符號,由工廠決定該回傳哪個子類別實例:
Operation oper = OperationFactory.createOperate("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();此後若要修改加法,只需改
OperationAdd;新增複雜運算(平方根、立方根、對數、三角函數等)只需新增子類別並在工廠加分支;介面變動則完全不影響運算邏輯。
結構圖#
classDiagram
class Operation {
+double NumberA
+double NumberB
+GetResult() double
}
class OperationAdd {
+GetResult() double
}
class OperationSub {
+GetResult() double
}
class OperationMul {
+GetResult() double
}
class OperationDiv {
+GetResult() double
}
class OperationFactory {
+createOperate(string) Operation$
}
Operation <|-- OperationAdd
Operation <|-- OperationSub
Operation <|-- OperationMul
Operation <|-- OperationDiv
OperationFactory ..> Operation : createsUML 類圖速成#
書中順帶介紹 UML(Unified Modeling Language)類圖的基本符號,是後續章節閱讀基礎:
類別與介面#
- 類別:分三層——類別名稱、特性(欄位/屬性)、操作(方法)
- 可見性:
+public、-private、#protected - 抽象類:類別名稱以斜體顯示
- 介面:頂端標
<<interface>>,亦可用「棒棒糖表示法」
類別之間的關係#
- 繼承(Inheritance):空心三角形 + 實線
- 實現介面(Realization):空心三角形 + 虛線
- 關聯(Association):實線箭頭,當一個類別「知道」另一個類別
- 聚合(Aggregation):空心菱形 + 實線箭頭,「擁有」關係,但部分不屬於整體
- 合成/組合(Composition):實心菱形 + 實線箭頭,嚴格的部分與整體關係,生命週期一致
- 依賴(Dependency):虛線箭頭,A 需要 B 才能完成功能
連線兩端可標基數(multiplicity),例如鳥有 1 對 2 翅膀;不限數量則用 n。
本章小結#
編程不只是寫完能跑就好,更要思考如何讓程式碼簡練、易維護、易擴展、易複用。
一個小小的計算器,從「面試敗筆」演化到使用簡單工廠模式(Simple Factory Pattern)的優雅實現,正是物件導向設計威力的最佳示範。