チュートリアル: 既定のインターフェイス メソッドを使用してインターフェイスを更新する

インターフェイスのメンバーを宣言するときに実装を定義できます。 最も一般的なシナリオは、数え切れないほどのクライアントから既にリリースされ、使用されているインターフェイスにメンバーを安全に追加することです。

このチュートリアルでは、次の作業を行う方法について説明します。

  • 実装を含むメソッドを追加して、インターフェイスを安全に拡張します。
  • パラメーター化された実装を作成して、柔軟性を高めます。
  • 実装者がオーバーライドの形でより具体的な実装を提供できるようにします。

必須コンポーネント

C# コンパイラを含め、.NET が実行されるようにコンピューターを設定する必要があります。 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; }
}

注文を表す 2 つ目のインターフェイスを定義しました。

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

チームは、これらのインターフェイスからユーザー向けのライブラリを構築し、顧客にとってより良いエクスペリエンスを作り出すことができました。 目標は、既存の顧客との関係を深め、新しい顧客との関係を改善することでした。

次回のリリースのためにライブラリをアップグレードする時期になりました。 求められている機能の 1 つは、注文数が多い顧客向けにロイヤルティ割引を有効にすることです。 この新しいロイヤルティ割引は、顧客が注文するたびに適用されます。 この特定の割引は、各顧客のプロパティです。 ICustomer の各実装で、ロイヤルティ割引に対して異なるルールを設定できます。

この機能を追加する最も自然な方法は、ロイヤルティ割引を適用するメソッドを使用して ICustomer インターフェイスを拡張することです。 この設計の提案から、経験豊富な開発者の間で次のような懸念が起こりました。「リリース済みのインターフェイスは変更できません。 破壊的変更を行わないでください。"インターフェイスのアップグレードには既定のインターフェイス実装を使用する必要があります。 ライブラリ作成者はインターフェイスに新しいメンバーを追加し、それらのメンバーに既定の実装を指定することができます。

既定のインターフェイス実装を使用すると、開発者がインターフェイスをアップグレードできるだけでなく、すべての実装者がその実装をオーバーライドできるようになります。 ライブラリのユーザーは、非破壊的変更として既定の実装を受け入れることができます。 ビジネス ルールが異なる場合は、オーバーライドできます。

既定のインターフェイス メソッドを使用してアップグレードする

チームは、最も可能性の高い既定の実装、つまり顧客に対するロイヤルティ割引について合意しました。

アップグレードでは、2 つのプロパティを設定する機能を提供する必要があります。割引の対象となるために必要な注文数と、割引の割合です。 この機能は、既定のインターフェイス メソッドに最適なシナリオです。 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) 似する必要があります。

パラメーター化を提供する

既定の実装は制限が厳しすぎます。 このシステムの多くの利用者は、異なる購入数のしきい値、異なる長さのメンバーシップ、または異なる割引率を選択する可能性があります。 これらのパラメーターを設定する方法を用意することで、より多くの顧客とって優れたアップグレード エクスペリエンスを提供できます。 既定の実装を制御するこれら 3 つのパラメーターを設定する静的メソッドを追加しましょう。

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

このわずかなコード フラグメントには、新しい言語機能が多数見られます。 インターフェイスには、フィールドやメソッドなどの静的メンバーを含めることができるようになりました。 さまざまなアクセス修飾子も使用できます。 他のフィールドはプライベートであり、新しいメソッドはパブリックです。 どの修飾子もインターフェイス メンバーに使用できます。

ロイヤルティ割引の計算に一般的な数式を使用する (ただしパラメーターは異なる) アプリケーションでは、カスタム実装を用意する必要がありません。静的メソッドを介して引数を設定できます。 たとえば、次のコードでは、メンバーシップが 1 か月を超えるすべての顧客に報酬を与える "顧客感謝" を設定します。

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 上の サンプル リポジトリでスターター アプリケーションを入手できます。

これらの新機能は、新しいメンバーに妥当な既定の実装がある場合に、インターフェイスを安全に更新できることを意味します。 複数のクラスから実装される 1 つの機能的なアイデアを表現するように、慎重にインターフェイスをデザインしてください。 その結果、同じ機能的なアイデアに対して新しい要件が見つかったときに、そのインターフェイス定義を簡単にアップグレードできるようになります。