web-dev-qa-db-ja.com

メソッドはその副作用を説明する必要がありますか?

私はボブマーティンのClean Codeを読んでいましたが、名前付けに関連して、興味深いコードの匂いが1つあります。

N7:名前は副作用を説明する必要があります
名前は、関数、変数、またはクラスが行うまたは行うすべてを説明する必要があります。名前で副作用を隠さないでください。単純な動詞を使用して、その単純なアクション以外の機能を説明しないでください。たとえば、TestNGの次のコードを考えてみます。

public ObjectOutputStream getOos() throws IOException {
    if (m_oos == null) {
        m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
}

この関数は、「oos」を取得するだけではありません。まだ作成されていない場合は、「oos」を作成します。したがって、より適切な名前はcreateOrReturnOosです。

メソッド名はメソッドが何をするかではなく、何をするかを示すべきだと思います(これは実装のカプセル化を壊すためです)。

したがって、私にとって、このメソッドは1つのことを行います。名前が示すようにObjectOutputStreamオブジェクトを取得(取得)し、名前をcreateOrReturnOosに変更すると、呼び出し側に実装の詳細が明らかになります。また、呼び出し元としてObjectOutputStreamを取得したいだけで、どのように取得/作成されたかは気にしません。

メソッドがプライベートの場合、メソッドがクラスの可視性を持ち、強い凝集性が望ましいため、提案された名前がより有益であることに同意できるかもしれませんが、メソッドがパブリックであるため、メソッドの名前を抽象化するもう1つの理由です。

この状況(かなり頻繁に発生します)では、常に2つの相反する質問があります。メソッドの処理を抽象化するか、メソッドの処理を詳細に説明する必要がありますか?しかし、繰り返しますが、これはパブリックメソッドであるため、メソッドの機能を抽象化したいと思います。
また、多分ボブは彼が言いたかったことの悪い例を見つけました。

あなたの考え?

5

ステップ1はもちろん、メソッドに副作用がないようにすることです。

しかし、例に焦点を当てましょう。はい、一般的には、関数の機能からその機能を抽象化する必要がありますが、副作用は明らかに関数の一部whatの一部です。名前でそれらを明示的にすることで、何が起こっているかについてコードをより明確にし、バグを減らします。

ストリームで機能するこの種の関数には、特別な意味もあります。この種の関数は、ストリームをクリーンアップする必要があるかどうかをプログラマーに示すので、この種の関数の呼び出し元は、ストリームが新しいかどうかを知る必要があります。この種のリソース所有権を管理することは、あらゆる種類の厄介なバグ(特に非メモリ管理言語)につながる危険なシナリオです。そのため、意図の明確な伝達がさらに重要になります。

4
Telastyn

したがって、私にとって、このメソッドは1つのことを行います。名前が示すとおり、ObjectOutputStreamオブジェクトを取得(取得)します。

いいえ。存在しないものは取得できません。以前は存在しなかったオブジェクトを作成し、そのオブジェクトを返すと、プログラムの状態が変化します(他の重大な副作用が発生する可能性があります)。それは検索ではありません。検索は、安全で影響の少ない操作である必要があります。

新しいオブジェクトを作成して戻すと、プログラムの状態が変わります。少なくとも、ヒープに新しいオブジェクトを追加し、メモリを少し消費し、オブジェクトの作成に関係するCPUサイクルを使用しています。この関数が頻繁に呼び出される場合、それだけでアプリケーションのパフォーマンスに大きな影響を与える可能性があります。

また、新しいオブジェクトを作成すると、他の副作用が生じる可能性があります。

  • ファイルを開く
  • スレッドを開始する
  • データベースへの書き込み
  • ネットワークソケットを開く
  • 他のオブジェクトの状態をリセットする

Oosが作成時にトリガーされる可能性のあるものをいくつ知っていますか?必要なすべて(および指定されたすべて)が既存のオブジェクトへの参照である場合、それらのどれもあなたを気にする必要はありません。

私の最後のポイントは非常に重要です。コードスニペットは、このコンテキストにはoneOosしかないことを示しています。したがって、新しいOosを作成すると、関連オブジェクト(キャッシュ、カウンター、スタック)の状態が消去され、それらが元の状態に戻る場合があります。おっとっと。

名前をcreateOrReturnOosに変更すると、呼び出し側に実装の詳細が明らかになります。

Oosオブジェクトが呼び出し元に返されなかった場合-それが一時的な隠されたものである場合-それは真実です。ただし、作成したオブジェクトを返します。その起源と歴史は重要です

そして、呼び出し元としてObjectOutputStreamを取得したいだけで、どのように取得/作成されたかは気にしません。

あなたがすべき。状態を変更する操作と変更しない操作を知ることは非常に重要です

  • 一部のリソース(データベース、クラスター化データキャッシュなど)は、書き込みを行う1つのノードを持ち、他のすべてのノードは読み取りのみを行うことで拡張します。どの関数が状態を変更し、どの関数が状態を変更しないかがわからない場合、アプリケーションは予期せず壊れます。
  • 一部の内部リソース(スレッドプールなど)は、1つの関数/コンポーネントの問題で使い果たされる可能性があります。回復力を高めるには、これらのリソースを個別のプールに分離し、破壊の可能性があるコンポーネントを個別のプールに割り当てることをお勧めします。これにより、あるコンポーネントが突然リソースを使い果たして他のコンポーネントが不足することがなくなります。しかし、それが機能するためには、どの関数がこれらのリソースを消費するかを知る必要があります。
  • 多くの場合、セキュリティ上の懸念から、どの機能が状態を変更し、どれが状態を報告するかを知る必要があります。その知識があれば、状態を変更する機能を制限して、特権プロセスのみがそれらを使用できるようにすることができます。
  • グレースフルデグラデーション(望ましい機能)とは、アプリケーションがリソース(たとえば、書き込みを実行できるマスターデータベース)の一部を失っても、アプリケーションが多くのことを実行できることを意味します。適切に設計されたアプリケーションは、情報を変更できない場合でも情報を返すことができます。しかし、そのようなコードを設計するには、どの関数が状態を変更するかknowする必要があります。

これまで、私はあなたが考慮していないかもしれない副作用と、なぜそれらが重要であるかについてだけ話していました。ただし、別の重要な考慮事項があります。

正直

あなたは発信者に嘘をついています。

呼び出し元はObjectOutputStreamが既存のストリームを返すことを期待する必要があります。直接アクセスできない場合でも、呼び出し元が継承すると想定している重要な状態(キャッシュ、操作の履歴、ファイルに書き込まれた出力)がある場合があります。特にストリームは、状態と履歴がたくさんになる傾向があります。まあ、あなたは彼らに嘘をついた。これは、その状態のない真新しいオブジェクトです。彼らはそれを知りません。あなたはそのように嘘をつくべきではありません。

たぶんすべき現在のコンテキストに存在するOosである必要があります。 1つの欠如は、何かが死んだか、コードの他の場所にあるバグが古いOosを削除したことを意味する場合があります。今、あなたはそのエラーを隠しました。これをデバッグするとfunになります。

また、発信者が自分の制御フローを作成する自由を否定しています。現在、私はFPの一種であり、例外が疑われますが、Javaの土地にいます。壊れる場合とそうでない場合があることを行うことは、一般的なJavaイディオムであり、catchブロックを残して、そのような場合に対処します。コードパス全体がそこにのみ存在する場合があります。発信者にそのオプションを拒否しています。そして、他のより単純なもの。

しないでください要求されなかったときに存在しなかったものを自動的に作成します。発信者が存在しないものを要求した場合は、そのことを伝えます。次に何をするかを彼に決めさせてください。

彼に嘘をつくのをやめなさい。

多分ボブは彼が言いたかったことの悪い例を見つけました

彼はしませんでした。 クリーンコードは、私が今まで読んだソフトウェア開発に関する最高の書物の一部であり、私が不満を言うセクションには何もありません。

3
itsbruce

しかし、あなたはdo IOExceptionをスローする可能性があるため、作成されることに注意してください!

実際、それがcanスローするのは、最初に呼び出されるときだけです。 2回目は、呼び出しコードに無意味なtry/catchブロック(これがJavaであると想定)を配置する必要があります。

このコードをリファクタリングして、明示的なストリーム作成メソッドを追加します。 I/Oがうまくいかない場合、I/Oをゲッター内で発生させることは一般的に良くありません。

(オブジェクトの作成にI/Oが含まれない場合は、評価に同意します)

2
grahamparks

はい、メソッド名は副作用を説明するべきだと思います。

1つには、この例ではチェック例外を使用します。これにより、実装の詳細が明らかになります。とにかく、このメソッドの呼び出しにはtry/catchブロックが必要です(理由は何もありません。これは内部で処理され、NPEをスローするかnullを返すことができるため、状況は改善されませんが、少なくとも呼び出し元でよりクリーンになります。サイド)、それで、より明確なメソッド名は害を及ぼさないでしょう。

ただし、実際にはgetSingletonOosと呼んでいます。 getOosは単純な古いゲッターのように聞こえ、非常に誤解を招きやすいものです。比較:

// Plain getter, no side effect.
public ObjectOutputStream getOos() {
    return m_oos;
}

// Additional side effects are described by the name.
public ObjectOutputStream getSingletonOos() throws IOException {
    if (m_oos == null) {
        m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
}

2番目の例では、最初の例よりも多くのことが行われ、名前に反映されているはずです。他にどのように違いを伝えますか?

1
scriptin

他の人が指摘したように、メソッドは、例外をスローする可能性があることを公開しているため、実際には単なるゲッターではないという事実を公開しています。したがって、メソッドの名前で、それがゲッターまたはファクトリーとして機能することを明確にすることもできます。例外。

公に宣言された例外によって複雑にならない別のシナリオでは、次のようになります。

メソッドの副作用を公表するかどうかの問題は、その単一のメソッドを見ることではなく、オブジェクト全体を考慮することによって答えられます

メソッドに副作用があるという事実は、これらの副作用が何らかの形、形、またはメソッドが属しているオブジェクトのパブリックインターフェースを通じて可視である場合は、公開する必要があります。

したがって、オブジェクトの一部のメソッドが内部的に副作用を実行するという事実をオブジェクトが完全に隠すことができる場合は、この事実をパブリックメソッドの名前で公開しないでください。ただし、オブジェクトの他のメソッドを呼び出すことにより、副作用が発生したという事実を発見できた場合、これらの副作用を引き起こすメソッドは、名前にそのことを示す必要があります。

0
Mike Nakis