1. 商業邏輯中的介面:按鈕和動作
這是介面主題的最後一堂課,所以這裡會有超多實戰範例,讓你更懂介面在實務上的用法。
介面在大型應用中特別有價值。所以下面舉的都是比較複雜的例子,有時甚至超前課程進度。有興趣就繼續看!沒興趣就直接跳下個等級 :P
最簡單的例子 — 按鈕和點擊
我們繼續開發我們的學習用主控台小程式——假設我們有一個簡單的使用者元件系統(像是按鈕、文字欄位、切換開關)在主控台選單裡。
我們希望有些元件能對點擊有反應(像按鈕),有些則不能(像靜態標題)。介面可以很優雅地實現這種需求。
步驟 1:建立介面
// 可以被「點擊」的元件:
public interface IClickable
{
void Click();
}
步驟 2:把介面套用到不同型態的元件
// 所有選單元件的基底類別
public class MenuItem
{
public string Title { get; set; }
public MenuItem(string title)
{
Title = title;
}
public virtual void Display()
{
Console.WriteLine(Title);
}
}
// 按鈕:可以「點擊」
public class Button : MenuItem, IClickable
{
public Button(string title) : base(title) {}
public void Click()
{
Console.WriteLine($"[按鈕] {Title} 被按下了!");
}
}
// 純文字標籤:不能點擊
public class Label : MenuItem
{
public Label(string title) : base(title) {}
}
步驟 3:用介面多型
IClickable[] clickableItems = new IClickable[]
{
new Button("儲存"),
new Button("離開")
// Label 不能加進來——它不是 IClickable!
};
foreach (var item in clickableItems)
{
item.Click();
}
看到魔法了嗎?這個清單只收「會」Click 的物件,所以你不會在執行時遇到「點錯東西」的錯誤。
2. 介面與「策略」模式:動態選擇演算法
假設你寫了一個應用程式,可以用不同方式儲存報表:存檔案、存資料庫、丟到「雲端」,甚至傳到你老闆的 Telegram(別問,什麼都有可能)。
步驟 1:定義需求
我們要讓 ReportGenerator 這個類別能用任何儲存方式,不用知道細節。
步驟 2:描述策略介面
public interface IDataSaver
{
void Save(string reportData);
}
步驟 3:實作不同的方式
public class FileDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[FileDataSaver] 儲存到檔案...\n" + reportData);
// 這裡可以寫 File.WriteAllText(...)
}
}
public class DatabaseDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[DatabaseDataSaver] 儲存到資料庫...\n" + reportData);
// 這裡可以寫資料庫存取程式碼
}
}
步驟 4:用介面切換儲存策略
public class ReportGenerator
{
private readonly IDataSaver _dataSaver;
public ReportGenerator(IDataSaver dataSaver)
{
_dataSaver = dataSaver;
}
public void GenerateReport()
{
string report = "這是很重要的報表!";
Console.WriteLine("產生報表中...");
_dataSaver.Save(report);
}
}
靈活示範
// 可以不用改 ReportGenerator 就換儲存方式!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();
IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();
實用重點:如果明天你想把報表寫到什麼超新潮服務,只要寫一個新類別實作介面,然後把物件塞進架構裡,舊程式碼一行都不用改。
3. .NET 裡的介面:IDisposable 和 using
.NET 裡最常見的介面之一就是 IDisposable。所有會用到非受控資源的類別(像檔案、串流、網路連線)都會實作它。
為什麼要有 IDisposable?
當你用到一定要釋放的資源(像關檔案),你就實作 IDisposable,寫一個 Dispose 方法。這樣你就可以把物件丟進 using,離開區塊時 Dispose 會自動被呼叫。
範例:模擬檔案操作
public class FakeFile : IDisposable
{
public string FileName { get; }
public FakeFile(string fileName)
{
FileName = fileName;
Console.WriteLine($"開啟檔案: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"關閉檔案: {FileName}");
}
}
// 在主程式裡:
using (var file = new FakeFile("otchet.txt"))
{
Console.WriteLine("寫入檔案中...");
// 離開 using 區塊時檔案會自動「關閉」
}
輸出:
開啟檔案: otchet.txt
寫入檔案中...
關閉檔案: otchet.txt
真的很有用:你不會忘記關檔案——介面跟語言機制會保護你。
4. 集合和 LINQ 裡的介面
你在用清單、陣列、字典時,其實早就在用介面了,只是沒特別注意。
List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // 沒問題!
// 現在可以這樣遍歷元素:
foreach(var x in enumerable)
{
Console.WriteLine(x);
}
大部分 LINQ 方法都是透過 IEnumerable<T> 這個介面操作集合。這讓你的程式碼不用管集合的實際型態。
為什麼要這樣?
你可以把 List<T> 換成 T[]、HashSet<T>,甚至你自己寫的集合——你的程式碼都還是能跑!
5. 測試用的介面(Mock 物件)
寫測試時很重要的一點是隔離程式碼,不要碰到真的資料庫或檔案。介面讓你可以用假物件(mock)來測試,不會動到真的資料或檔案。
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("已經把資料存到 mock 物件!");
}
}
// 在測試裡
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();
Console.WriteLine($"Save 有被呼叫嗎? {saver.WasCalled}");
結果:測試不會碰到真實世界,但可以檢查方法有沒有被呼叫!
6. 明確實作介面來解決衝突
有時候一個類別要實作兩個有同名方法的介面,但這些方法邏輯不一樣。
public interface IFlyable
{
void Move();
}
public interface ISwimmable
{
void Move();
}
public class Duck : IFlyable, ISwimmable
{
// 明確實作
void IFlyable.Move()
{
Console.WriteLine("鴨子在飛!");
}
void ISwimmable.Move()
{
Console.WriteLine("鴨子在游!");
}
}
Duck duck = new Duck();
// duck.Move(); // 錯誤:沒有這個方法!
((IFlyable)duck).Move(); // "鴨子在飛!"
((ISwimmable)duck).Move(); // "鴨子在游!"
這看起來有點硬,但有時規格就是這樣要求!
7. 事件模型裡的介面:INotifyPropertyChanged
.NET 有標準的事件介面——像是資料模型有變動時 UI 要知道(WPF、MAUI、其他 GUI 都很常見)。
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
重點:任何支援資料繫結的框架都會希望你的類別實作這個介面——這樣 UI 屬性變動時會自動更新。
8. 工廠模式(Factory)裡的介面
介面超適合拿來做工廠——就是那種會生出不同但可互換物件的類別。
public interface ITransport
{
void Move();
}
public class Bicycle : ITransport
{
public void Move() => Console.WriteLine("騎腳踏車!");
}
public class Car : ITransport
{
public void Move() => Console.WriteLine("開車!");
}
public class TransportFactory
{
public static ITransport Create(string type)
{
return type switch
{
"bike" => new Bicycle(),
"car" => new Car(),
_ => throw new ArgumentException("未知的交通工具類型")
};
}
}
ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "騎腳踏車!"
9. 事件用的介面:自訂事件範例
來描述一個「事件監聽者」介面:
public interface ILoginListener
{
void OnLogin(string userName);
}
// 會觸發事件的類別
public class LoginManager
{
private List<ILoginListener> listeners = new();
public void Subscribe(ILoginListener listener) => listeners.Add(listener);
public void Login(string userName)
{
Console.WriteLine($"使用者 {userName} 登入了。");
foreach (var listener in listeners)
listener.OnLogin(userName);
}
}
// 實作介面的類別
public class WelcomeMessage : ILoginListener
{
public void OnLogin(string userName)
{
Console.WriteLine($"歡迎, {userName}!");
}
}
LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("Vasya");
// 使用者 Vasya 登入了。
// 歡迎, Vasya!
面試時:如果被問到「你會怎麼做自己的事件系統?」就大方講介面監聽者這一套!
10. 插件用的介面(應用程式擴充性)
很多大型應用都支援插件。靠介面,你的程式可以「即時載入」新模組,完全不用知道它們的內部細節。
public interface IPlugin
{
string Name { get; }
void Run();
}
// 你的應用程式:
public class PluginLoader
{
public void LoadAndRun(IEnumerable<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine($"啟動插件: {plugin.Name}");
plugin.Run();
}
}
}
插件可以給第三方開發——只要有實作介面就好。你的應用程式就能無限擴充!
GO TO FULL VERSION