web-dev-qa-db-ja.com

インスタンス変数よりもローカル変数を優先する理由はありますか?

私が取り組んでいるコードベースは、インスタンス変数を頻繁に使用して、さまざまな簡単なメソッド間でデータを共有しています。元の開発者は、これがUncle Bob/Robert MartinのClean Codeの本に記載されているベストプラクティスに準拠していることを強く主張します。そして、「関数の引数の理想的な数はゼロ(ニラディック)です。(...)引数は難しいです。それらは概念的な力の多くをとります。」

例:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  private byte[] encodedData;
  private EncryptionInfo encryptionInfo;
  private EncryptedObject payloadOfResponse;
  private URI destinationURI;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    getEncodedData(encryptedRequest);
    getEncryptionInfo();
    getDestinationURI();
    passRequestToServiceClient();

    return cryptoService.encryptResponse(payloadOfResponse);
  }

  private void getEncodedData(EncryptedRequest encryptedRequest) {
    encodedData = cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private void getEncryptionInfo() {
    encryptionInfo = cryptoService.getEncryptionInfoForDefaultClient();
  }

  private void getDestinationURI() {
    destinationURI = router.getDestination().getUri();
  }

  private void passRequestToServiceClient() {
    payloadOfResponse = serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }
}

ローカル変数を使用して、それを次のようにリファクタリングします。

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    byte[] encodedData = cryptoService.decryptRequest(encryptedRequest, byte[].class);
    EncryptionInfo encryptionInfo = cryptoService.getEncryptionInfoForDefaultClient();
    URI destinationURI = router.getDestination().getUri();
    EncryptedObject payloadOfResponse = serviceClient.handle(destinationURI, encodedData,
      encryptionInfo);

    return cryptoService.encryptResponse(payloadOfResponse);
  }
}

これは短く、さまざまな簡単なメソッド間の暗黙的なデータ結合を排除し、変数のスコープを必要最小限に制限します。しかし、これらの利点にもかかわらず、ボブおじさんの慣行に矛盾しているように見えるので、このリファクタリングが正当化されることを元の開発者に納得させることはできません。

したがって、私の質問:インスタンス変数よりもローカル変数を支持する目的、科学的根拠は何ですか?なかなか指をつけられないようです。私の直感は、隠されたカップリングは悪いことであり、狭いスコープは広いカップリングよりも優れていることを教えてくれます。しかし、これを裏付ける科学とは何でしょうか?

逆に、私が見落としている可能性があるこのリファクタリングの欠点はありますか?

113
Alexander

インスタンス変数よりもローカル変数を優先する目的、科学的根拠は何ですか?

スコープはバイナリ状態ではなく、グラデーションです。これらを最大から最小にランク付けできます。

_Global > Class > Local (method) > Local (code block, e.g. if, for, ...)
_

編集:私が「クラススコープ」と呼んでいるのは、「インスタンス変数」という意味です。私の知る限り、これらは同義語ですが、私はJava devではなくC#の開発者です。簡潔にするため、静的はすべてグローバルカテゴリにまとめました。質問のトピック

スコープが小さいほど良いです。理論的根拠は変数は可能な限り最小のスコープ内に存在する必要があるです。これには多くの利点があります。

  • それはあなたに現在のクラスの責任について考えることを強制し、SRPに固執するのを助けます。
  • これにより、グローバルな名前の競合を回避する必要がなくなります。 2つ以上のクラスにNameプロパティがある場合、FooNameBarName、...のようにそれらにプレフィックスを付ける必要はありません。したがって、変数名を簡潔で簡潔なままにしますできるだけ。
  • 利用可能な変数(Intellisenseなど)をコンテキストに関連する変数に制限することにより、コードを整理します。
  • 何らかの形式のアクセス制御を可能にするので、知らないアクター(同僚が開発した別のクラスなど)がデータを操作することはできません。
  • これらの変数の宣言がこれらの変数の実際の使用法に可能な限り近づくことを確実にするので、コードがより読みやすくなります。
  • 過度に広いスコープで変数を宣言したいだけの場合は、OOPまたはそれを実装する方法を十分に理解していない開発者を示していることがよくあります。スコープが非常に広い変数を見ると、おそらく何かがあることを示す赤旗として機能します。 OOPアプローチで問題が発生しています(開発者一般またはコードベース固有)。
  • (Kevinによるコメント)地元住民を使用すると、正しい順序で物事を行うことを強いられます。元の(クラス変数)コードでは、誤ってpassRequestToServiceClient()をメソッドの先頭に移動しても、コンパイルされます。ローカルでは、初期化されていない変数を渡した場合にのみ、その間違いをすることができます。

