Руководство. Обновление интерфейсов с помощью методов интерфейса по умолчанию

Вы можете определить реализацию при объявлении члена интерфейса. Наиболее распространенным сценарием является безопасное добавление членов в интерфейс, который уже выпущен и используется многочисленными клиентами.

Из этого руководства вы узнаете, как выполнять следующие задачи:

  • Безопасно расширять интерфейсы, добавляя методы с реализациями.
  • Создавать параметризованные реализации, которые обеспечивают большую гибкость.
  • Позволить разработчику реализовать более конкретную реализацию в виде переопределения.

Необходимые компоненты

Необходимо настроить компьютер для запуска .NET, включая компилятор C#. Компилятор C# доступен в Visual Studio 2022 или пакете SDK для .NET.

Обзор сценария

Этот учебник начинается с первой версии библиотеки связи с клиентом. Начальное приложение можно получить в нашем репозитории примеров на сайте 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 к ICustomer необходимо. Классу 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.

Эти новые функции означают, что интерфейсы могут обновляться безопасно при наличии разумной реализации по умолчанию для этих новых элементов. Тщательно проектируйте интерфейсы для выражения отдельных функциональных идей, реализованных несколькими классами. Это упрощает обновление этих определений интерфейса при обнаружении новых требований для этой же функциональности.