Day 13 我們學過 Inheritance。
例如:
Dog is an Animal
→ Dog 是 Animal 的一種
→ is-a
→ Inheritance
但不是所有 Class 之間都是 is-a。
例如:
Car is an Engine?
→ No
Car has an Engine?
→ Yes
Car 不是 Engine 的一種。
而是:
Car 需要一個 Engine 幫它提供功能。
今天就來學:
先建立:
class GasEngine
{
public void Start()
{
Console.WriteLine("Gas Engine Start");
}
}
現在 Car 需要使用:
Start()
如果只是為了取得 Start(),寫成:
class Car : GasEngine
{
}
雖然語法可以成立,但關係不合理。
因為:
Car is a GasEngine?
→ No
Car 並不是 GasEngine 的一種。
真正的關係應該是:
Car has a GasEngine
所以這裡不適合使用 Inheritance。
先記住:
is-a
→ Inheritance
has-a
→ Composition
既然:
Car has a GasEngine
我們可以讓 Car 持有一個 GasEngine Object。
class Car
{
private readonly GasEngine _engine;
public Car(GasEngine engine)
{
_engine = engine;
}
public void Start()
{
_engine.Start();
}
}
這段程式可以拆成三個步驟理解:
1. Car 保存 Engine
2. 建立 Car 時把 Engine 傳進來
3. Car 需要時使用 Engine
先看:
private readonly GasEngine _engine;
這是在 Car 裡建立一個 Field。
GasEngine
→ Field 的 Type
_engine
→ Field 名稱
它的用途是:
讓 Car 保存一個 GasEngine Object,之後可以繼續使用。
可以想成:
Car
└── _engine
└── GasEngine Object
這裡:
private
代表:
_engine
→ 主要由 Car 內部使用
而:
readonly
代表:
_engine
→ 建立 Car 時設定
→ 之後不能隨意改成另一個 Object
初學先記:
private
→ Class 內部使用
readonly
→ 設定完成後不能重新指定
即可。
再看 Constructor:
public Car(GasEngine engine)
{
_engine = engine;
}
這代表:
建立 Car 的時候,要從外面提供一個 GasEngine Object。
例如:
GasEngine engine = new GasEngine();
Car car = new Car(engine);
第一行:
GasEngine engine = new GasEngine();
先建立一個 GasEngine Object。
new GasEngine()
↓
GasEngine Object
↓
engine
接著:
Car car = new Car(engine);
把 engine 傳進 Car 的 Constructor。
engine
↓
new Car(engine)
↓
Car(GasEngine engine)
然後:
_engine = engine;
把收到的 Engine 保存到 _engine。
engine 和 _engine 有什麼不同?這是很容易混淆的地方。
public Car(GasEngine engine)
{
_engine = engine;
}
這裡有兩個名稱:
engine
_engine
engineGasEngine engine
是 Constructor Parameter。
用途:
接收外面傳進來的 GasEngine Object。
_engineprivate readonly GasEngine _engine;
是 Car 的 Field。
用途:
把 GasEngine Object 保存起來,之後繼續使用。
所以:
_engine = engine;
可以理解成:
把傳進來的
engine,保存到 Car 的_engine。
整理:
| 程式碼 | 用途 |
|---|---|
GasEngine engine |
接收外面傳進來的 Object |
GasEngine _engine |
Car 內部保存 Object |
_engine = engine |
把收到的 Object 保存起來 |
最簡單記:
engine
→ 傳進來的
_engine
→ 保存下來的
完整流程:
new GasEngine()
↓
建立 GasEngine Object
↓
engine
↓
new Car(engine)
↓
Constructor 接收 engine
↓
_engine = engine
↓
Car 保存 Engine
現在:
public void Start()
{
_engine.Start();
}
因為 Car 已經把 Engine 保存到:
_engine
所以需要啟動時,就可以:
_engine.Start();
流程:
Car.Start()
↓
_engine.Start()
↓
GasEngine.Start()
所以 Composition 的核心就是:
Car 自己不是 Engine,而是持有 Engine,並使用 Engine 提供的功能。
Car
│
│ has-a
↓
GasEngine
這就是 Composition(組合)。
目前 Car 寫的是:
private readonly GasEngine _engine;
代表:
Car
→ 只能使用 GasEngine
但未來可能還有:
GasEngine
ElectricEngine
兩種 Engine 雖然不同,但都有一個共同需求:
都要可以 Start()
我們希望 Car 表達的是:
我不在乎是哪一種 Engine,只要能
Start()就可以。
這時就可以使用 Interface。
先定義:
interface IEngine
{
void Start();
}
這就是 Interface。
可以先理解成:
Interface 用來定義共同規則。
這裡:
void Start();
代表:
只要實作 IEngine
→ 就一定要提供 Start()
IEngine 不負責決定:
Start() 要怎麼做
它只規定:
一定要能 Start()
正式一點的名稱叫:
Contract(契約)
所以:
Interface
→ 定義 Contract
→ 規定必須提供哪些行為
IEngine?C# 常見的命名慣例會在 Interface 名稱前加上:
I
例如:
IEngine
IPayment
INotification
所以看到:
IEngine
通常就知道它是一個 Interface。
建立:
class GasEngine : IEngine
{
public void Start()
{
Console.WriteLine("Gas Engine Start");
}
}
再建立:
class ElectricEngine : IEngine
{
public void Start()
{
Console.WriteLine("Electric Engine Start");
}
}
兩個 Class 都實作:
IEngine
所以都必須提供:
Start()
但實作方式可以不同:
GasEngine
→ Gas Engine Start
ElectricEngine
→ Electric Engine Start
整理:
IEngine
→ 規定一定要有 Start()
GasEngine
→ 實作 Start()
ElectricEngine
→ 實作 Start()
: 這次代表什麼?之前學過:
class Dog : Animal
代表:
Dog
→ 繼承 Animal
現在:
class GasEngine : IEngine
代表:
GasEngine
→ 實作 IEngine
所以:
Class : Class
→ Inheritance
Class : Interface
→ Interface Implementation
原本:
private readonly GasEngine _engine;
代表 Car 只能保存 GasEngine。
現在改成:
private readonly IEngine _engine;
完整程式:
class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Start()
{
_engine.Start();
}
}
這裡:
IEngine _engine
代表:
Car 裡保存一個
IEngineType 的 Field。
而這個 Field 實際可以參考:
GasEngine Object
或
ElectricEngine Object
因為兩者都實作:
IEngine
所以:
GasEngine
→ 可以傳進 Car
ElectricEngine
→ 也可以傳進 Car
可以先理解成:
Car
→ 不限定是哪一種 Engine
Car
→ 只要求它符合 IEngine
先使用 GasEngine:
IEngine engine =
new GasEngine();
Car car =
new Car(engine);
car.Start();
流程:
GasEngine Object
↓
IEngine engine
↓
傳進 Car
↓
保存到 _engine
↓
Car.Start()
↓
_engine.Start()
結果:
Gas Engine Start
如果改成:
IEngine engine =
new ElectricEngine();
Car car =
new Car(engine);
car.Start();
結果:
Electric Engine Start
注意:
Car
→ 完全沒有修改
我們只替換:
GasEngine
↓
ElectricEngine
因為 Car 依賴的是:
IEngine
而不是固定的:
GasEngine
所以:
Car
→ 只要求 Engine 能 Start()
GasEngine
→ 可以
ElectricEngine
→ 也可以
這就是 Interface 帶來的重要好處:
只要符合相同規則,不同實作就可以替換。
現在把兩個概念放在一起。
IEngine
→ 定義規則
→ 一定要有 Start()
Car
→ 持有 IEngine
→ 使用 IEngine 提供的功能
整體:
IEngine
/ \
↓ ↓
GasEngine ElectricEngine
Car
│
│ has-a
↓
IEngine
可以理解成:
Interface
→ 定義「對方要提供什麼」
Composition
→ 把符合需求的 Object
放進自己的 Class 使用
以今天的例子:
IEngine
→ 規定 Start()
GasEngine / ElectricEngine
→ 實作 Start()
Car
→ Constructor 接收 IEngine
→ 保存到 _engine
→ 使用 _engine.Start()
最簡單的方式,就是先看兩個 Class 的關係。
is-a例如:
Dog is an Animal?
→ Yes
代表:
Dog
→ 是 Animal 的一種
可以考慮:
Inheritance
has-a例如:
Car has an Engine?
→ Yes
代表:
Car
→ 不是 Engine
Car
→ 持有 Engine
→ 使用 Engine 的功能
可以考慮:
Composition
整理:
| 關係 | 常見設計 |
|---|---|
| A 是 B 的一種 | Inheritance |
| A 持有 B、使用 B 的功能 | Composition |
直接記:
is-a
→ Inheritance
has-a
→ Composition
不要只是為了取得某個 Method,就建立不合理的 Inheritance。
interface IEngine
{
void Start();
}
class GasEngine : IEngine
{
public void Start()
{
Console.WriteLine("Gas Engine Start");
}
}
class ElectricEngine : IEngine
{
public void Start()
{
Console.WriteLine("Electric Engine Start");
}
}
class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Start()
{
Console.WriteLine("Car Start");
_engine.Start();
}
}
IEngine engine =
new GasEngine();
Car car =
new Car(engine);
car.Start();
結果:
Car Start
Gas Engine Start
IEngine engine =
new ElectricEngine();
Car car =
new Car(engine);
car.Start();
結果:
Car Start
Electric Engine Start
Car 完全不用修改。
因為:
Car
→ 依賴 IEngine
GasEngine / ElectricEngine
→ 都符合 IEngine
整體流程:
IEngine
→ 規定 Start()
↓
GasEngine
ElectricEngine
→ 提供實作
↓
Car Constructor
→ 接收 IEngine
↓
_engine
→ 保存 IEngine
↓
Car.Start()
→ _engine.Start()
Day 14 學過:
Animal animal = new Dog();
Interface 也可以作為共同 Type。
IEngine engine =
new GasEngine();
也可以:
IEngine engine =
new ElectricEngine();
所以:
Variable Type
→ IEngine
實際 Object Type
→ GasEngine / ElectricEngine
兩者都可以:
engine.Start();
因此:
Interface 也可以作為共同 Type,搭配 Polymorphism。
這裡知道這個關係即可。
建立:
interface IPayment
{
void Pay(decimal amount);
}
再建立:
CreditCardPayment
LinePayPayment
兩個 Class 都實作:
IPayment
並提供:
Pay(decimal amount)
接著建立:
class CheckoutService
{
private readonly IPayment _payment;
public CheckoutService(
IPayment payment)
{
_payment = payment;
}
public void Checkout(decimal amount)
{
_payment.Pay(amount);
}
}
試著先使用:
CreditCardPayment
再換成:
LinePayPayment
今天最重要的是三個概念。
Interface
→ 定義共同規則
→ 規定必須提供哪些行為
Composition
→ 一個 Object 持有其他 Object
→ 使用它提供的功能
is-a
→ Inheritance
has-a
→ Composition
把整篇串起來:
IEngine
→ 規定一定要有 Start()
↓
GasEngine
ElectricEngine
→ 實作 Start()
↓
Car
→ Constructor 接收 IEngine
→ 保存到 _engine
→ 使用 _engine.Start()
最簡單的 Mental Model:
Interface
→ 對方要提供什麼?
Composition
→ 我怎麼使用對方?
一句話結論:
Interface 定義「要提供什麼功能」;Composition 則讓一個 Object 持有並使用其他 Object 提供的功能。