パブリックメンバーを仮想化または抽象化しないでください-本当にですか?


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クラスに示されているようにラップすることで、基本クラス開発者は仮想/抽象メソッドへの呼び出しをインターセプトできます。

しかし、私はこれを設計ガイドラインとしては見ていません。マイクロソフトでさえそれに従いませんStream。これを確認するためにクラスを見てください。

そのガイドラインをどう思いますか?それは理にかなっていますか、それともAPIを過度に複雑にしていると思いますか?


5
メソッドを作成するとvirtual 、オプションでオーバーライドできます。 メソッドはオーバーライドされない可能性があるため、おそらくパブリックにする必要があります。メソッドを作成するabstract と、それらをオーバーライドする必要があります。コンテキストprotectedでは特に有用ではないため、おそらくである必要がありpublicます。
ロバートハーベイ

4
実際、protected抽象クラスのプライベートメンバーを派生クラスに公開する場合に最も便利です。いずれにせよ、私はあなたの友人の意見を特に心配していません。特定の状況に最も適したアクセス修飾子を選択してください。
ロバートハーヴェイ

4
あなたの同僚はTemplate Method Patternを提唱していました。2つの方法の相互依存性に応じて、両方の方法のユースケースがあります。
グレッグブルクハルト

8
@GregBurghardt:OPの同僚は、必須かどうかにかかわらず、常にテンプレートメソッドパターンを使用することを提案しているように聞こえます。これは典型的なパターンの乱用です-ハンマーを持っている場合、遅かれ早かれすべての問題は爪のように見え始めます;
Doc Brown

3
@PeterPerot:パブリックフィールドだけの単純なDTOで始めるのに問題はありませんでした。そのようなDTOがビジネスロジックを持つメンバーを必要とすることが判明した場合、それらをプロパティを持つクラスにリファクタリングします。確かに、ライブラリベンダーとして働いており、パブリックAPIを変更しないように注意しなければならない場合、物事は異なります。そして、パブリックフィールドを同じ名前のパブリックプロパティに変えても問題が発生する可能性があります。
Doc Brown

回答:


30

言って

Method1を実装し、Method2をオーバーライドする派生クラスの開発者が引数の検証を繰り返す必要があるため、パブリックメソッドを仮想または抽象にするのはアンチパターンです。

原因と結果を混同しています。すべてのオーバーライド可能なメソッドには、カスタマイズ不可能な引数検証が必要であると仮定しています。しかし、それはまったく逆です。

場合( -より一般的な-またはカスタマイズおよび非カスタマイズ部分)1が、それはすべてのクラスの派生である固定された引数の検証を提供した方法で方法を設計したい、そしてそれは、エントリポイントは非仮想にするために理にかなっています、代わりに、内部的に呼び出されるカスタマイズ可能な部分に仮想メソッドまたは抽象メソッドを提供します。

しかし、固定されたカスタマイズ不可能な部分がないため、パブリック仮想メソッドを持つことは完全に理にかなっている例がたくさんあります: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の場合はスローします。
IMil

1
@PeterPerot私は権威からの議論を伝えようとしていませんでした。Microsoftが使用するのはそれほどではありませんpublic virtualが、public virtual大丈夫な場合があります。空のカスタマイズ不可能な部分は、コードを将来的に保証するものだと主張することができます...しかし、それは機能しません。戻って変更すると、派生型が壊れる可能性があります。したがって、それは何も得ません。
Theraot

6

あなたの同僚が示唆する方法でそれを行うことは、基本クラスの実装者により多くの柔軟性を提供します。しかし、それに伴って複雑さも増しますが、これは通常、推定される利点によって正当化されません。

基本クラスの実装者の柔軟性の向上は、優先する当事者の柔軟性の低下を犠牲にすることに留意してください。彼らは、特に気にしないかもしれないいくつかの課せられた行動を得ます。彼らにとっては、物事はより厳しくなりました。これは正当化され、役立つ場合がありますが、これはすべてシナリオに依存します。

