誰もこれについて話し合っているのを見たことがないので、もっと考えてみます。短い答え/アドバイスは、値を返すのが簡単だと思うという理由だけで、ローカル変数よりもインスタンス変数を使用しないことです。ローカル変数とインスタンス変数を適切に使用しないと、コードの操作が非常に難しくなります。追跡が非常に困難な重大なバグがいくつか発生します。重大なバグの意味と、それがどのように見えるかを理解したい場合は、読み進めてください。
関数への書き込みを提案するように、インスタンス変数のみを使用してみましょう。非常に単純なクラスを作成します。
public class BadIdea {
public Enum Color { GREEN, RED, BLUE, PURPLE };
public Color[] map = new Colors[] {
Color.GREEN,
Color.GREEN,
Color.RED,
Color.BLUE,
Color.PURPLE,
Color.RED,
Color.PURPLE };
List<Integer> indexes = new ArrayList<Integer>();
public int counter = 0;
public int index = 0;
public void findColor( Color value ) {
indexes.clear();
for( index = 0; index < map.length; index++ ) {
if( map[index] == value ) {
indexes.add( index );
counter++;
}
}
}
public void findOppositeColors( Color value ) {
indexes.clear();
for( index = 0; i < index < map.length; index++ ) {
if( map[index] != value ) {
indexes.add( index );
counter++;
}
}
}
}
これは私が知っているばかげたプログラムですが、これを使用して、このようなことにインスタンス変数を使用することは非常に悪い考えであるという概念を説明することができます。最も重要なことは、これらのメソッドがすべてのインスタンス変数を使用していることです。そして、呼び出されるたびに、インデックス、カウンター、およびインデックスを変更します。最初に見つかる問題は、これらのメソッドを次々と呼び出すと、以前の実行からの回答が変更される可能性があることです。たとえば、次のコードを記述したとします。
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
idea.findColor( Color.GREEN ); // whoops we just lost the results from finding all Color.RED
findColor はインスタンス変数を使用して戻り値を追跡するため、一度に 1 つの結果しか返すことができません。もう一度呼び出す前に、これらの結果への参照を保存してみましょう。
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findColor( Color.GREEN ); // this causes red positions to be lost! (i.e. idea.indexes.clear()
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
この 2 番目の例では、3 行目の赤い位置を保存しましたが、同じことが起こりました!?なぜそれらを失ったのでしょうか?! idea.indexes が割り当てられたのではなくクリアされたため、一度に使用できる回答は 1 つだけです。再度呼び出す前に、その結果の使用を完全に終了する必要があります。メソッドを再度呼び出すと、結果がクリアされ、すべてが失われます。これを修正するには、毎回新しい結果を割り当てる必要があるため、赤と緑の回答が別々になります。それでは、答えを複製して新しいコピーを作成しましょう。
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes.clone();
int redCount = idea.counter;
idea.findColor( Color.GREEN );
List<Integer> greenPositions = idea.indexes.clone();
int greenCount = idea.counter;
最後に、2 つの別々の結果が得られました。赤と緑の結果が分離されました。しかし、プログラムが機能する前に、BadIdea が内部でどのように動作しているかについて多くのことを知る必要がありましたね。結果が破壊されないように、呼び出すたびに戻り値を複製することを忘れないでください。発信者がこれらの詳細を覚えておく必要があるのはなぜですか? それをする必要がなければ、もっと簡単ではないでしょうか?
また、呼び出し元はローカル変数を使用して結果を記憶する必要があることにも注意してください。したがって、BadIdea のメソッドでローカル変数を使用しなかった場合でも、呼び出し元はそれらを使用して結果を記憶する必要があります。それで、あなたは本当に何を達成しましたか?あなたは本当に問題を発信者に移し、発信者にもっとやらせるように強制しました。また、呼び出し元にプッシュする作業は、規則には多くの例外があるため、従うのが簡単な規則ではありません。
それでは、2 つの異なる方法でそれを実行してみましょう。私がいかに「賢く」、同じインスタンス変数を再利用して「メモリを節約」し、コードをコンパクトに保ったかに注目してください。;-)
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findOppositeColors( Color.RED ); // this causes red positions to be lost again!!
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
同じことが起こりました!くそー、でも私はとても「賢く」、メモリを節約していたので、コードはより少ないリソースを使用していました!!! これは、メソッドの呼び出しが順序に依存するように、インスタンス変数を使用することの本当の危険です。メソッド呼び出しの順序を変更すると、BadIdea の基本的な状態を実際に変更していなくても、結果が異なります。マップの内容は変更していません。メソッドを異なる順序で呼び出すと、プログラムの結果が異なるのはなぜですか?
idea.findColor( Color.RED )
idea.findOppositeColors( Color.RED )
これら 2 つの方法を交換した場合とは異なる結果が生成されます。
idea.findOppositeColors( Color.RED )
idea.findColor( Color.RED )
これらのタイプのエラーは、特にこれらの行が隣り合っていない場合、追跡するのが非常に困難です。これらの 2 行の間に新しい呼び出しを追加するだけで、プログラムを完全に壊すことができ、まったく異なる結果が得られます。確かに、処理する行数が少ない場合は、エラーを簡単に見つけることができます。しかし、大規模なプログラムでは、プログラム内のデータが変更されていなくても、それらを再現しようとして何日も無駄にする可能性があります。
そして、これはシングルスレッドの問題だけを見ています。BadIdea がマルチスレッドの状況で使用されていた場合、エラーは非常に奇妙なものになる可能性があります。findColors() と findOppositeColors() が同時に呼び出されるとどうなりますか? 墜落 髪が抜け落ち 死と時空が崩壊して特異点 宇宙が飲み込まれる?おそらく、そのうちの少なくとも2つ。スレッドはおそらくあなたの頭の上にありますが、スレッドにたどり着いたときに、それらの悪い慣行があなたに本当の心痛を引き起こさないように、私たちが今あなたを悪いことから遠ざけることができることを願っています.
メソッドを呼び出すときに注意する必要があることに気付きましたか? それらは相互に上書きし、おそらくランダムにメモリを共有し、外部で機能させるために内部でどのように機能したかの詳細を覚えておく必要があり、呼び出される順序を変更すると、次の行に非常に大きな変化が生じます。シングルスレッドの状況でしか機能しませんでした。このようなことを行うと、触れるたびに崩壊するように見える非常に脆弱なコードが生成されます。私が示したこれらのプラクティスは、コードが脆弱であることに直接貢献しました。
これはカプセル化のように見えるかもしれませんが、それをどのように記述したかという技術的な詳細が呼び出し元に知られている必要があるため、これは正反対です。呼び出し元は、コードを機能させるために非常に特殊な方法でコードを記述する必要があり、コードの技術的な詳細を知らずにそれを行うことはできません。これは、クラスが抽象化/インターフェイスの背後にある技術的な詳細を隠すと想定されているため、リーキーな抽象化と呼ばれることがよくありますが、技術的な詳細が漏れて、呼び出し元に動作の変更を余儀なくさせます。すべてのソリューションにはある程度の漏れがありますが、これらのような上記の手法のいずれかを使用すると、解決しようとしている問題が何であれ、それらを適用するとひどく漏れることが保証されます. それでは、GoodIdea を見てみましょう。
ローカル変数を使って書き直してみましょう:
public class GoodIdea {
...
public List<Integer> findColor( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] == value ) {
results.add( i );
}
}
return results;
}
public List<Integer> findOppositeColors( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] != value ) {
results.add( i );
}
}
return results;
}
}
これにより、上で説明したすべての問題が修正されます。カウンターを追跡したり返したりしていないことはわかっていますが、追跡した場合は、新しいクラスを作成して、リストの代わりにそれを返すことができます。複数の結果をすばやく返すために、次のオブジェクトを使用することがあります。
public class Pair<K,T> {
public K first;
public T second;
public Pair( K first, T second ) {
this.first = first;
this.second = second;
}
}
長い答えですが、非常に重要なトピックです。