다음을 통해 공유


자습서: 기본 인터페이스 메서드를 사용하여 인터페이스 업데이트

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

이 자습서에서는 다음 방법을 알아봅니다.

  • 구현을 사용하여 메서드를 추가하여 인터페이스를 안전하게 확장합니다.
  • 더 큰 유연성을 제공하기 위해 매개 변수가 있는 구현을 만듭니다.
  • 구현자가 재정의를 통해 보다 구체적인 구현을 제공할 수 있게 합니다.

필수 조건

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 인터페이스에서 제공합니다. 그러나 클래스는 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;
}

이 작은 코드 조각에는 많은 새로운 언어 기능이 표시됩니다. 이제 인터페이스에는 필드 및 메서드를 비롯한 정적 멤버가 포함될 수 있습니다. 다른 액세스 한정자도 사용하도록 설정됩니다. 다른 필드는 private이고 새 메서드는 public입니다. 인터페이스 멤버에는 모든 한정자가 허용됩니다.

충성도 할인을 계산하기 위해 일반 수식을 사용하지만 매개 변수가 다른 애플리케이션은 사용자 지정 구현을 제공할 필요가 없습니다. 정적 메서드를 통해 인수를 설정할 수 있습니다. 예를 들어 다음 코드는 고객에게 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의 샘플 리포지토리에서 시작 애플리케이션을 가져올 수 있습니다.

이러한 새로운 기능은 새 멤버에 대한 적절한 기본 구현이 있을 때 인터페이스를 안전하게 업데이트할 수 있음을 의미합니다. 여러 클래스에서 구현한 단일 기능 아이디어를 표현하기 위해 인터페이스를 신중하게 디자인합니다. 이렇게 하면 동일한 기능적 아이디어에 대한 새 요구 사항이 검색될 때 해당 인터페이스 정의를 더 쉽게 업그레이드할 수 있습니다.