[C#] Technique to Override Default Interface Implementations and Reuse Original Logic

The “Default Interface Methods” feature introduced in C# 8.0 made it possible for interfaces themselves to hold logic.

Normally, if you define the same method in the implementing class, the default implementation in the interface is completely ignored (overridden). However, there are cases where you want to say, “Use custom logic under specific conditions, but use the interface’s default logic otherwise.”

Since calls like base.Method() used in class inheritance are not possible with interfaces, this requires a bit of ingenuity.

In this article, I will explain a design pattern that utilizes protected static methods to override default implementations in a reusable way.

目次

The Challenge: Cannot Call base

In class inheritance, you can reuse the parent class’s behavior by calling base.Method() within the overridden method. However, in the case of interfaces, the base keyword cannot be used.

Therefore, to make the interface’s default logic callable from the implementation class, the logic portion needs to be extracted as a static method.

Practical Code Example: Calculating Discount Rates Based on Membership Duration

The following code is an example of combining standard discount calculation logic based on member rank (Interface side) with preferential logic for long-term subscribers (Class side).

Pay attention to how a protected static method is defined within the IMembership interface to share the calculation logic.

using System;

namespace MembershipSystem
{
    // Membership Interface
    public interface IMembership
    {
        string MemberId { get; }
        int Rank { get; } // 1: General, 2: Silver, 3: Gold

        // 1. Public default method
        // If the implementation class does nothing, this method is called.
        // Internally, it delegates to the static method.
        decimal GetDiscountRate() => CalculateStandardDiscount(this);

        // 2. The entity of reusable logic (protected static)
        // Accessible from implementation classes, but not exposed externally (public).
        protected static decimal CalculateStandardDiscount(IMembership member)
        {
            return member.Rank switch
            {
                2 => 0.05m, // Silver: 5%
                3 => 0.10m, // Gold: 10%
                _ => 0.00m  // General: 0%
            };
        }
    }

    // Long-term Member Class
    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. Custom implementation of the interface method (Override)
        public decimal GetDiscountRate()
        {
            // If more than 3 years have passed since the contract, flat 15% discount (Custom Logic)
            // DateTime.Today changes depending on the execution date, so logic is shown with fixed dates here.
            var threeYearsAgo = DateTime.Today.AddYears(-3);
            if (JoinDate <= threeYearsAgo)
            {
                return 0.15m;
            }

            // 4. If conditions are not met, reuse the interface's standard logic
            // Call directly as a static method.
            return IMembership.CalculateStandardDiscount(this);
        }
    }

    class Program
    {
        static void Main()
        {
            // Member with more than 3 years (Rank is General)
            var veteran = new LoyalMember("M001", 1, DateTime.Today.AddYears(-5));
            
            // Member just registered (Rank is Gold)
            var newbie = new LoyalMember("M002", 3, DateTime.Today.AddMonths(-1));

            Console.WriteLine("--- Discount Rate Calculation Results ---");
            
            // Veteran member: 15% due to custom judgment
            Console.WriteLine($"ID:{veteran.MemberId} (Veteran) -> {veteran.GetDiscountRate():P0}");

            // New member: 10% due to fallback to standard logic
            Console.WriteLine($"ID:{newbie.MemberId} (Newbie)  -> {newbie.GetDiscountRate():P0}");
        }
    }
}

Execution Result

--- Discount Rate Calculation Results ---
ID:M001 (Veteran) -> 15%
ID:M002 (Newbie)  -> 10%

Technical Points

1. Role of the protected static Method

By adding the protected static modifier to a method within an interface, you achieve the following effects:

  • protected: Accessible only from classes implementing the interface (similar to derived classes), and not exposed externally. Encapsulation is maintained.
  • static: Since it becomes a method independent of the instance, it can be called in the format InterfaceName.MethodName(...).

2. Passing this as an Argument

Since the static method (CalculateStandardDiscount) does not hold instance state (this), it needs to receive an instance of type IMembership as an argument. The calling side passes itself like IMembership.CalculateStandardDiscount(this), allowing the logic within the interface to reference its properties.

3. Method Definition in the Implementation Class

Normally, when using an interface’s default implementation, you do not define the method on the class side. However, if you want to perform conditional branching like in this case, define a method with the same name as public on the class side. This ensures the class-side method is prioritized as the implementation of the interface.

Summary

Default interface implementations are convenient, but simply using them can lack flexibility. If you want to achieve both “logic sharing” and “individual customization,” adopt the pattern introduced here: “Extract logic to protected static and delegate.” This allows you to express complex business rules while avoiding code duplication.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次