Samouczek: aktualizowanie interfejsów przy użyciu domyślnych metod interfejsu

Implementację można zdefiniować podczas deklarowania elementu członkowskiego interfejsu. Najbardziej typowym scenariuszem jest bezpieczne dodawanie członków do interfejsu, który został już wydany i używany przez niezliczonych klientów.

Z tego samouczka dowiesz się, jak wykonywać następujące czynności:

  • Bezpieczne rozszerzanie interfejsów przez dodawanie metod za pomocą implementacji.
  • Utwórz sparametryzowane implementacje, aby zapewnić większą elastyczność.
  • Włącz implementacje, aby zapewnić bardziej szczegółową implementację w postaci przesłonięcia.

Wymagania wstępne

Musisz skonfigurować maszynę do uruchamiania platformy .NET, w tym kompilatora języka C#. Kompilator języka C# jest dostępny w programie Visual Studio 2022 lub zestawie .NET SDK.

Omówienie scenariusza

Ten samouczek rozpoczyna się od wersji 1 biblioteki relacji klienta. Aplikację startową można pobrać w repozytorium przykładów w witrynie GitHub. Firma, która utworzyła tę bibliotekę, zamierzyła klientów z istniejącymi aplikacjami, aby wdrożyć swoją bibliotekę. Udostępniali minimalne definicje interfejsów dla użytkowników biblioteki do zaimplementowania. Oto definicja interfejsu klienta:

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

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

Zdefiniowali drugi interfejs reprezentujący kolejność:

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

Dzięki tym interfejsom zespół może utworzyć bibliotekę dla swoich użytkowników, aby stworzyć lepsze środowisko dla swoich klientów. Ich celem było stworzenie głębszej relacji z istniejącymi klientami i ulepszenie relacji z nowymi klientami.

Teraz nadszedł czas, aby uaktualnić bibliotekę na potrzeby następnej wersji. Jedna z żądanych funkcji zapewnia rabat lojalnościowy dla klientów, którzy mają wiele zamówień. Ten nowy rabat lojalnościowy jest stosowany za każdym razem, gdy klient składa zamówienie. Określony rabat jest właściwością każdego klienta. Każda implementacja ICustomer programu może ustawić różne reguły rabatu lojalnościowego.

Najbardziej naturalnym sposobem dodania tej funkcji jest ulepszenie interfejsu ICustomer za pomocą metody stosowania rabatu lojalnościowego. Ta sugestia projektowa spowodowała obawy doświadczonych deweloperów: "Interfejsy są niezmienne po ich wydaniu! Nie wprowadzaj zmiany powodującej niezgodność!" Do uaktualniania interfejsów należy używać domyślnych implementacji interfejsów. Autorzy bibliotek mogą dodawać nowe elementy członkowskie do interfejsu i zapewniać domyślną implementację dla tych elementów członkowskich.

Implementacje interfejsu domyślnego umożliwiają deweloperom uaktualnianie interfejsu, jednocześnie umożliwiając wszystkim implementatorom zastąpienie tej implementacji. Użytkownicy biblioteki mogą zaakceptować domyślną implementację jako zmianę, która nie ulega zmianie. Jeśli ich reguły biznesowe są inne, mogą one zastąpić.

Uaktualnianie przy użyciu domyślnych metod interfejsu

Zespół zgodził się na najbardziej prawdopodobną domyślną implementację: rabat lojalnościowy dla klientów.

Uaktualnienie powinno zapewnić funkcjonalność ustawiania dwóch właściwości: liczby zamówień potrzebnych do kwalifikowania się do rabatu oraz procentu rabatu. Te funkcje sprawiają, że jest to idealny scenariusz dla domyślnych metod interfejsu. Możesz dodać metodę do interfejsu ICustomer i zapewnić najbardziej prawdopodobną implementację. Wszystkie istniejące i wszystkie nowe implementacje mogą używać implementacji domyślnej lub udostępniać własne.

Najpierw dodaj nową metodę do interfejsu, w tym treść metody:

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

Autor biblioteki napisał pierwszy test w celu sprawdzenia implementacji:

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()}");

Zwróć uwagę na następującą część testu:

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

To rzut z SampleCustomer na ICustomer jest konieczne. Klasa SampleCustomer nie musi dostarczać implementacji dla ComputeLoyaltyDiscountelementu ; jest to dostarczane przez ICustomer interfejs. Jednak SampleCustomer klasa nie dziedziczy składowych ze swoich interfejsów. Ta reguła nie uległa zmianie. Aby wywołać dowolną metodę zadeklarowaną i zaimplementowaną w interfejsie, zmienna musi być typem interfejsu, ICustomer w tym przykładzie.

Podaj parametryzacja

Implementacja domyślna jest zbyt restrykcyjna. Wielu konsumentów tego systemu może wybrać różne progi dla liczby zakupów, innej długości członkostwa lub innego rabatu procentowego. Możesz zapewnić lepsze środowisko uaktualniania dla większej liczby klientów, zapewniając sposób ustawiania tych parametrów. Dodajmy metodę statyczną, która ustawia te trzy parametry kontrolujące implementację domyślną:

// 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;
}

Istnieje wiele nowych możliwości języka pokazanych w tym małym fragmentcie kodu. Interfejsy mogą teraz zawierać statyczne elementy członkowskie, w tym pola i metody. Różne modyfikatory dostępu są również włączone. Inne pola są prywatne, nowa metoda jest publiczna. Każdy z modyfikatorów jest dozwolony w elementach członkowskich interfejsu.

Aplikacje korzystające z formuły ogólnej do obliczania rabatu lojalnościowego, ale różne parametry, nie muszą zapewniać implementacji niestandardowej; mogą one ustawiać argumenty za pomocą metody statycznej. Na przykład poniższy kod określa "uznanie dla klientów", które nagradza każdego klienta z ponad jednym miesiącem członkostwa:

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

Rozszerzanie implementacji domyślnej

Dodany do tej pory kod dostarczył wygodnej implementacji dla tych scenariuszy, w których użytkownicy chcą czegoś takiego jak implementacja domyślna lub udostępnić niepowiązany zestaw reguł. W przypadku ostatniej funkcji refaktoryzujmy nieco kod, aby umożliwić scenariusze, w których użytkownicy mogą chcieć opierać się na domyślnej implementacji.

Rozważmy startup, który chce przyciągnąć nowych klientów. Oferują 50% zniżki od pierwszego zamówienia nowego klienta. W przeciwnym razie istniejący klienci otrzymują rabat standardowy. Autor biblioteki musi przenieść domyślną implementację protected static do metody, aby każda klasa implementujący ten interfejs mogła ponownie użyć kodu w implementacji. Domyślna implementacja elementu członkowskiego interfejsu wywołuje również tę udostępnioną metodę:

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;
}

W implementacji klasy, która implementuje ten interfejs, przesłonięcia mogą wywoływać statyczną metodę pomocnika i rozszerzać tę logikę w celu zapewnienia rabatu "nowego klienta":

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

Cały gotowy kod można zobaczyć w repozytorium przykładów w witrynie GitHub. Aplikację startową można pobrać w repozytorium przykładów w witrynie GitHub.

Te nowe funkcje oznaczają, że interfejsy można bezpiecznie aktualizować, gdy istnieje rozsądna implementacja domyślna dla tych nowych elementów członkowskich. Starannie projektuj interfejsy, aby wyrazić pojedyncze pomysły funkcjonalne implementowane przez wiele klas. Ułatwia to uaktualnienie tych definicji interfejsu po odnalezieniu nowych wymagań dla tego samego pomysłu funkcjonalnego.