しかし、これらの利点にもかかわらず、ボブおじさんの慣行に矛盾しているように見えるので、このリファクタリングが正当化されることを元の開発者に納得させることはできません。

逆に、私が見落としている可能性があるこのリファクタリングの欠点はありますか?

ここでの問題は、ローカル変数の引数が有効であるということですが、正しくない追加の変更を加えたため、提案された修正で臭いテストが失敗しました。

「クラス変数なし」の提案を理解しており、それにメリットがある一方で、実際にはメソッド自体も削除しており、それはまったく別の球技です。メソッドはそのままで、クラス変数に保存するのではなく、値を返すように変更する必要があります。

_private byte[] getEncodedData() {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
}

private EncryptionInfo getEncryptionInfo() {
    return cryptoService.getEncryptionInfoForDefaultClient();
}

// and so on...
_

私はprocessメソッドで行ったことに同意しますが、本体を直接実行するのではなく、プライベートサブメソッドを呼び出す必要がありました。

_public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    byte[] encodedData = getEncodedData();
    EncryptionInfo encryptionInfo = getEncryptionInfo();

    //and so on...

    return cryptoService.encryptResponse(payloadOfResponse);
}
_

特に、何度か再利用する必要のあるメソッドに遭遇した場合は、追加の抽象化レイヤーが必要です。 現在メソッドを再利用していない場合でも、コードの読みやすさを支援するだけの目的であっても、関連するサブメソッドを既に作成しておくことは依然として良い習慣です。 。

ローカル変数の引数に関係なく、私はすぐに、提案された修正が元の修正よりもかなり読みにくいことに気づきました。クラス変数の無駄な使用もコードの可読性を損なうことは認めますが、一目見ただけでは、すべてのロジックを単一の(今では長々とした)メソッドに積み重ねているのと比べて違います。

169
Flater

元のコードは、引数のようなメンバー変数を使用しています。引数の数を最小限に抑えると言ったとき、彼が本当に意味することは、メソッドが機能するために必要なデータの量を最小限にすることです。そのデータをメンバー変数に入れても何も改善されません。

79
Alex

他の回答はすでにローカル変数の利点を完全に説明しているので、残っているのはあなたの質問のこの部分だけです:

しかし、これらの利点にもかかわらず、私は元の開発者に、このリファクタリングが正当化されることを納得させることができないようです。

それは簡単なはずです。ボブおじさんのクリーンコードの次の引用を彼に単に指摘してください:

副作用なし

副作用は嘘です。関数は1つのことを約束しますが、他の隠されたことも行います。時々、それはそれ自身のクラスの変数に予期しない変更を加えるでしょう。場合によっては、関数またはシステムグローバルに渡されるパラメーターになります。どちらの場合も、それらは欺瞞的で有害な誤解であり、しばしば奇妙な時間結合と順序依存性をもたらします。

(例は省略)

この副作用により、一時的な結合が生じます。つまり、checkPasswordは特定の時間(つまり、セッションを初期化しても安全な場合)にのみ呼び出すことができます。順不同で呼び出されると、セッションデータが誤って失われる可能性があります。一時的な結合は、特に副作用として隠されている場合、混乱を招きます。一時的な結合が必要な場合は、関数の名前でそれを明確にする必要があります。この場合、関数checkPasswordAndInitializeSessionの名前を変更することがありますが、これは「1つのことを行う」に違反します。