これを実装するための命名規則(私が知っている)は、パブリックインターフェイスに適切な名前を予約し、内部メソッドの名前の前に "Do"を付けることです。

1つの便利なケースは、実行されたアクションに設定と終了が必要な場合です。ストリームを開いて、オーバーライドが完了した後に閉じるように。一般に、同じ種類の初期化とファイナライズ。使用するのに有効なパターンですが、すべての抽象シナリオおよび仮想シナリオで使用することを義務付けるのは無意味です。


1
実行(Do)メソッドの接頭辞は、一つの選択肢です。Microsoftは、Coreメソッドの接尾辞をよく使用します。
ピーターペロ

@ピーター・ペロー。Microsoftの資料でCoreプレフィックスを目にしたことはありませんが、これは最近あまり注意を払っていないためかもしれません。Coreモニカーを宣伝し、.NET Coreの名前を付けるためだけに、最近これを始めたのではないかと思います。
マーティンマート

いいえ、それは古い帽子ですBindingList。さらに、フレームワーク設計ガイドラインなどの推奨事項をどこかで見つけました。そして:それは後置です。;-)
ピーターペロ

派生クラスの柔軟性が低いことがポイントです。基本クラスは抽象化境界です。基本クラスは、パブリックAPIの機能をコンシューマに伝え、それらの目標を達成するためにAPIが必要であることを定義します。派生クラスが基本クラスのパブリックメソッドをオーバーライドできる場合、リスコフの置換原則に違反するリスクが高くなります。
エイドリアンマッカーシー

2

C ++では、これは非仮想インターフェイスパターン(NVI)と呼ばれます。(かつて、それはテンプレートメソッドと呼ばれていました。混乱を招きましたが、一部の古い記事にはその用語があります。)NVIは、それについて少なくとも数回書いたハーブサッターによって推進されています。最も初期の1つがここにあると思います

正しく思い出せば、派生クラスは基本クラスの動作を変更するのではなく、その方法を変更するという前提があります。

たとえば、シェイプには、シェイプを再配置するMoveメソッドがあります。Shapeは移動の意味を(概念レベルで)定義するため、具体的な実装(正方形や円など)はMoveを直接オーバーライドしないでください。Squareは、位置が内部的にどのように表現されるかという点でCircleとは異なる実装詳細を持っている可能性があるため、Move機能を提供するために何らかのメソッドをオーバーライドする必要があります。

簡単な例では、これは多くの場合、すべての作業をプライベートな仮想ReallyDoTheMoveに委任するパブリックMoveに要約されるため、多くのオーバーヘッドがあり、メリットはありません。

ただし、この1対1の対応は必須ではありません。たとえば、AnimateメソッドをShapeのパブリックAPIに追加し、ReallyDoTheMoveをループで呼び出すことで実装できます。最終的に、どちらも1つのプライベート抽象メソッドに依存する2つのパブリック非仮想メソッドAPIになります。Circles and Squaresは追加の作業を行う必要がなく、Animateをオーバーライドすることもできません

基本クラスは、コンシューマが使用するパブリックインターフェイスを定義し、これらのパブリックメソッドを実装するために必要なプリミティブ操作のインターフェイスを定義します。派生型は、これらのプリミティブ操作の実装を提供します。

クラス設計のこの側面を変更するC#とC ++の違いを認識していません。


良い発見!2000年代の2番目のリンクが指すポスト(またはそのコピー)を正確に見つけたことを覚えています。私は同僚の主張のさらなる証拠を探していたことを思い出し、C#コンテキストでは何も見つけられなかったが、C ++を見つけた。この。です それ!:-)しかし、C#の土地では、このパターンはあまり使用されていないようです。たぶん、後で基本機能を追加すると派生クラスが壊れる可能性があり、パブリック仮想メソッドの代わりにTMPまたはNVIPを厳密に使用することが必ずしも意味をなさないことに気付いたかもしれません。
ピーターペロ

自己への注意:このパターンの名前はNVIPです。
ピーターペロ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.