[Перевод] Гайд: обновление интерфейсов с помощью дефолтных членов в C# 8.0
Начав работу с C# 8.0 на .NET Core 3.0, при создании члена интерфейса вы можете определить его реализацию. Наиболее распространенным сценарием является безопасное добавление членов к интерфейсу, уже выпущенному и используемому бесчисленным количеством клиентов.
В этом гайде вы узнаете как:
- Безопасно расширять интерфейсы путем добавления методов с реализациями.
- Создавать реализации с параметрами для обеспечения большей гибкости.
- Получить право осуществлять более специфичные реализации с возможностью ручного контроля.
С чего начать?
Сперва вам необходимо настроить машину для работы с .NET Core, включая компилятор C# 8.0 с предварительным просмотром. Такой компилятор доступен начиная с Visual Studio 2019, или с более новых .NET Core 3.0 preview SDK. Дефолтные члены интерфейса доступны начиная с .NET Core 3.0 (Preview 4).
Обзор сценария
Этот туториал начинается с первой версии библиотеки взаимоотношения с клиентом. Вы можете получить стартовое приложение в нашем репозитории на GitHub. Компания, создавшая эту библиотеку, предполагала, что клиенты с существующими приложениями адаптируют их под эту библиотеку. Пользователям были представлены минимальные определения интерфейса для реализации:
public interface ICustomer
{
IEnumerable PreviousOrders { get; }
DateTime DateJoined { get; }
DateTime? LastOrder { get; }
string Name { get; }
IDictionary Reminders { get; }
}
Также был определен второй интерфейс, который показывает порядок:
public interface IOrder
{
DateTime Purchased { get; }
decimal Cost { get; }
}
На основе этих интерфейсов команда может собрать библиотеку для своих пользователей, чтобы создать лучший опыт для клиентов. Целью команды являлось повышение уровня взаимодействия с существующими клиентами и развитие отношений с новыми.
Пришло время обновить библиотеку для следующего релиза. Одна из наиболее востребованных особенностей — добавление скидки лояльным клиентам, делающих большое количество заказов. Эта новая индивидуальная скидка применяется каждый раз, когда клиент делает заказ При каждой реализации ICustomer могут задаваться различные правила для скидки за лояльность.
Наиболее удобный способ добавления этой функции — расширить интерфейс ICustomer
с применением любых скидок. Это предложение вызвало обеспокоенность у опытных разработчиков. «Интерфейсы являются неизменяемыми после выпуска! Это критическое изменение!» В C# 8.0 добавлены дефолтные реализации интерфейсов для обновления интерфейсов. Авторы библиотек могут добавлять новые члены и реализовывать их по умолчанию
Реализация интерфейсов по умолчанию позволяют разработчикам обновить интерфейс, по-прежнему позволяя другим разработчикам переопределять эту реализацию. Пользователи библиотеки могут принимать реализацию по умолчанию в качестве некритического изменения.
Обновление с использованием членов интерфейса по умолчанию
Команда согласилась с наиболее вероятной реализацией по умолчанию: скидка за лояльность для клиентов.
Обновление должно быть функциональным для установки двух свойств: количества заказов, необходимых для получения скидки, и процента скидки. Это делает его идеальным сценарием для членов интерфейса по умолчанию. Вы можете добавить метод в интерфейс ICustomer и предоставить его наиболее вероятную реализацию. Все существующие и любые новые реализации могут осуществляться по умолчанию или иметь свои настройки.
Сначала добавьте новый метод к реализации:
// Версия 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);
// Проверка скидки:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
Обратите внимание на следующую часть теста:
// Проверка скидки:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
Это часть, начиная с SampleCustomer
и заканчивая ICustomer
важна. Класс SampleCustomer
не должен предоставлять реализацию для ComputeLoyaltyDiscount
; это обеспечивается интерфейсом ICustomer
. Однако класс SampleCustomer
не наследует члены от своих интерфейсов. Это правило не изменилось. Для вызова любого метода, реализованного в интерфейсе, переменная должна быть типом интерфейса, в этом примере — ICustomer
.
Параметризация
Это хорошее начало. Но реализация по умолчанию слишком ограничена. Многие потребители этой системы могут выбрать разные пороговые значения для количества покупок, разную продолжительность членства или разную скидку в процентах.Вы можете улучшить процесс обновления для большего количества клиентов, предоставив способ установки этих параметров. Давайте добавим статический метод, который устанавливает эти три параметра, управляющие реализацией по умолчанию:
// Версия 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.
Эти новые функции означают, что интерфейсы могут быть безопасно обновлены, если для новых членов есть приемлемая реализация по умолчанию. Тщательно проектируйте интерфейсы, чтобы выразить отдельные функциональные идеи, которые могут быть реализованы несколькими классами. Это облегчает обновление этих определений интерфейса при обнаружении новых требований для той же функциональной идеи.