つまり、ボブおじさんは、関数が引数を取る必要があると言っているだけでなく、関数は可能な限り非ローカル状態との相互作用を避けるべきだとも言っています。

47
meriton

「それは誰かの叔父が考えていることと矛盾する」は決して良い議論ではありません。絶対に。叔父から知恵を奪ってはいけません。自分で考えてください。

とはいえ、インスタンス変数は、永続的または半永続的に保存する必要がある情報を保存するために使用する必要があります。ここの情報は違います。インスタンス変数なしで実行するのは非常に簡単なので、それらは実行できます。

テスト:各インスタンス変数のドキュメントコメントを記述します。完全に無意味ではない何かを書くことができますか?そして、4つのアクセサにドキュメントコメントを書き込みます。彼らは同様に無意味です。

最悪の場合、別のcryptoServiceを使用するため、変更を復号化する方法を想定します。 4行のコードを変更する代わりに、4つのインスタンス変数を別のものに置き換え、4つのゲッターを別のものに置き換え、4行のコードを変更する必要があります。

しかし、もちろんコード行で支払われる場合は、最初のバージョンが望ましいです。 11行ではなく31行。書き込み、永続的な維持、デバッグ時の読み取り、変更が必要な場合の適応、2番目のcryptoServiceをサポートする場合の複製のために、3行以上の行。

(ローカル変数を使用すると、正しい順序で呼び出しを行わなければならないという重要なポイントを逃しました)。

25
gnasher729

インスタンス変数よりもローカル変数を優先する目的、科学的根拠は何ですか?なかなか指をつけられないようです。私の直感は、隠れたカップリングは悪いことであり、狭いスコープは広いカップリングよりも優れていることを教えてくれます。しかし、これを裏付ける科学とは何でしょうか?

インスタンス変数は、そのHostオブジェクトのプロパティを表すためのものであり、notは、オブジェクト自体よりもスコープが狭い計算のスレッドに固有のプロパティを表すためのものです。まだカバーされていないように見えるこのような区別を付ける理由のいくつかは、同時実行性と再入可能性に関係しています。メソッドがインスタンス変数の値を設定することによってデータを交換する場合、2つの同時スレッドがそれらのインスタンス変数の互いの値を簡単に破壊し、断続的で見つけにくいバグを生み出す可能性があります。

インスタンス変数に依存するデータ交換パターンがメソッドを再入不可能にするリスクが高いため、1つのスレッドだけでもこれらの線に沿って問題が発生する可能性があります。同様に、同じ変数を使用してメソッドの異なるペア間でデータを伝達する場合、メソッド呼び出しの非再帰的チェーンでさえ実行する単一のスレッドが、関連するインスタンス変数の予期しない変更に関連するバグに遭遇するリスクがあります。

このようなシナリオで確実に正しい結果を得るには、個別の変数を使用して、一方が他方を呼び出すメソッドの各ペア間で通信するか、すべてのメソッド実装に他のすべての実装の詳細をすべて考慮させる必要があります。直接または間接的に呼び出されるメソッド。これは壊れやすく、スケーリングが不十分です。

13
John Bollinger

process(...)だけを議論すると、同僚の例はビジネスロジックの意味ではるかに読みやすくなります。逆に、あなたの反例は、意味を抽出するために大ざっぱな一見以上のものをとります。

そうは言っても、クリーンなコードは判読可能で高品質です。ローカルな状態をよりグローバルな空間に押し出すことは、高レベルのアセンブリなので、品質はゼロです。

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest request) {
    checkNotNull(encryptedRequest);

    return encryptResponse
      (routeTo
         ( destination()
         , requestData(request)
         , destinationEncryption()
         )
      );
  }

  private byte[] requestData(EncryptedRequest encryptedRequest) {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private EncryptionInfo destinationEncryption() {
    return cryptoService.getEncryptionInfoForDefaultClient();
  }

  private URI destination() {
    return router.getDestination().getUri();
  }

  private EncryptedObject routeTo(URI destinationURI, byte[] encodedData, EncryptionInfo encryptionInfo) {
    return serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }

  private void encryptResponse(EncryptedObject payloadOfResponse) {
    return cryptoService.encryptResponse(payloadOfResponse);
  }
}

