Home [C# 筆記] Interface 接口(介面)實現
Post
Cancel

[C# 筆記] Interface 接口(介面)實現

[C# 筆記] Interface 什麼是接口(介面)?

1
2
OrderPressor --> IPriceCalculator
訂單處理系統  依賴於  價格計算接口(介面)
1
2
3
IPriceCalculator --> Price_1111 (雙11價格計算系統)
價格計算接口(介面)  --> Price_618  (618價格計算系統)
                 --> PriceNormal (正常價格計算系統)

範例

比如說,現在「雙11」大促銷,所有的訂單都免費,那麼我們應該怎麼改動呢?

我們可以直接在 ShippingCalculatorCalculatesShipping方法中,直接讓它的結果 return 0 嗎?

如果直接在這裡修改了程式碼,那「雙11」結束之後,難道我們還要再改回來?

1
2
3
4
5
6
7
8
9
10
//計算訂單的運費
public class ShippingCalculator
{
    public int CalculatesShipping(Order order)
    {
        //訂單超過$50免運,否則運費$10
        if (order.TotalPrice < 50) return 10;
        return 0;
    }
}

顯然,直接在ShippingCalculator中修改程式碼,是一種非常低級的操作,更好的選擇是:針對「雙11」的促銷,我們創建一個新的運費計算類,可以叫做「DoubleElevenShippingCalculator」。

而這個「雙11」價格計算類中,也同樣需要一個運費計算的方法,而他的計算過程直接 return 0 就可以了。

1
2
3
4
5
6
//「雙11」計算運費系統
public class DoubleElevenShippingCalculator {
    public int CalculatesShipping(Order order) {
        return 0;
    }
}

回到「訂單處理系統」OrderProcessor,我們該如何使用新的「雙11運費計算系統」呢?

方案一:(這個方式感覺很蠢)

  • 創建一個私有字段,不要使用 ShippingCalculator,而是使用 DoubleElevenShippingCalculator
  • 同時在構造方法中,也為私有的「雙11運費計算系統」_doubleElevenShippingCalculator創建一個新實體
  • 然後在 Process() 中,通過判斷時間來決定使用哪一個類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//訂單處理系統
public class OrderProcessor
{
    private readonly ShippingCalculator _shippingCalculator;
    private readonly DoubleElevenShippingCalculator _doubleElevenShippingCalculator;

    public OrderProcessor()
    {
        //在創建OrderProcessor的時候 實體化ShippingCalculator
        _shippingCalculator = new ShippingCalculator();
        _doubleElevenShippingCalculator = new DoubleElevenShippingCalculator();
    }
    .....
}

比如說,通過一個datetime 來確定今天是不是雙11,如果今天是雙11,那麼我們就使用雙11的價格計算器…,通過這樣的處理,我們也可以實現雙11價格計算、和普通價格計算處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void Process(Order order)
{
    //先判斷訂單是否已經被處理過
    if (order.IsShipped)
        throw new InvalidOperationException("訂單已出貨");
    //雙11
    DateTime doubleEvelen = new DateTime(2023, 11, 11);

    //如果訂單狀態正常,將會開始處理訂單,建立發貨信息
    order.Shipment = new Shipment {
        Cost =DateTime.Now == doubleEvelen? _doubleElevenShippingCalculator.CalculatesShipping(order): _shippingCalculator.CalculatesShipping(order),
        ShippingDate = DateTime.Today.AddDays(1)
    };
     Console.WriteLine($"訂單#{order.Id}完成,已出貨");
}

不過這個方式感覺很蠢,不僅多了很多程式碼,而且還把「雙11」的判斷機制交給了 Order Process,反而讓整個業務邏輯變得更加臃腫、更加繁瑣了,所以這種方法是肯定行不通的。

方案二:使用接口(介面)

使用接口來剝離「order processor」 vs 「shipping calculator」 之間的關係。

比如說,「餐廳 vs 廚師」的例子,餐廳需要的是一個可以炒菜的人,我不在乎這個人是小明還是小張、我也不在乎他長得是高是矮、是胖還是瘦。

我們把廚師看作是一個類,只要是這個廚師的對象(物件),是能夠按照我們的需求執行炒菜這個方法就可以了。

同樣的道理,計算運費,我不在乎你是「雙11計算」還是「普通計算」,只要能夠給訂單提供正確的方法就可以啦。

所以對於 order processor 來說,只要它可以處理訂單的過程,調用shipping calculator這個方法就解決了。

那麼為了達到個目的,我們可以把運費的計算過程全部抽象出來,放在一個接口(介面)中,這個接口(介面)就叫作「IShippingCalculator」。

接下來,所有與運費相關的計算,都將會滿足這個 shipping calculator接口(介面)標準。

而這個接口(介面)、這個標準,我們只需要包含 shipping calculate 這個方法就足夠了。

1
2
3
4
public interface IShippingCalculator
{
    int CalculateShipping(Order order);
}

那麼,接下來我們的「雙11 double eleven」 和 「普通的 shipping calculator」只要符合這個接口(介面)的定義就可以了。

「雙11計算運費」實現接口(介面)

1
2
3
4
5
6
7
8
//「雙11」計算運費
public class DoubleElevenShippingCalculator : IShippingCalculator
{
    public int CalculateShipping(Order order)
    {
        return 0;
    }
}

「普通計算運費」實現接口(介面)

1
2
3
4
5
6
7
8
9
10
//計算訂單的運費
public class ShippingCalculator : IShippingCalculator
{
    public int CalculateShipping(Order order)
    {
        //訂單超過$50免運,否則運費$10
        if (order.TotalPrice < 50) return 10;
        return 0;
    }
}

回到 OrderProcessor,現在我們就可以把系統依賴,從運費計算系統調整為面向運費計算標準了。

接下來把 ShippingCalculator 改為接口(介面),同時更改我們的構造方法,而我們真正的運費計算過程,則是通過構造方法的參數,傳遞到訂單處理系統中,而傳遞過程中依然使用 interface IShippingCalculator,讓外部傳入的IShippingCalculator等於私有成員變量_shippingCalculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//訂單處理系統
public class OrderProcessor
{
    private readonly IShippingCalculator _shippingCalculator;

    public OrderProcessor(IShippingCalculator shippingCalculator)
    {
        //在創建OrderProcessor的時候,讓外部傳入的IShippingCalculator 等於私有成員變量_shippingCalculator
        _shippingCalculator = shippingCalculator;
    }
    public void Process(Order order)
        {
            //先判斷訂單是否已經被處理過
            if (order.IsShipped)
                throw new InvalidOperationException("訂單已出貨");

            //如果訂單狀態正常,將會開始處理訂單,建立發貨信息
            order.Shipment = new Shipment {
                Cost = _shippingCalculator.CalculateShipping(order),
                ShippingDate = DateTime.Today.AddDays(1)
            };
            Console.WriteLine($"訂單#{order.Id}完成,已出貨");
        }
}

現在我們可以通過使用IShippingCalculator來同時指代「普通的ShippingCalculator」或是「雙11的ShippingCalculator」。

通過使用接口(介面),使用一定的標準,我們可以很好的把兩種不同的運費計算過程統一起來。

而使用接口(介面),不會對當前的訂單處理系統產生任何影響。而對於訂單處理系統來說,運費的計算跟它是沒有關係的,他不需要關心運費的計算過程,只需要把運費的計算結果放在訂單中就可以了。

而整個運費的計算過程對於「訂單處理系統」來說,屬於黑箱操作,用這樣的方式,我們的運費和訂單的耦合就被解開了。

不過我們還差最後一步,回到Main方法。

我們還需要創建「運費計算系統」,並把「運費計算系統」傳遞到這個「訂單系統」中。

首先,我們先來嘗試一下「雙11計算價格接口(介面)」

1
2
3
//「雙11」計算運費
IShippingCalculator doubleEleven = new DoubleElevenShippingCalculator();
var orderProcessor = new OrderProcessor(doubleEleven);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
    var order = new Order {
        Id = 123,
        DatePlaced = DateTime.Now,
        TotalPrice = 100
    };

    //「雙11」計算運費
    IShippingCalculator doubleEleven = new DoubleElevenShippingCalculator();
    var orderProcessor = new OrderProcessor(doubleEleven);
    orderProcessor.Process(order);

    Console.Read();
}

這時候我們可能就會想:我們在實體化orderProcessor的時候,還不是得把「價格系統」傳遞進去嗎?所以「訂單處理系統 orderProcessor」和「價格系統」同樣還是會有依賴關係嘛,那麼這個答案「是」也「不是」。

「訂單系統」的確與「價格系統」依然是有依賴關係的,但是「訂單系統」依賴的不再是「價格系統」,而是「價格系統」的接口(介面)。

也就是說,誰可以給我「訂單系統」提供價格計算,我就依賴於誰。

而這種依賴關係,僅僅存在於 Main方法這個邏輯操作的層面上,而在兩個系統的業務邏輯上,是不存在任何依賴關係的。

比如說,現有「雙11」活動結束了,我們的價格系統要怎樣才能變回原本的價格呢?那其實這個就很簡單了,與「雙11價格」類似。

我們在 Main方法 中創建一個「普通的日常價格系統」,類型為接口(介面),然後使一個if語句來判斷今天的日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void Main(string[] args)
{
    var order = new Order {
        Id = 123,
        DatePlaced = DateTime.Now,
        TotalPrice = 100
    };

    IShippingCalculator normal = new ShippingCalculator();//日常價格
    IShippingCalculator doubleEleven = new DoubleElevenShippingCalculator();//「雙11」計算運費
    var orderProcessor = new OrderProcessor(doubleEleven);

    //如果不是雙11,就使用普通計算價格
    if (DateTime.Now != new DateTime(2023, 11, 11)) {
        orderProcessor = new OrderProcessor(normal);
    }
    orderProcessor.Process(order);

    Console.Read();
}

完整Code

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//計算運費介面
public interface IShippingCalculator
{
    int CalculateShipping(Order order);
}
//計算訂單的運費(日常)
public class ShippingCalculator : IShippingCalculator
{
    public int CalculateShipping(Order order)
    {
        //訂單超過$50免運,否則運費$10
        if (order.TotalPrice < 50) return 10;
        return 0;
    }
}
//「雙11」計算運費
public class DoubleElevenShippingCalculator : IShippingCalculator
{
    public int CalculateShipping(Order order)
    {
        return 0;
    }
}
//訂單
public class Order
{
    public int Id { get; set; }
    public DateTime DatePlaced { get; set; }
    public decimal TotalPrice { get; set; }
    public bool IsShipped { get; set; }
    public Shipment Shipment { get; set; }
}
//訂單處理系統
public class OrderProcessor
{
    private readonly IShippingCalculator _shippingCalculator;

    public OrderProcessor(IShippingCalculator shippingCalculator)
    {
        //在創建OrderProcessor的時候,讓外部傳入的IShippingCalculator 等於私有成員變量_shippingCalculator
        _shippingCalculator = shippingCalculator;
    }
    public void Process(Order order)
    {
        //先判斷訂單是否已經被處理過
        if (order.IsShipped)
            throw new InvalidOperationException("訂單已出貨");

        //如果訂單狀態正常,將會開始處理訂單,建立發貨信息
        order.Shipment = new Shipment {
            Cost = _shippingCalculator.CalculateShipping(order),
            ShippingDate = DateTime.Today.AddDays(1)
        };
        Console.WriteLine($"訂單#{order.Id}完成,已出貨");
    }
}
//發貨信息
public class Shipment
{
    public int Cost { get; set; } //運費
    public DateTime ShippingDate { get; set; }
}

internal class Program
{
    static void Main(string[] args)
    {
        var order = new Order {
            Id = 123,
            DatePlaced = DateTime.Now,
            TotalPrice = 100
        };

        IShippingCalculator normal = new ShippingCalculator();//日常價格
        IShippingCalculator doubleEleven = new DoubleElevenShippingCalculator();//「雙11」計算運費
        var orderProcessor = new OrderProcessor(doubleEleven);

        //如果不是雙11,就使用普通計算價格
        if (DateTime.Now != new DateTime(2023, 11, 11)) {
            orderProcessor = new OrderProcessor(normal);
        }
        orderProcessor.Process(order);

        Console.Read();
    }
}

https://www.bilibili.com/video/BV1Ss4y1B7zE?p=38

This post is licensed under CC BY 4.0 by the author.

[C# 筆記] 多重繼承 vs 多重實現

[C# 筆記] 接口(介面) vs 多態(多型)