자습서: 기본 인터페이스 메서드로 인터페이스를 업데이트

인터페이스 멤버 선언 시 구현을 정의할 수 있습니다. 가장 일반적인 시나리오는 이미 릴리스되어 수많은 클라이언트가 사용하는 인터페이스에 멤버를 안전하게 추가하는 것입니다.

이 자습서에서는 다음과 같은 작업을 수행하는 방법을 알아봅니다.

  • 구현으로 메서드를 추가하여 안전하게 인터페이스를 확장합니다.
  • 매개 변수가 있는 구현을 생성하여 향상된 유연성을 제공합니다.
  • 구현자가 재정의 형식으로 더 구체적인 구현을 제공하도록 지원합니다.

필수 조건

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

이들은 주문을 나타내는 두 번째 인터페이스를 정의했습니다.

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

이러한 인터페이스에서, 팀은 사용자가 고객을 위해 더 나은 경험을 만들 수 있는 라이브러리를 빌드할 수 있었습니다. 이들의 목표는 기존 고객과 더 깊은 관계를 형성하고 신규 고객과의 관계를 개선하는 것이었습니다.

이제, 다음 릴리스를 위해 라이브러리를 업그레이드할 시간입니다. 요청된 기능 중 하나는 주문건이 많은 고객을 위해 충성도 할인을 지원하는 것입니다. 고객이 주문할 때마다 이 새로운 충성도 할인이 적용됩니다. 특정 할인은 각 개인 고객의 속성입니다. 구현된 각 ICustomer마다 고객 할인에 대해 다른 규칙을 설정할 수 있습니다.

이 기능을 추가하는 가장 일반적인 방법은 충성도 할인을 적용할 메서드로 ICustomer 인터페이스를 개선하는 것입니다. 이러한 설계 제안은 숙련된 개발자 사이에서 우려를 일으켰습니다. "인터페이스는 릴리스된 후에는 변경이 불가합니다! 호환성이 손상되는 변경을 하지 마세요!" 인터페이스 업그레이드에 기본 인터페이스 구현을 사용해야 합니다. 라이브러리 작성자가 인터페이스에 새 멤버를 추가하고 해당 멤버에 대한 기본 구현을 제공할 수 있습니다.

기본 인터페이스 구현을 통해 구현자는 해당 구현을 재지정하고 개발자는 인터페이스를 업그레이드할 수 있습니다. 라이브러리의 사용자는 기본 구현을 일반적인 변경으로 받아들일 수 있습니다. 비즈니스 규칙이 다른 경우 재지정할 수 있습니다.

기본 인터페이스 메서드를 사용하여 업그레이드

팀은 가장 가능성 있는 기본 구현, 즉 고객을 위한 충성도 할인에 합의했습니다.

업그레이드는 할인 자격을 갖추기 위해 필요한 주문 수, 할인율, 이 두 가지 속성에 기능을 제공해야 합니다. 이러한 기능을 사용하면 기본 인터페이스 메서드에 대한 완벽한 시나리오가 됩니다. 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)이어야 합니다.

매개 변수화 제공

그러나 기본 구현은 너무 제한적입니다. 이 시스템의 많은 소비자가 구매 건수에 다른 임계값, 다른 멤버십 기간 또는 다른 할인율을 선택할 수 있습니다. 이러한 매개 변수를 설정하는 방법을 제공하여 더 많은 고객에게 향상된 업그레이드 환경을 제공할 수 있습니다. 기본 구현을 제어하는 세 개의 매개 변수를 설정하는 정적 메서드를 추가해 보겠습니다.

// 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의 샘플 리포지토리에서 시작 애플리케이션을 다운로드할 수 있습니다.

이러한 새 기능은 신규 멤버에 대한 합리적인 기본 구현이 있는 경우 인터페이스를 안전하게 업데이트할 수 있음을 의미합니다. 여러 클래스에서 구현한 단일 기능 아이디어를 표현하기 위해 인터페이스를 신중하게 설계하세요. 이를 통해 동일한 기능 아이디어에 새로운 요구 사항이 발견될 경우 해당 인터페이스 정의를 훨씬 쉽게 업그레이드할 수 있습니다.