1. 前言
所以我們已經知道介面是一種很強大的合約。那麼,以介面為主的程式設計到底是什麼?這是一種設計哲學,意思就是:「依賴抽象,不要依賴具體實現。」
再來舉個例子。你想煮咖啡,你去買咖啡機。你其實不在意這台機器到底是 哪一台「超級無敵機 V3000」還是「Mega-Automat Barista 5000」,對吧?你只在意這台機器會「煮咖啡」——也就是說,它有實作「合約」ICoffeeMaker。你只要按下「煮咖啡」的按鈕,它就會幫你煮。至於它是用研磨豆、膠囊還是什麼別的方式,對你這個使用者來說根本沒差。
在程式裡,這代表你在方法裡面,不是要求一定要 Document 或 Image 這種具體型別,而是要求 IPrintable。你的程式就能跟任何有實作這個介面的物件合作,完全不用知道它實際是什麼型別。
介面合約 就是保證每個實作這個介面的 class 都一定會實作所有介面成員(方法、屬性、事件)。對程式來說,任何有實作這個介面的物件都支援某個「功能集合」,你可以放心用它,不用管裡面怎麼做的。
就像你跟同事約好,每次都在大廳的紅色沙發見面。他怎麼到那裡——搭電梯、走樓梯還是跑牆壁——都不重要。重點是:他一定會在那裡。介面就是程式碼裡的「紅色沙發」。
範例
public interface IPrintable
{
void Print();
}
合約:任何實作 IPrintable 的人,都要會把自己印到螢幕上。怎麼印——細節隨便你。
2. 合約作為系統各部分的連接點
實際上怎麼用
一個系統可能有幾十個 class,不同人、不同時間、不同目的寫的。但只要大家都遵守同一個合約(也就是實作同一個介面),你就可以用同一種方式來用它們。
實際應用例子:想像有個客戶資料庫。有 Customer class、Employee class、Contractor class。每個 class 存資料的方式都不一樣,但如果大家都實作一個介面 IContactInfo,這個介面要求有 GetEmail() 跟 GetPhoneNumber() 這兩個方法,對外部程式來說,根本不用管這個物件是哪一種型別——重點是你一定拿得到 email 跟電話。
public interface IContactInfo
{
string GetEmail();
string GetPhoneNumber();
}
public class Customer : IContactInfo
{
public string Email { get; set; }
public string Phone { get; set; }
public string GetEmail() => Email;
public string GetPhoneNumber() => Phone;
}
// Employee 跟 Contractor 也是一樣...
現在如果你要印出所有公司有聯絡的人的資料(不管他是誰),你只要走訪 IContactInfo 的清單,呼叫需要的方法——就搞定了。
3. 以介面為主的程式設計
以介面為主的程式設計,就是寫程式時只依賴介面(合約),不依賴具體 class。只要 class 有實作這個介面,怎麼做都可以,只要符合介面要求就好。
為什麼這很重要?
- 可擴展性:要加新型別很簡單,不用改舊的程式。
- 可測試性:測試時可以很容易用 mock(假物件)來取代。
- 彈性:實作可以天天換,介面都不會變。
- 架構乾淨:模組之間耦合度低,可以重複利用。
範例——熟悉的程式:「銀行帳戶」
之前的例子裡,我們有個抽象 class BankAccount,有個抽象方法 Withdraw(),然後 SavingsAccount、CheckingAccount 這些具體型別去實作細節。
現在我們加一個介面——比如說要顯示餘額資訊:
public interface IBalanceReporter
{
void ReportBalance();
}
public abstract class BankAccount : IBalanceReporter
{
public double Balance { get; set; }
public abstract void Withdraw(double amount);
public void ReportBalance()
{
Console.WriteLine($"目前餘額:{Balance} 歐元");
}
}
現在所有用 IBalanceReporter 的人,都可以呼叫 ReportBalance(),不用管帳戶的實際型別。
4. 透過合約做通用處理和多型
範例:通用處理器
只要我們用介面,就可以寫出跟型別無關的泛用方法:
static void PrintAllBalances(IBalanceReporter[] accounts)
{
foreach (var reporter in accounts)
{
reporter.ReportBalance();
}
}
這個清單裡可以放任何有實作 IBalanceReporter 的物件:SavingsAccount、CheckingAccount,甚至 MockAccountForTesting 這種測試用的。完全沒在作弊,真的都能跑。
圖解:介面合約帶來什麼
+-------------------+ 實作 +-------------------+
| BankAccount | <---------------- | IBalanceReporter |
| (SavingsAccount) | |-------------------|
| (CheckingAccount)| | + ReportBalance() |
+-------------------+ +-------------------+
| ^
| |
+-----------+-----------------------+
|
任何其他有實作這個合約的 class
5. 合約、商業邏輯與架構設計
介面合約很清楚地分開 class「要做什麼」跟「怎麼做」。所以軟體架構師很愛用介面來設計系統邏輯。常常是先設計介面(合約),實作以後再說。
生活例子:支付系統
電子錢包、信用卡、PayPal、加密貨幣……每種都有自己的細節,但如果你抽象出一個介面 IPaymentProvider:
public interface IPaymentProvider
{
void Pay(decimal amount);
bool Refund(decimal amount);
}
跟這個介面互動的程式根本不在意到底是刷卡還是扣帳戶。這對架構跟實際開發都超方便:要支援新支付系統,完全不用動到其他程式。
6. 把商業邏輯抽到合約裡
合約可以把主要的商業規則拉到介面層,細節(比如檢查額度、給現金回饋等等)就交給各自的 class 去處理。
再一個例子
public interface ILogger
{
void LogInfo(string message);
void LogError(string message);
}
public class ConsoleLogger : ILogger
{
public void LogInfo(string message) => Console.WriteLine($"INFO: {message}");
public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}
public class FileLogger : ILogger
{
public void LogInfo(string message) => /* 寫入檔案 */;
public void LogError(string message) => /* 寫入檔案 */;
}
然後 client code 這樣寫(不管用哪種 logger):
void DoWork(ILogger logger)
{
logger.LogInfo("工作開始了。");
// ... 做一些事 ...
logger.LogError("工作出錯了。");
}
這就是以介面為主的程式設計:client code 只依賴合約(介面),不依賴具體實作。
7. 常見錯誤與陷阱
新手常常犯的錯:程式寫死依賴某個 class,不是依賴介面。這會帶來一堆問題:
- 要換東西或測試很難——一堆程式都要改。
- 要加新型別,很多地方都要動。
- 模組之間耦合度超高。
反過來說,如果一開始就用介面設計系統、參數都用介面傳,你就能做到低耦合、高彈性。
最重要的建議:想事情的時候,把模組之間的互動當作「合約」之間的互動,不要只想著具體實作。
GO TO FULL VERSION