クラスが独自のパブリックメソッドを使用しても大丈夫ですか?


23

バックグラウンド

現在、デバイスが送信および受信するオブジェクトを持っている状況があります。このメッセージには、次のようないくつかの構成要素があります。

public void ReverseData()
public void ScheduleTransmission()

ScheduleTransmissionこの方法は、必要呼び出すためにReverseData、それが呼ばれるたびにメソッドを。ただし、アプリケーションでオブジェクトがインスタンス化されている場所からReverseData外部で呼び出す必要がある場合があります(名前空間の外側に完全に追加する必要があります)。

「受信」に関してはReverseDataobject_receivedイベントハンドラーで外部から呼び出されて、データを元に戻します。

質問

オブジェクトが独自のパブリックメソッドを呼び出すことは一般に許容されますか?


5
パブリックメソッドを独自のメソッドから呼び出すことに問題はありません。しかし、メソッド名に基づいて、ReverseData()は、内部データを反転する場合、パブリックメソッドになるのは少し危険に思えます。ReverseData()がオブジェクトの外部で呼び出され、ScheduleTransmission()で再度呼び出された場合はどうなるでしょう。
カーン

@Kaanこれらは私のメソッドの本当の名前ではありませんが、密接に関連しています。実際、「データの反転」は合計ワードの8ビットのみを反転し、送受信時に行われます。
スヌープ

問題はまだ残っています。送信がスケジュールされる前にこれらの8ビットが2回反転されるとどうなりますか?これは、パブリックインターフェイスに大きな穴があるように感じます。私の答えで述べたように、パブリックインターフェイスについて考えることは、この問題を明らかにするのに役立つかもしれません。
ダニエルT.

2
@StevieVこれは、カーンが提起する懸念を増幅するだけだと思います。オブジェクトの状態を変更するメソッドを公開しているように思えますが、状態は主にメソッドが呼び出された回数に依存します。これは、コード全体でオブジェクトの状態が何であるかを追跡しようとする悪夢になります。これらの概念的な状態とは別のデータ型から恩恵を受けるように思えるので、心配することなく、コードのどの部分に含まれているかを知ることができます。
jpmc26

2
@StevieV DataやSerializedDataのようなもの。データはコードが見るものであり、SerializedDataはネットワークを介して送信されるものです。次に、あるデータ型から別のデータ型に逆データ(またはデータをシリアル化/逆シリアル化)変換します。
csiz

回答:


33

特に許容できるだけでなく、拡張機能を許可する場合は特にお勧めします。C#でクラスの拡張機能をサポートするには、以下のコメントに従ってメソッドに仮想のフラグを立てる必要があります。ただし、ReverseData()をオーバーライドしてもScheduleTransmission()の動作方法が変更されても驚かないように、これを文書化することをお勧めします。

それは本当にクラスのデザインに帰着します。ReverseData()は、クラスの基本的な動作のように聞こえます。この動作を他の場所で使用する必要がある場合は、おそらく他のバージョンを使用したくないでしょう。ScheduleTransmission()に固有の詳細がReverseData()に漏れないように注意する必要があります。それは問題を引き起こします。しかし、あなたはすでにクラス外でこれを使用しているので、おそらくあなたはすでにそれを考え抜いたでしょう。


うわー、私には本当に奇妙に思えたが、これは役立ちます。他の人が何を言っているのかも見てみたいです。ありがとうございました。
スヌープ

2
「したがって、ReverseData()をオーバーライドしてもScheduleTransmission()の動作が変わっても、だれも驚かないように」:いいえ、そうではありません。少なくともC#にはありません(質問にはC#タグがあることに注意してください)。ReverseData()抽象的でない限り、子クラスで何をしても、呼び出されるのは常に元のメソッドです。
Arseni Mourzenko

2
@MainMaまたはvirtual...メソッドをオーバーライドする場合は、定義上、virtualまたはにする必要がありますabstract
ポケチュウ

1
@ Pokechu22:確かに、virtual機能します。すべての場合において、OPによると、署名はpublic void ReverseData()であるため、内容のオーバーライドに関する回答の一部はわずかに誤解を招く可能性があります。
Arseni Mourzenko

@MainMaあなたは、基本クラスのメソッドが仮想メソッドを呼び出す場合、そうでない限り通常の仮想的な方法で動作しないと言っていabstractますか?私はabstractvsの答えを調べましたがvirtual、言及されていることはわかりませんでした。
JDługosz

20

絶対に。

メソッドの可視性には、クラス外または子クラス内のメソッドへのアクセスを許可または拒否するという唯一の目的があります。public、protected、およびprivateメソッドは、常にクラス自体の内部で呼び出すことができます。

パブリックメソッドの呼び出しに問題はありません。あなたの質問のイラストは、2つのパブリックメソッドを持つことがまさにあなたがすべきことである状況の完璧な例です。