これは、任意のスコープで変数を使用する必要がなくなるレンディションです。はい、コンパイラはそれらを生成しますが、重要な部分はそれがそれを制御するため、コードが効率的になることです。また、比較的読みやすいです。

ネーミングのポイント。意味があり、すでに入手可能な情報を拡張した最短の名前が必要です。すなわち。 destinationURI、 'URI'は型シグネチャによってすでに認識されています。

9
Kain0_0

これらの変数とプライベートメソッドを完全に削除します。ここに私のリファクタリングがあります:

_public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    return cryptoService.encryptResponse(
        serviceClient.handle(router.getDestination().getUri(),
        cryptoService.decryptRequest(encryptedRequest, byte[].class),
        cryptoService.getEncryptionInfoForDefaultClient()));
  }
}
_

プライベートメソッドの場合、 router.getDestination().getUri()getDestinationURI()よりも明確で読みやすくなっています。同じクラスで同じ行を2回使用した場合でも、同じことを繰り返すだけです。別の見方をすると、getDestinationURI()が必要な場合、SomeBusinessProcessクラスではなく他のクラスに属している可能性があります。

変数とプロパティについては、後で使用する値を保持することが一般的に必要です。クラスにプロパティのパブリックインターフェイスがない場合、それらはおそらくプロパティであってはなりません。使用するクラスプロパティの最悪の種類は、おそらく副作用としてプライベートメソッド間で値を渡すためのものです。

とにかく、クラスはprocess()を実行するだけで済み、オブジェクトは破棄されます。状態をメモリに保持する必要はありません。さらにリファクタリングの可能性は、CryptoServiceをそのクラスから取り除くことです。

コメントに基づいて、私はこの答えを追加したいと思います。これは実際の実践に基づいています。実際、コードレビューで最初に取り上げるのは、クラスをリファクタリングして、暗号化/復号化の作業を取り除くことです。それが終わったら、メソッドと変数が必要かどうか、それらに正しい名前が付けられているかどうかなどを尋ねます。最終的なコードはおそらくこれに近いでしょう:

_public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;

  public Response process(Request request) {
    return serviceClient.handle(router.getDestination().getUri());
  }
}
_

上記のコードでは、さらにリファクタリングする必要はないと思います。ルールと同様に、いつ、いつ適用しないかを知るには経験が必要だと思います。ルールは、すべての状況で機能することが証明されている理論ではありません。

一方、コードレビューは、コードの一部が通過するまでの時間に実際の影響を与えます。私の秘訣は、コードを減らして理解しやすくすることです。変数名は、私がそれを削除できれば、論評のポイントになる可能性があります。

7
imel96

Flater's answer スコーピングの問題をかなりカバーしていますが、ここにも別の問題があると思います。

データを処理する関数と単純にがデータにアクセスする関数には違いがあることに注意してください。

前者は実際のビジネスロジックを実行しますが、後者は入力を節約し、おそらくよりシンプルで再利用可能なインターフェイスを追加することで安全性を追加します。

この場合、データアクセス関数はタイピングを保存せず、どこでも再利用されないようです(またはそれらを削除することで他の問題が発生する可能性があります)。したがって、これらの関数は存在すべきではありません。

名前付き関数にビジネスロジックのみを保持することで、両方の世界の最高のものを取得します( Flaterの回答imel96の回答 の間のどこか)。

