教程:使用默认接口方法更新接口

可以在声明接口成员时定义实现。 最常见的方案是将成员安全地添加到已释放并供无数客户端使用的接口。

本教程介绍以下操作:

  • 通过添加具有实现的方法安全地扩展接口。
  • 创建参数化实现以提供更大的灵活性。
  • 使实现者能够以替代的形式提供更具体的实现。

先决条件

需要将计算机设置为运行 .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()}");

SampleCustomerICustomer 的强制转换是必需的。 类 SampleCustomer 不需要提供 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;
}

在实现此接口的类的实现中,替代可以调用静态帮助程序方法,并扩展该逻辑以提供“新客户”折扣:

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

可以在 GitHub 上的示例存储库中查看完整的已完成代码。 可以在 GitHub 上的示例存储库上获取初学者应用程序。

这些新功能意味着当这些新成员有合理的默认实现时,接口可以安全地更新。 仔细设计接口,以表达由多个类实现的单个功能理念。 这样,当发现针对同一功能理念的新要求时,可以更轻松地升级这些接口定义。