공개 회원을 가상 / 추상화하지 마십시오.


20

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를 지나치게 복잡하게 생각하십니까?


5
메소드 작성 virtual 은 선택적 대체를 허용합니다. 재정의되지 않을 수 있으므로 메서드가 공개되어야합니다. 메소드를 작성하면 메소드 abstract 를 대체해야합니다. 문맥 protected에서 특별히 유용하지 않기 때문에 아마도이어야합니다 public.
Robert Harvey

4
실제로 protected추상 클래스의 개인 멤버를 파생 클래스에 노출하려는 경우 가장 유용합니다. 어쨌든, 나는 당신의 친구의 의견에 대해 특별히 걱정하지 않습니다. 특정 상황에 가장 적합한 액세스 수정자를 선택하십시오.
Robert Harvey

4
동료가 템플릿 방법 패턴을 옹호하고있었습니다 . 두 방법의 상호 의존성에 따라 두 가지 방법으로 유스 케이스가있을 수 있습니다.
Greg Burghardt

8
@GregBurghardt : OP의 동료 가 템플릿 메소드 패턴이 필요한지 여부에 관계없이 항상 사용하도록 제안하는 것처럼 들립니다 . 그것은 전형적인 패턴의 남용입니다-망치가 있으면 조만간 모든 문제가 못처럼 보이기 시작합니다. ;-)
Doc Brown

3
@PeterPerot : 퍼블릭 필드만으로 간단한 DTO로 시작하는 데 아무런 문제가 없었으며, 그러한 DTO가 비즈니스 로직을 가진 멤버를 요구할 때 속성이있는 클래스로 리팩토링했습니다. 라이브러리 공급 업체로 일할 때 공개 API를 변경하지 않도록주의해야하는 경우에는 상황이 다르므로 공개 필드를 같은 이름의 공개 속성으로 바꾸더라도 문제가 발생할 수 있습니다.
Doc Brown

답변:


30

속담

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

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

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

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

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

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

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


1
ToString()방법의 경우 Microsoft는 비 가상화 방식으로 만들고 가상 템플릿 방법을 도입했습니다 ToStringCore(). 이유 :이 때문에 : ToString()-상속자 참고 사항 . 그들은 ToString()null을 반환해서는 안된다고 말합니다 . 그들은 구현함으로써 이러한 요구를 강요 할 수있었습니다 ToString() => ToStringCore() ?? string.Empty.
피터 페로

9
@PeterPerot : 당신이 연결 한 가이드 라인은 반환하지 않는 것이 좋습니다 string.Empty. 그리고 ToStringCore메소드 와 같은 것을 도입하여 코드에서 시행 할 수없는 많은 다른 것들을 권장합니다 . 따라서이 기술은 아마도 올바른 도구가 아닙니다 ToString.
Doc Brown

3
@Theraot : ToString과 Equals 또는 GetHashcode가 다르게 디자인 된 이유 또는 주장을 찾을 수는 있지만 오늘날은 그대로입니다 (적어도 좋은 디자인이라고 생각하기에 충분하다고 생각합니다).
Doc Brown

1
@PeterPerot "" anyObjectIGot.ToString()대신 많은 코드를 보았습니다 anyObjectIGot?.ToString()"-어떻게 관련이 있습니까? 귀하의 ToStringCore()접근 방식은 null 문자열이 반환되는 것을 방지하지만 NullReferenceException객체가 null 인 경우 여전히을 throw합니다 .
IMil

1
@PeterPerot 나는 권위의 주장을 전달하려고하지 않았습니다. Microsoft가 많이 사용하는 public virtual것은 아니지만 public virtual괜찮은 경우 가 있습니다. 우리 는 코드를 미래의 증거로 만드는 것처럼 사용자 정의 할 수없는 빈 부분을 주장 할 수 는 있지만 ... 작동하지 않습니다. 돌아가서 변경하면 파생 유형이 손상 될 수 있습니다. 따라서 아무것도 얻지 못합니다.
치료

6

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

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

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

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


1
메소드의 접두사는 하나 개의 옵션입니다. Microsoft는 종종 핵심 방법 접미사를 사용합니다 .
피터 페로

@ 피터 페로. Microsoft 자료에서 Core 접두사를 본 적이 없지만 최근에 많은 관심을 기울이지 않았기 때문일 수 있습니다. 나는 그들이 최근에 .NET Core의 이름을 만들기 위해 Core moniker를 홍보하기 위해이 작업을 시작했다고 생각합니다.
마틴 마트

아뇨, 낡은 모자입니다 : BindingList. 또한 나는 어딘가에서 권장 사항을 찾았습니다. 어쩌면 프레임 워크 디자인 지침 또는 유사한 것이있을 수 있습니다. 그리고 그것은 postfix 입니다. ;-)
Peter Perot

파생 클래스의 유연성이 떨어집니다. 기본 클래스는 추상화 경계입니다. 기본 클래스는 소비자에게 퍼블릭 API의 기능을 알려주고 이러한 목표를 달성하기 위해 필요한 API를 정의합니다. 파생 클래스가 기본 클래스의 공용 메서드를 재정의 할 수있는 경우 Liskov 대체 원칙을 위반할 위험이 높아집니다.
Adrian McCarthy

2

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

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

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

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

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

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

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


잘 찾아라! 이제 나는 2000 년대에 두 번째 링크 지점 (또는 그 사본)을 정확하게 발견 한 것을 기억합니다. 동료의 주장에 대한 더 많은 증거를 찾고 있었고 C # 컨텍스트에서 C ++ 컨텍스트에서 아무것도 찾지 못했다는 것을 기억합니다. 이. 입니다. 그것! :-) 그러나 C # 땅으로 돌아 가면이 패턴이 많이 사용되지 않는 것 같습니다. 어쩌면 사람들은 나중에 기본 기능을 추가하면 파생 클래스를 손상시킬 수 있으며 공개 가상 방법 대신 TMP 또는 NVIP를 엄격하게 사용하는 것이 항상 의미가있는 것은 아니라는 것을 깨달았습니다.
피터 페로

자체 참고 사항 :이 패턴의 이름은 NVIP입니다.
Peter Perot
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.