public EncryptedResponse process(EncryptedRequest encryptedRequest) {

    byte[] requestData = decryptRequest(encryptedRequest);
    EncryptedObject responseData = handleRequest(router.getDestination().getUri(), requestData, cryptoService.getEncryptionInfoForDefaultClient());
    EncryptedResponse response = encryptResponse(responseData);

    return response;
}

// define: decryptRequest(), handleRequest(), encryptResponse()
4
user673679

最初で最も重要なこと:ボブ叔父さんは時々説教師のように見えますが、彼のルールには例外があると述べています。

Clean Codeの全体的なアイデアは、読みやすさを改善し、エラーを回避することです。互いに違反しているいくつかのルールがあります。

関数についての彼の主張は、ニラディック関数が最良であるが、最大3つのパラメーターが許容可能であるというものです。個人的には4でも大丈夫だと思います。

インスタンス変数を使用する場合、それらは一貫したクラスを作成する必要があります。つまり、変数はすべてではないにしても多くの非静的メソッドで使用されるべきです。

クラスの多くの場所で使用されていない変数は移動する必要があります。

私は元のバージョンもリファクタリングされたバージョンも最適とは考えていません。@ Flaterは、戻り値を使用して何ができるかをすでに十分に述べています。読みやすさが向上し、戻り値を使用するためのエラーが減少します。

2
kap

ローカル変数はスコープを縮小するため、変数の使用方法が制限され、特定のクラスのエラーを防止し、読みやすさが向上します。

インスタンス変数は、関数の呼び出し方法を減らし、特定のクラスのエラーを減らし、読みやすさを向上させます。

どちらかが正しいと言い、もう一方が間違っているというのは、特定のケースでは有効な結論になるかもしれませんが、一般的なアドバイスとして...

TL; DR:熱意が強すぎる理由は、熱意が高すぎるためだと思います。

1
drjpizzle

どちらも同じように動作し、パフォーマンスの違いは目立たないので、scientific引数はないと思います。それは主観的な好みに帰着します。

そして、私もあなたのやり方があなたの同僚よりも好きになる傾向があります。どうして?何人かの本の著者が言っているにもかかわらず、私はそれを読んで理解する方が簡単だと思うので。

どちらの方法でも同じことを実現できますが、彼の方法はさらに広がっています。そのコードを読むには、いくつかの関数とメンバー変数の間を行き来する必要があります。すべてが1つの場所に凝縮されているわけではありません。理解するには、頭の中ですべてを覚えておく必要があります。それははるかに大きな認知負荷です。

対照的に、あなたのアプローチはすべてをはるかに密に詰め込みますが、それを貫通不可能にするほどではありません。行ごとに読むだけで、理解するのにそれほど暗記する必要はありません。

しかし、もし彼がsedがそのような方法でレイアウトされているコードであるなら、私は彼にとってそれが逆の可能性があると想像することができます。

0
Vilx-

Get ...で始まるメソッドはvoidを返すべきではないという事実にもかかわらず、メソッド内の抽象化のレベルの分離は最初のソリューションで与えられます。 2番目のソリューションはよりスコープが広いですが、メソッドで何が行われているのかを推測するのはさらに困難です。ローカル変数の割り当てはここでは必要ありません。メソッド名を保持し、コードを次のようなものにリファクタリングします。

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    return getEncryptedResponse(
            passRequestToServiceClient(getDestinationURI(), getEncodedData(encryptedRequest) getEncryptionInfo())
        );
  }

  private EncryptedResponse getEncryptedResponse(EncryptedObject encryptedObject) {
    return cryptoService.encryptResponse(encryptedObject);
  }

  private byte[] getEncodedData(EncryptedRequest encryptedRequest) {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private EncryptionInfo getEncryptionInfo() {
    return cryptoService.getEncryptionInfoForDefaultClient();
  }

  private URI getDestinationURI() {
    return router.getDestination().getUri();
  }

  private EncryptedObject passRequestToServiceClient(URI destinationURI, byte[] encodedData, EncryptionInfo encryptionInfo) {
    return serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }
}
0
Roman Weis