教學課程:使用預設介面方法更新介面

您可以在宣告介面成員時定義實作。 最常見的情節是安全地將成員新增至已發行並由無數個用戶端所使用的介面。

在本教學課程中,您將了解如何:

  • 新增附實作的方法來安全地擴充介面。
  • 建立參數化實作以提高彈性。
  • 讓實作者提供覆寫形式的更特定實作。

必要條件

您需要設定電腦,以執行 .NET (包括 C# 編譯器)。 C# 編譯器可與 Visual Studio 2022.NET SDK 搭配使用。

案例概觀

本教學課程從客戶關係程式庫第 1 版開始。 您可以在 GitHub 的範例存放庫中取得入門應用程式。 建置此程式庫的公司想要讓客戶透過現有應用程式來採用其程式庫。 他們提供可讓其程式庫使用者實作的基本介面定義。 以下是客戶的介面定義:

public interface ICustomer
{
    IEnumerable<IOrder> PreviousOrders { get; }

    DateTime DateJoined { get; }
    DateTime? LastOrder { get; }
    string Name { get; }
    IDictionary<DateTime, string> Reminders { get; }
}

他們定義代表訂單的第二個介面:

public interface IOrder
{
    DateTime Purchased { get; }
    decimal Cost { get; }
}

針對這些介面,小組可以建置適用於其使用者的程式庫,來為其客戶打造更好的體驗。 其目標是加深與現有客戶的關係,並改善與新客戶的關係。

現在,您可以將程式庫升級到下一版。 其中一項所要求功能是讓擁有許多訂單的客戶享有忠誠度折扣。 此新的忠誠度折扣會在每次客戶下單時套用。 此特定折扣屬於每個個別客戶。 ICustomer 的每個實作都可為忠誠度折扣設定不同規則。

新增此功能的最自然方式,就是使用方法來增強 ICustomer 介面以套用任何忠誠度折扣。 此設計建議在有經驗的開發人員之間造成疑慮:「發行介面之後,介面就不可變! 不要進行中斷性變更!」您應該使用預設介面實作來升級介面。 程式庫作者可以將新成員新增至介面,並為這些成員提供預設實作。

預設介面實作可讓開發人員升級介面,同時仍可讓任何實作者覆寫該實作。 程式庫使用者可以接受預設實作作為非中斷性變更。 如果他們的商務規則不同,則可以進行覆寫。

使用預設介面方法升級

小組同意最有可能的預設實作,就是客戶的忠誠度折扣。

升級應該提供設定兩個屬性的功能:必須符合折扣資格的訂單數目,以及折扣百分比。 這些功能使其非常適合使用預設介面方法。 您可以將方法新增至 ICustomer 介面,並提供最有可能的實作。 所有現有及任何新的實作都可使用預設實作,或提供自己的實作。

首先,將新方法新增至介面,包括方法的主體:

// Version 1:
public decimal ComputeLoyaltyDiscount()
{
    DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
    if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
    {
        return 0.10m;
    }
    return 0;
}

程式庫作者撰寫第一個測試來檢查實作:

SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))
{
    Reminders =
    {
        { new DateTime(2010, 08, 12), "childs's birthday" },
        { new DateTime(1012, 11, 15), "anniversary" }
    }
};

SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);
c.AddOrder(o);

o = new SampleOrder(new DateTime(2103, 7, 4), 25m);
c.AddOrder(o);

// Check the discount:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

請注意下列測試部分:

// Check the discount:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

SampleCustomer 必須轉換成 ICustomerSampleCustomer 類別不需要提供 ComputeLoyaltyDiscount 的實作;這會由 ICustomer 介面提供。 不過,SampleCustomer 類別不會從其介面繼承成員。 該規則並未變更。 為了呼叫在介面中宣告和實作的任何方法,變數必須是介面類型,在本例中為 ICustomer

提供參數化

預設實作太過侷限。 此系統的許多取用者可能選擇不同購買數目閾值、不同成員資格長度,或不同分比折扣。 您可以藉由提供設定這些參數的方法,來為更多客戶提供更好的升級體驗。 我們將新增靜態方法,設定這三個參數來控制預設實作:

// Version 2:
public static void SetLoyaltyThresholds(
    TimeSpan ago,
    int minimumOrders = 10,
    decimal percentageDiscount = 0.10m)
{
    length = ago;
    orderCount = minimumOrders;
    discountPercent = percentageDiscount;
}
private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
private static int orderCount = 10;
private static decimal discountPercent = 0.10m;

public decimal ComputeLoyaltyDiscount()
{
    DateTime start = DateTime.Now - length;

    if ((DateJoined < start) && (PreviousOrders.Count() > orderCount))
    {
        return discountPercent;
    }
    return 0;
}

在該小節程式碼片段中,顯示了許多新的語言功能。 介面現在可以包含靜態成員,包括欄位和方法。 也會啟用不同的存取修飾詞。 其他欄位是私用的,而新方法是公用的。 介面成員上允許任何修飾詞。

使用一般公式來計算忠誠度折扣 (但參數不同) 的應用程式不需要提供自訂實作;這些應用程式可透過靜態方法來設定引數。 例如,下列程式碼會設定「客戶感謝」,獎勵其成員資格超過一個月的任何客戶:

ICustomer.SetLoyaltyThresholds(new TimeSpan(30, 0, 0, 0), 1, 0.25m);
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

擴充預設實作

如果使用者想要類似預設實作的其他實作,或是想要提供一組不相關的規則,您到目前為止所新增程式碼為這些情節提供便利的實作。 針對最後一項功能,我們將稍微重構程式碼,讓使用者可以在預設實作上建置。

假設有間新創公司想要吸引新客戶。 該公司提供 50% 折扣給首次下單的新客戶。 至於現有的客戶則享有標準折扣。 程式庫作者必須將預設實作移入 protected static 方法,才能讓實作此介面的任何類別在其實作中重複使用程式碼。 介面成員的預設實作也會呼叫此共用方法:

public decimal ComputeLoyaltyDiscount() => DefaultLoyaltyDiscount(this);
protected static decimal DefaultLoyaltyDiscount(ICustomer c)
{
    DateTime start = DateTime.Now - length;

    if ((c.DateJoined < start) && (c.PreviousOrders.Count() > orderCount))
    {
        return discountPercent;
    }
    return 0;
}

在實作此介面的類別實作中,覆寫可以呼叫靜態 Helper 方法,並擴充該邏輯以提供「新客戶」折扣:

public decimal ComputeLoyaltyDiscount()
{
   if (PreviousOrders.Any() == false)
        return 0.50m;
    else
        return ICustomer.DefaultLoyaltyDiscount(this);
}

您可以在 GitHub 上的範例存放庫 \(英文\) 中查看已完成的完整程式碼。 您可以在 GitHub 的範例存放庫中取得入門應用程式。

這些新功能表示介面可在這些新成員有合理的預設實作時安全地更新。 請謹慎設計介面來表達可由多個類別實作的單一功能概念。 這讓您可以在發現該相同功能概念有新需求時,更輕鬆地升級這些介面定義。