在上一篇文章中,我們看到繼承帶來的限制與設計陷阱。本篇將把鏡頭轉向另一個更自由、更靈活的設計哲學 —— 組合(Composition)。
組合就像是把系統拆解成可重組的積木,每個模組只負責一個任務,清晰、精準、可插拔,讓程式不僅易於擴充,也更能回應未來的變化。
🌸 組合怎麼解決問題?
用組合代替繼承,解決行為歧義(Ambiguity of Behavior)
讓戰鬥法師「擁有」戰士和法師的能力,而不是「繼承」它們。
1 2 3 4 5 6 7 8 9 10
| public class Battlemage { private Warrior _warrior = new Warrior(); private Mage _mage = new Mage();
public void MeleeAttack() => _warrior.Attack(); public void CastSpell() => _mage.CastSpell(); }
|
用 Interface 來分離行為,避免多繼承,解決屬性重複(Duplicate State)
讓 Warrior 和 Mage 各自實現自己的行為,但把狀態留給 Character,並且避免通過多繼承直接繼承狀態。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Character { public int Health { get; set; } }
public interface IWarrior { void Attack(); }
public interface IMage { void CastSpell(); }
public class Battlemage : Character, IWarrior, IMage { public void Attack() { } public void CastSpell() { } }
|
這樣屬性只存在於 Character 中,行為分開,避免了屬性重複的問題。
用組合與單一職責原則(Single Responsibility Principle)解耦處理功能耦合過緊的問題
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public interface IStorable { void Save(); void Load(); }
public interface ICompressible { void Compress(); void Decompress(); }
public class File : IStorable { public void Save() { } public void Load() { } }
public class CompressedFile : IStorable, ICompressible { private File _file = new File();
public void Save() => _file.Save(); public void Load() => _file.Load(); public void Compress() { } public void Decompress() { } }
|
這樣每個 Class 專注於自己的功能,CompressedFile 可以靈活組合功能而不需要多繼承。
當有新需求時,不需動現有 Class,只要「新增組件」即可
我們可以建一個新的 Class 來實現這個功能,然後將它提供給需要使用的物件。這種方法允許我們動態地增加物件的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class SmartHome { private List<ISmartDevice> devices = new List<ISmartDevice>();
public void AddDevice(ISmartDevice device) { devices.Add(device); }
public void ControlAllDevices(bool turnOn) { foreach (var device in devices) { if (turnOn) device.TurnOn(); else device.TurnOff(); } } }
public interface ISmartDevice { void TurnOn(); void TurnOff(); }
|
組合的延伸:清晰邊界、不依賴 protected 與 virtual
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class GameObject { public List<IComponent> Components { get; } = new List<IComponent>();
public void AddComponent(IComponent component) { Components.Add(component); }
public void Update() { foreach (var component in Components) { component.Update(); } } }
public interface IComponent { void Update(); }
public class PhysicsComponent : IComponent { public void Update() { } }
public class RenderComponent : IComponent { public void Update() { } }
|
封裝變化點:策略模式(Strategy Pattern)
讓「付款方式」可替換,而不用動到主要流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class PaymentProcessor { private IPaymentStrategy paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy strategy) { paymentStrategy = strategy; }
public void ProcessPayment(decimal amount) { paymentStrategy.Pay(amount); } }
public interface IPaymentStrategy { void Pay(decimal amount); }
public class CreditCardPayment : IPaymentStrategy { public void Pay(decimal amount) { } }
public class PayPalPayment : IPaymentStrategy { public void Pay(decimal amount) { } }
|
👾 組合的挑戰
每個實作都要重複寫同樣邏輯。這就是組合的痛點之一:重複。
重複實作行為
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public interface IAudioPlayer { void Play(); void Pause(); void Stop(); void SkipTrack(); }
public class MP3Player : IAudioPlayer { public void Play() { Console.WriteLine("MP3播放器開始播放"); } public void Pause() { Console.WriteLine("MP3播放器暫停"); } public void Stop() { Console.WriteLine("MP3播放器停止"); } public void SkipTrack() { Console.WriteLine("MP3播放器跳過曲目"); } }
public class CDPlayer : IAudioPlayer { public void Play() { Console.WriteLine("CD播放器開始播放"); } public void Pause() { Console.WriteLine("CD播放器暫停"); } public void Stop() { Console.WriteLine("CD播放器停止"); } public void SkipTrack() { Console.WriteLine("CD播放器跳過曲目"); } }
public class StreamingPlayer : IAudioPlayer { public void Play() { Console.WriteLine("串流播放器開始播放"); } public void Pause() { Console.WriteLine("串流播放器暫停"); } public void Stop() { Console.WriteLine("串流播放器停止"); } public void SkipTrack() { Console.WriteLine("串流播放器跳過曲目"); } }
|
每個播放器類都需要實現相同的方法(Play, Pause, Stop, SkipTrack),導致了大量的重複
C# 的解法:Default Interface Methods(C# 8 起)
C# 雖然不支援多重繼承,但允許一個類別實作多個介面(Interface),而從 C# 8 開始,介面也可以擁有「預設實作方法(Default Interface Methods)」。這是一個極具彈性的設計,它讓我們可以在不破壞既有實作的前提下,為介面新增行為。
1 2 3 4 5 6 7
| interface IExample { void Ex1(); void Ex2() => Console.WriteLine("IExample.Ex2"); }
|
這樣做有幾個好處:
✅ 不會破壞現有實作的相容性
✅ 實作端可以選擇是否覆寫預設邏輯
✅ 可讓介面具備部分「共享邏輯」,減少重複
多重能力的組合:以音訊播放為例
我們可以透過多個專注功能的介面來組合出不同的播放器行為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public interface IAudioFile { string FileName { get; set; } void Play() => Console.WriteLine($"Playing {FileName}"); }
public interface IStreamable { void Stream() => Console.WriteLine("Streaming audio"); }
public interface ICompressible { void Compress() => Console.WriteLine("Compressing audio"); }
public interface IMP3File : IAudioFile, IStreamable, ICompressible { void ApplyID3Tags() => Console.WriteLine("Applying ID3 tags"); }
public interface IFLACFile : IAudioFile, ICompressible { void ApplyVorbisComment() => Console.WriteLine("Applying Vorbis comment"); }
public class MP3Player : IMP3File { public string FileName { get; set; }
public void Play() => Console.WriteLine($"MP3 player playing: {FileName}"); }
public class FLACPlayer : IFLACFile { public string FileName { get; set; }
}
|
使用
1 2 3 4 5 6 7 8 9 10 11 12
| IMP3File mp3 = new MP3Player { FileName = "song.mp3" }; mp3.Play(); mp3.Stream(); mp3.Compress(); mp3.ApplyID3Tags();
IFLACFile flac = new FLACPlayer { FileName = "audio.flac" }; flac.Play(); flac.Compress(); flac.ApplyVorbisComment();
|
這樣的設計實現了:
可選擇性覆寫介面邏輯
多重功能自由組合,彈性高
不會造成繼承階層混亂,也避免菱形問題
相比繼承,介面 + 預設實作的組合方式,不僅提升了模組性與可維護性,也讓行為邏輯的來源更加清晰明確。
☘️ 結語
當我們思考類別設計時,與其專注在「它是什麼(is-a)」的血緣邏輯,不如轉向「它能做什麼(can-do)」的能力邏輯。
這種思考會讓我們更常考慮這些關鍵設計原則與工具:
✅ Interface:定義行為合約,清楚邊界
✅ Composition:彈性組合,按需注入
✅ SOLID:促進職責分離與擴展彈性
✅ Contract:清晰定義期望與使用方式
✅ Strategy:封裝變化、延遲決策
用組合思維設計的系統,不僅更能應對需求變動,也更容易測試、維護與擴充。
📖 參考
Joost’s Dev Blog
default interface methods