공개 회원을 가상 / 추상화하지 마십시오. 무언가를 추가하기로 결정

2000 년대에 제 동료는 공개 방법을 가상 또는 추상적으로 만드는 것이 반 패턴이라고 말했습니다.

예를 들어, 그는 잘 설계되지 않은 다음과 같은 클래스를 고려했습니다.

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

그는 말했다

  • 구현 Method1하고 재정의 하는 파생 클래스의 개발자 Method2는 인수 유효성 검사를 반복해야합니다.
  • 경우에 기본 클래스의 개발자의 사용자 정의 부분 주위에 무언가를 추가하기로 결정 Method1또는 Method2후, 그는 그것을 할 수 없습니다.

대신 내 동료가이 접근법을 제안했습니다.

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

그는 공개 방법 (또는 속성)을 가상 또는 추상으로 만드는 것이 필드를 공개하는 것만 큼 나쁘다고 말했습니다. 필드를 속성에 래핑하면 나중에 필요한 경우 해당 필드에 대한 액세스를 가로 챌 수 있습니다. 공용 가상 / 추상 멤버에도 동일하게 적용됩니다. ProtectedAbstractOrVirtual클래스에 표시된대로 멤버를 래핑 하면 기본 클래스 개발자가 가상 ​​/ 추상 메소드로 이동하는 모든 호출을 가로 챌 수 있습니다.

그러나 나는 이것을 설계 지침으로 보지 않습니다. 심지어 Microsoft조차도 따르지 않습니다 Stream. 클래스를 살펴보고 이를 확인하십시오.

그 지침에 대해 어떻게 생각하십니까? 이해가 되나요, 아니면 API를 지나치게 복잡하게 생각하십니까?



답변

속담

Method1을 구현하고 Method2를 대체하는 파생 클래스의 개발자로 인해 공개 메소드를 가상 또는 추상으로 만드는 것은 안티 패턴이며 인수 유효성 검증을 반복해야합니다.

원인과 결과가 섞여 있습니다. 모든 재정의 가능한 메소드에는 사용자 정의 할 수없는 인수 유효성 검증이 필요하다고 가정합니다. 그러나 그것은 다른 방법입니다.

경우 (-보다 일반적인 – 또는 사용자 정의 및 사용자 지정할 수없는 부분) 하나는 모든 클래스의 파생 몇 가지 고정 된 인수의 유효성 검사를 제공하는 방법으로 방법을 설계하고 싶어, 다음 은 엔트리 포인트가 아닌 가상 수 있도록하는 것이 합리적이다 대신 내부적으로 호출되는 사용자 정의 가능한 부분에 대한 가상 또는 추상 방법을 제공합니다.

그러나 사용자 정의 할 수없는 고정 부분이 없기 때문에 공개 가상 메소드를 갖는 것이 합리적 인 많은 예가 있습니다. ToString또는 표준 메소드를 보거나 Equals또는 GetHashCode– 표준 object이 아닌 메소드를 공개하거나 동시에 가상? 나는 그렇게 생각하지 않습니다.

또는 자신의 코드 측면에서 기본 클래스의 코드가 최종적으로 의도적으로 다음과 같이 보일 때

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

사이의 분리를 가지고 Method1Method1Core만 특별한 이유없이 일을 복잡하게한다.


답변

동료가 제안한 방식으로 수행하면 기본 클래스의 구현 자에게 더 많은 유연성이 제공됩니다. 그러나 일반적으로 추정되는 이점으로 정당화되지 않는 더 복잡한 문제가 발생합니다.

마음 기본 클래스 구현에 대한 증가 된 유연성의 비용으로 오는 적은 최우선 자에게 유연성을 제공합니다. 그들은 특별히 신경 쓰지 않을 행동을 취합니다. 그들에게는 상황이 더욱 엄격 해졌습니다. 이것은 정당화되고 도움이 될 수 있지만 이것은 모두 시나리오에 달려 있습니다.

이를 구현하기위한 명명 규칙은 공용 인터페이스에 적합한 이름을 예약하고 내부 메소드 이름 앞에 “Do”를 붙이는 것입니다.

한 가지 유용한 사례는 수행 된 작업에 설정 및 종료가 필요한 경우입니다. 재정의가 끝나면 스트림을 열고 닫는 것처럼. 일반적으로 동일한 종류의 초기화 및 마무리. 사용하는 유효한 패턴이지만 모든 추상 및 가상 시나리오에서 사용하도록 명령하는 것은 의미가 없습니다.


답변

C ++에서는이를 비가 상 인터페이스 패턴 (NVI)이라고합니다. (한 번에 템플릿 방법이라고 불렀습니다. 혼동 스럽지만 일부 오래된 기사에는 그 용어가 있습니다.) NVI는 Herb Sutter에 의해 적어도 몇 번 글을 썼습니다. 가장 이른 것 중 하나가 여기에 있다고 생각 합니다 .

내가 올바르게 기억, 전제는 파생 클래스가 변경되지해야한다는 것입니다 무엇을 기본 클래스는 않지만 어떻게 그것을 않습니다.

예를 들어, Shape에는 모양을 재배치하기위한 Move 메서드가있을 수 있습니다. 형태가 이동의 의미 (개념적 수준)를 정의하므로 구체적인 구현 (예 : 정사각형 및 원과 같은)은 이동을 직접 재정의해서는 안됩니다. 사각형은 위치가 내부적으로 표현되는 방식에있어 Circle과 다른 구현 세부 사항을 가질 수 있으므로 이동 기능을 제공하기 위해 일부 메소드를 대체해야합니다.

간단한 예에서, 이것은 종종 모든 작업을 개인 가상 ReallyDoTheMove에 위임하는 공개 Move로 귀결되므로 이익이없는 많은 오버 헤드처럼 보입니다.

그러나이 일대일 통신은 요구 사항이 아닙니다. 예를 들어 Shape의 공개 API에 Animate 메서드를 추가 할 수 있으며 루프에서 ReallyDoTheMove를 호출하여 구현할 수 있습니다. 하나의 개인 추상 메소드에 의존하는 두 개의 공개 비가 상 메소드 API로 끝납니다. Circles and Squares는 추가 작업을 수행 할 필요가 없으며 Animate를 재정의 할 수도 없습니다 .

기본 클래스는 소비자가 사용하는 공용 인터페이스를 정의하고 해당 공용 메소드를 구현하는 데 필요한 기본 조작의 인터페이스를 정의합니다. 파생 된 형식은 이러한 기본 작업의 구현을 제공합니다.

클래스 디자인 의이 측면을 변경하는 C #과 C ++의 차이점을 알지 못합니다.