C# 8.0で導入された「インターフェイスの既定の実装(Default Interface Methods)」は、インターフェイス自体にロジックを持たせることを可能にしました。
通常、実装クラス側で同じメソッドを定義すると、インターフェイスの既定実装は完全に無視(上書き)されます。しかし、**「特定の条件では独自のロジックを使い、それ以外ではインターフェイスの既定ロジックを使いたい」**というケースも存在します。クラス継承における base.Method() のような呼び出しはインターフェイスではできないため、これには少し工夫が必要です。
今回は、protected static メソッドを活用して、既定の実装を再利用可能な形でオーバーライドする設計パターンについて解説します。
課題:base呼び出しができない
クラス継承であれば、オーバーライドしたメソッド内で base.Method() を呼び出すことで親クラスの振る舞いを再利用できます。しかし、インターフェイスの場合、base キーワードは使えません。
そのため、インターフェイスの既定ロジックを実装クラスから呼び出せるようにするには、ロジック部分を静的メソッドとして切り出す必要があります。
実践的なコード例:会員期間に応じた割引率の計算
以下のコードは、会員ランクに基づく標準の割引計算ロジック(インターフェイス側)と、長期契約者に対する優待ロジック(クラス側)を組み合わせる例です。
IMembership インターフェイス内に protected static なメソッドを定義し、計算ロジックを共有している点に注目してください。
using System;
namespace MembershipSystem
{
// 会員インターフェイス
public interface IMembership
{
string MemberId { get; }
int Rank { get; } // 1:一般, 2:シルバー, 3:ゴールド
// 1. 公開される既定のメソッド
// 実装クラスが何もしなければ、このメソッドが呼ばれます。
// 内部で静的メソッドに委譲しています。
decimal GetDiscountRate() => CalculateStandardDiscount(this);
// 2. 再利用可能なロジックの実体(protected static)
// 実装クラスからもアクセス可能ですが、外部(public)には公開されません。
protected static decimal CalculateStandardDiscount(IMembership member)
{
return member.Rank switch
{
2 => 0.05m, // シルバー: 5%
3 => 0.10m, // ゴールド: 10%
_ => 0.00m // 一般: 0%
};
}
}
// 長期会員クラス
public class LoyalMember : IMembership
{
public string MemberId { get; }
public int Rank { get; set; }
public DateTime JoinDate { get; set; }
public LoyalMember(string id, int rank, DateTime joinDate)
{
MemberId = id;
Rank = rank;
JoinDate = joinDate;
}
// 3. インターフェイスのメソッドを独自実装(上書き)
public decimal GetDiscountRate()
{
// 契約から3年以上経過している場合は、一律15%割引(独自ロジック)
// DateTime.Today は実行日によって変わるため、ここでは固定日で判定ロジックを示します。
var threeYearsAgo = DateTime.Today.AddYears(-3);
if (JoinDate <= threeYearsAgo)
{
return 0.15m;
}
// 4. 条件に合致しない場合は、インターフェイスの標準ロジックを再利用
// 静的メソッドとして直接呼び出します。
return IMembership.CalculateStandardDiscount(this);
}
}
class Program
{
static void Main()
{
// 3年以上経過している会員(ランクは一般)
var veteran = new LoyalMember("M001", 1, DateTime.Today.AddYears(-5));
// 登録したばかりの会員(ランクはゴールド)
var newbie = new LoyalMember("M002", 3, DateTime.Today.AddMonths(-1));
Console.WriteLine("--- 割引率の計算結果 ---");
// ベテラン会員:独自の判定により 15%
Console.WriteLine($"ID:{veteran.MemberId} (Veteran) -> {veteran.GetDiscountRate():P0}");
// 新規会員:標準ロジックへのフォールバックにより 10%
Console.WriteLine($"ID:{newbie.MemberId} (Newbie) -> {newbie.GetDiscountRate():P0}");
}
}
}
実行結果
--- 割引率の計算結果 ---
ID:M001 (Veteran) -> 15%
ID:M002 (Newbie) -> 10%
技術的なポイント
1. protected static メソッドの役割
インターフェイス内のメソッドに protected static 修飾子を付けることで、以下の効果が得られます。
- protected: インターフェイスを実装するクラス(派生クラスのような位置づけ)からのみアクセス可能となり、外部には公開されません。カプセル化が保たれます。
- static: インスタンスに依存しないメソッドとなるため、
InterfaceName.MethodName(...)の形式で呼び出すことができます。
2. 引数に this を渡す
静的メソッド(CalculateStandardDiscount)はインスタンスの状態(this)を持たないため、引数として IMembership 型のインスタンスを受け取る必要があります。呼び出し側では IMembership.CalculateStandardDiscount(this) のように自分自身を渡すことで、インターフェイス内のロジックに自分のプロパティを参照させることができます。
3. 実装クラスでのメソッド定義
通常、インターフェイスの既定実装を使用する場合、クラス側ではメソッドを定義しません。しかし、今回のように条件分岐を行いたい場合は、クラス側で同名のメソッドを public で定義します。これにより、インターフェイスの実装としてクラス側のメソッドが優先して使用されるようになります。
まとめ
インターフェイスの既定実装は便利ですが、単純に使うだけでは柔軟性に欠ける場合があります。「ロジックの共有」と「個別のカスタマイズ」を両立させたい場合は、今回紹介した 「ロジックを protected static に切り出して委譲するパターン」 を採用してください。これにより、コードの重複を避けつつ、複雑なビジネスルールを表現できます。
