web-dev-qa-db-ja.com

公開メンバーを仮想/抽象化しないでください-本当に?

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

彼は、パブリックメソッド(またはプロパティ)を仮想または抽象にすることは、フィールドをパブリックにすることと同じくらい悪いことだと私に言いました。フィールドをプロパティにラップすることにより、必要に応じて、後でそのフィールドへのアクセスを遮断できます。同じことがパブリックvirtual/abstractメンバーにも当てはまります。ProtectedAbstractOrVirtualクラスに示されているようにそれらをラップすると、基本クラスの開発者はvirtual/abstractメソッドへの呼び出しをインターセプトできます。

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

そのガイドラインについてどう思いますか?それは意味がありますか、それともAPIが複雑すぎると思いますか?

20
Peter Perot

言う

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

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

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

しかし、固定されたカスタマイズできない部分がないため、パブリック仮想メソッドを使用することが完全に理にかなっている多くの例があります。ToStringEqualsGetHashCode- objectクラスの設計を改善して、これらが同時にパブリックおよび仮想ではないようにしますか?私はそうは思いません。

または、独自のコードに関して:基本クラスのコードが最終的かつ意図的に次のようになっている場合

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

Method1およびMethod1Core明確な理由がない場合にのみ事態を複雑にします。

30
Doc Brown

同僚が提案する方法でこれを行うと、基本クラスの実装者により多くの柔軟性が提供されます。しかし、それに伴って、通常は推定されるメリットによって正当化されない複雑さが増します。

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

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

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

6
Martin Maat

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

私が正しく思い出した場合、前提は、派生クラスはwhatを変更してはならないが、howそれを行います。

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

単純な例では、これは多くの場合、すべての作業をプライベート仮想ReallyDoTheMoveに委任するだけのパブリックMoveに要約されるため、メリットがないために多くのオーバーヘッドのように見えます。

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

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

クラス設計のこの側面を変更するC#とC++の違いについては知りません。

2
Adrian McCarthy