iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 15 篇

Day 15|Interface 與 Composition:把需要的功能組合進來

  • 分享至 

  • xImage
  •  

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 幫它提供功能。

今天就來學:

  • Interface
  • Composition

1. 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

2. 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

2-1. 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
→ 設定完成後不能重新指定

即可。


2-2. Engine 怎麼傳進 Car?

再看 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

engine

GasEngine engine

是 Constructor Parameter。

用途:

接收外面傳進來的 GasEngine Object。

_engine

private 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

2-3. 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(組合)。


3. 如果 Engine 有很多種怎麼辦?

目前 Car 寫的是:

private readonly GasEngine _engine;

代表:

Car
→ 只能使用 GasEngine

但未來可能還有:

GasEngine
ElectricEngine

兩種 Engine 雖然不同,但都有一個共同需求:

都要可以 Start()

我們希望 Car 表達的是:

我不在乎是哪一種 Engine,只要能 Start() 就可以。

這時就可以使用 Interface。


4. 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。


5. Class 如何實作 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

6. Car 改成使用 IEngine

原本:

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 裡保存一個 IEngine Type 的 Field。

而這個 Field 實際可以參考:

GasEngine Object
或
ElectricEngine Object

因為兩者都實作:

IEngine

所以:

GasEngine
→ 可以傳進 Car

ElectricEngine
→ 也可以傳進 Car

可以先理解成:

Car
→ 不限定是哪一種 Engine

Car
→ 只要求它符合 IEngine

7. 為什麼這樣更有彈性?

先使用 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 帶來的重要好處:

只要符合相同規則,不同實作就可以替換。


8. Interface 和 Composition 怎麼搭配?

現在把兩個概念放在一起。

Interface

IEngine
→ 定義規則
→ 一定要有 Start()

Composition

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()

9. Inheritance 和 Composition 怎麼選?

最簡單的方式,就是先看兩個 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。


10. 完整範例

IEngine

interface IEngine
{
    void Start();
}

GasEngine

class GasEngine : IEngine
{
    public void Start()
    {
        Console.WriteLine("Gas Engine Start");
    }
}

ElectricEngine

class ElectricEngine : IEngine
{
    public void Start()
    {
        Console.WriteLine("Electric Engine Start");
    }
}

Car

class Car
{
    private readonly IEngine _engine;

    public Car(IEngine engine)
    {
        _engine = engine;
    }

    public void Start()
    {
        Console.WriteLine("Car Start");

        _engine.Start();
    }
}

使用 GasEngine

IEngine engine =
    new GasEngine();

Car car =
    new Car(engine);

car.Start();

結果:

Car Start
Gas Engine Start

換成 ElectricEngine

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()

補充|Interface 也能搭配 Polymorphism

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

Day 15 小結

今天最重要的是三個概念。

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 提供的功能。


上一篇
Day 14|Polymorphism 與 Abstract:相同 Type,不同行為
下一篇
Day 16|List<T>:管理會變動的一組資料
系列文
現在就學C# 與 ASP.NET Core 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言