ただし、次のパターンを注意深く監視できます。

  1. メソッドHello()は次のWorld(string, int, int)ように呼び出します。

    Hello()
    {
        this.World("Some magic value here", 0, 100);
    }

    このパターンを避けてください。代わりに、オプションの引数を使用します。オプションの引数を使用すると、検出が容易になります。入力する呼び出し側Hello(は、デフォルト値でメソッドを呼び出すことができるメソッドがあることを必ずしも知る必要はありません。オプションの引数も自己文書化されます。World()実際のデフォルト値が何であるかを呼び出し元に示しません。

  2. メソッドHello(ComplexEntity)は次のWorld(string, int, int)ように呼び出します。

    Hello(ComplexEntity entity)
    {
        this.World(entity.Name, entity.Start, entity.Finish);
    }

    代わりに、オーバーロードを使用します。同じ理由:発見可能性が向上します。呼び出し元は、IntelliSenseを介してすべてのオーバーロードを一度に確認し、適切なものを選択できます。

  3. メソッドは、実質的な価値を追加することなく、他のパブリックメソッドを呼び出すだけです。

    考え直してください。このメソッドは本当に必要ですか?または、それを削除して、呼び出し元にさまざまなメソッドを呼び出させる必要がありますか?ヒント:メソッドの名前が正しくない、または見つけにくい場合は、おそらく削除する必要があります。

  4. メソッドは入力を検証してから、他のメソッドを呼び出します。

    Hello(string name, int start, int end)
    {
        if (name == null) throw new ArgumentNullException(...);
        if (start < 0) throw new OutOfRangeException(...);
        ...
        if (end < start) throw new ArgumentException(...);
    
        this.World(name, start, end);
    }

    代わりに、Worldパラメータ自体を検証するか、プライベートにする必要があります。


ReverseDataが実際に内部であり、「オブジェクト」のインスタンスを含む名前空間全体で使用される場合、同じ考えが適用されますか?
スヌープ

1
@StevieV:はい、もちろん。
Arseni Mourzenko

@MainMaポイント12の
カポル

@Kapol:ご意見ありがとうございます。最初の2つのポイントに関する説明を追加して、回答を編集しました。
Arseni Mourzenko

ケース1でさえ、私は通常、オプションの引数よりもオーバーロードを好みます。オプションがオーバーロードの使用がオプションではない、または特に多数のオーバーロードを生成するようなオプションである場合、カスタムタイプを作成し、それを引数として(提案2のように)使用するか、コードをリファクタリングします。
ブライアン

8

何かが公開されている場合、どのシステムからでもいつでも呼び出される可能性があります。これらのシステムの1つになれない理由はありません!

高度に最適化されたライブラリでは、で述べたイディオムをコピーしjava.util.ArrayList#ensureCapacity(int)ます。

ensureCapacity

  • ensureCapacity 公開されています
  • ensureCapacity 必要なすべての境界チェックとデフォルト値などがあります。
  • ensureCapacity 呼び出します ensureExplicitCapacity

ensureCapacityInternal

  • ensureCapacityInternal プライベートです
  • ensureCapacityInternal すべての入力はクラス内から取得されるため、最小限のエラーチェックがあります。
  • ensureCapacityInternal また呼び出す ensureExplicitCapacity

ensureExplicitCapacity

  • ensureExplicitCapacity またプライベートです
  • ensureExplicitCapacityエラーチェックはありません
  • ensureExplicitCapacity 実際の仕事をする
  • ensureExplicitCapacityensureCapacityand 以外のどこからも呼び出されないensureCapacityInternal

このようにして、信頼できるコードは、その入力が適切であることを知っているため、特権(および高速!)アクセスを取得します。信頼できないコードは、特定の厳密さを経てその整合性と爆弾を検証するか、デフォルトを提供するか、不正な入力を処理します。どちらも実際に機能する場所に移動します。

ただし、これは、JDKで最もよく使用されるクラスの1つであるArrayListで使用されます。あなたのケースがそのレベルの複雑さと厳密さを必要としない可能性は非常に高いです。簡単に言えば、すべてのensureCapacityInternal呼び出しが呼び出しに置き換えられた場合ensureCapacityでも、パフォーマンスは非常に優れたものになります。これは、おそらく十分に検討した後にのみ行われる微最適化です。


3

いくつかの良い答えが既に与えられており、私はまた、はい、オブジェクトが他のメソッドからそのパブリックメソッドを呼び出すかもしれないことに同意します。ただし、注意が必要なわずかな設計上の注意事項があります。

パブリックメソッドには、通常、「オブジェクトを一貫した状態にする、賢明なことをする、オブジェクトを(おそらく異なる)一貫した状態にする」という契約があります。ここで、「一致」とは、例えば、意味することができるLengthのはList<T>、そのより大きくないCapacityからインデックスに要素を参照すること0にすると、Length-1捨てないであろう。

しかし、オブジェクトのメソッド内では、オブジェクトは一貫性のない状態にある可能性があります。そのため、パブリックメソッドの1つを呼び出すと、非常に間違った動作をする可能性があります。したがって、他のメソッドからパブリックメソッドを呼び出す場合は、それらのコントラクトが「何らかの状態でオブジェクトを取得し、賢明なことを行い、オブジェクトを(おそらく異なる)ままにする(一貫性のない場合がありますが、状態が一貫していなかった)状態」。


1

別のパブリックメソッド内でパブリックメソッドを呼び出す場合のもう1つの例は、CanExecute / Executeアプローチです。検証と不変式保存の両方が必要な場合に使用します。

しかし、一般的に私はそれについて常に慎重です。メソッドa()がメソッド内で呼び出される場合、b()そのメソッドa()はの実装詳細であることを意味しb()ます。多くの場合、異なる抽象化レベルに属していることを示しています。そして、両者が公開されているという事実は、それが単一責任原則違反であるかどうか疑問に思います。


-5

申し訳ありませんが、他のほとんどの「はい」の回答に同意する必要があります。

あるパブリックメソッドを別のパブリックメソッドから呼び出すクラスを推奨しません

このプラクティスにはいくつかの潜在的な問題があります。

1:継承クラスの無限ループ

そのため、基本クラスはmethod2からmethod1を呼び出しますが、ユーザーまたは他の誰かがそれを継承し、method2を呼び出す新しいメソッドでmethod1を非表示にします。

2:イベント、ロギングなど

たとえば、イベント '1 added!'を起動するメソッドAdd1があります Add10メソッドでそのイベントを発生させたり、ログなどに10回書き込んだりすることはおそらくないでしょう。

3:スレッド化およびその他のデッドロック

たとえば、InsertComplexDataは、db接続を開き、トランザクションを開始し、テーブルをロックします。次に、InsertSimpleDataを呼び出し、接続を開き、トランザクションを開始し、テーブルがロック解除されるのを待ちます。

他の理由もあると確信しています。他の答えの1つは、「method1を編集し、method2が異なる動作を開始することに驚いている」というものです。

一般に、コードを共有する2つのパブリックメソッドがある場合は、一方が他方を呼び出すのではなく、両方がプライベートメソッドを呼び出す方が良いでしょう。

編集----

OPの特定のケースを展開してみましょう。

詳細はあまりありませんが、ReverseDataは、ScheduleTransmissionメソッドだけでなく、何らかの種類のイベントハンドラによって呼び出されることはわかっています。

リバースデータはオブジェクトの内部状態も変化させると思います

この場合、スレッドの安全性が重要であると考えられるため、この慣行に対する3番目の異議が適用されます。

ReverseDataスレッドセーフにするために、ロックを追加できます。ただし、ScheduleTransmissionもスレッドセーフである必要がある場合は、同じロックを共有する必要があります。

これを行う最も簡単な方法は、ReverseDataコードをプライベートメソッドに移動し、両方のパブリックメソッドがそれを呼び出すようにすることです。次に、パブリックステートメントにロックステートメントを配置し、ロックオブジェクトを共有できます。

明らかに、「それは決して起こらない!」と主張することができます。または「ロックを別の方法でプログラムできます」が、適切なコーディングの実践に関するポイントは、そもそもコードを適切に構造化することです。

学問的に言えば、これは確かにLに違反しています。パブリックメソッドは、パブリックにアクセスできるだけではありません。また、継承者によって変更可能です。コードは変更のために閉じられる必要があります。つまり、パブリックメソッドとプロテクトメソッドの両方でどのような作業を行うかを考える必要があります。

別のヘレスもあります。あなたもDDDに違反する可能性があります。オブジェクトがドメインオブジェクトの場合、そのパブリックメソッドは、ビジネスにとって意味のあるドメイン用語である必要があります。この場合、「1ダースの卵を購入」が「1個の卵を12回購入」と同じように開始することはほとんどありません。


3
これらの問題は、設計原則の一般的な非難というよりも、よく考えられていないアーキテクチャに似ています。(たぶん "1 added"を呼び出すことは問題ありません。そうでない場合は、プログラムを変える必要があります。2つのメソッドが同じリソースを排他ロックしようとしている場合、互いに呼び出してはいけません。 。)悪い実装を提示するのではなく、普遍的な原則としてパブリックメソッドへのアクセスに取り組む、より強固な議論に焦点を当てたいと思うかもしれません。たとえば、きちんとしたコードが良いのに、なぜ悪いのでしょうか?
-doppelgreener

そうでない場合は、イベントを置く場所がありません。同様にして第3位チェーンダウンあなたの方法の罰金それまでは1がトランザクションを必要とされ、その後、あなたがねじ込まれているすべてのもの
ユアン・

これらの問題はすべて、一般的な原則の欠陥よりもコード自体の品質が低いことを反映しています。3つのケースはすべて、不正なコードです。それらは「それをしないで、代わりにこれを行う」(おそらく「あなたは正確に何欲しいですか?」と尋ねることで)のいくつかのバージョンによって解決することができます。メソッド。無限ループの曖昧な可能性は、ここでは原則として問題に到達する唯一のものであり、それでも完全には取り組まれていません。
-doppelgreener

私の例の共有の「コード」は、1つのパブリックメソッドが別のメソッドを呼び出すことだけです。その「悪いコード」が良いと思うなら!それは私の競合である
ユアン

ハンマーを使って自分の手を壊すことができるからといって、ハンマーが悪いわけではありません。それは、手を壊すようなことをしないでください。
-doppelgreener
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.