Interface (インターフェース) と Class (クラス) ?
あるとき、私は Java ユーザー グループの会議に出席しました。カンファレンスでは、James Gosling (Java の父) が発起人としてスピーチを行いました。その記憶に残る Q&A セグメントで、彼は「Java をリファクタリングするとしたら、何を変更しますか?」と質問されました。 「授業をやめたい」と彼は答えた。笑いが収まった後、本当の問題はクラスそのものではなく、継承関係の実装にあることが説明されました。インターフェイスの継承 (関係の実装) の方が優れています。継承の実装はできる限り避ける必要があります。
柔軟性の喪失
継承を避けるべき理由は何ですか?最初の問題は、具体的なクラス名を明示的に使用すると、特定の実装にロックされてしまい、基礎となる変更が不必要に困難になることです。
現在のアジャイル プログラミング手法では、設計と開発を並行して行うという概念が中心となります。プログラムを詳細に設計する前に、プログラミングを開始します。この手法は、コーディングを開始する前に設計を完了する必要がある従来のアプローチとは異なりますが、多くの成功したプロジェクトにより、従来の段階的な手法よりも迅速に高品質のコードを開発できることが証明されています。しかし、並行開発の中核となるのは柔軟性です。新しく発見された要件をできるだけ簡単に既存のコードにマージできるような方法でコードを作成する必要があります。
必要な機能を実装するのではなく、明らかに必要な機能のみを実装し、適度に変化を許容する必要があります。このような柔軟な並行開発がなければ、それはまったく不可能です。
インターフェイスのプログラミングは、柔軟な構造の中核です。その理由を説明するために、それらが使用されると何が起こるかを見てみましょう。次のコードを考えてみましょう。
f()
{
LinkedList list = new LinkedList();
//...
g( list );
}
g(LinkedList list)
{
list.add(...);
g2(list)
}
高速クエリのニーズがあると仮定します。が発生したため、この LinkedList は解決できません。代わりに HashSet を使用する必要があります。既存のコードでは、f() だけでなく、g() (LinkedList パラメーターを受け取る)、および g() がリストを渡すコードも変更する必要があるため、変更をローカライズすることはできません。コードを次のように書き換えます。
f()
{
Collection list = new LinkedList();
//...
g( list );
}
g( Collection list )
{
list.add( ... );
g2( list )
}
リンクリストを次のように変更します。ハッシュの場合は、new LinkedList() の代わりに単に new HashSet() を使用することもできます。それだけです。他に変更するものはありません。
別の例として、次の 2 つのコードを比較します。
f()
{
Collection c = new HashSet();
//...
g( c );
}
g( Collection c )
{
for( Iterator i = c.iterator(); i.hasNext() )
do_something_with ( i.next() );
}
and
f2()
{
Collection c = new HashSet();
//。 . .
g2( c.iterator() );
}
g2( Iterator i )
{
while( i.hasNext() )
do_something_with( i . next() );
}
g2() メソッドは、マップからキーと値のペアを取得できるのと同じように、コレクションの導出を反復できるようになりました。実際、コレクションを走査する代わりにデータを生成するイテレータを作成できます。テスト フレームワークまたはファイルから情報を取得するイテレーターを作成できます。これにより、非常に高い柔軟性が実現します。
カップリング
継承を実装する場合、より重要な問題はカップリング、つまり厄介な依存関係、つまりプログラムのある部分が別の部分に依存することです。グローバル変数は、強結合が問題を引き起こす原因の典型的な例を提供します。たとえば、グローバル変数の型を変更すると、その変数を使用するすべての関数が影響を受ける可能性があるため、このコードすべてを検査、変更、再テストする必要があります。さらに、この変数を使用するすべての関数は、この変数を通じて相互に結合されます。つまり、使いにくいときに変数値が変更されると、ある関数が別の関数の動作に誤った影響を与える可能性があります。この問題はマルチスレッド プログラムに大きく隠されています。
設計者は、結合関係を最小限に抑えるよう努める必要があります。あるクラスのオブジェクトから別のクラスのオブジェクトへのメソッド呼び出しは疎結合の一種であるため、結合を完全に排除することはできません。カップリングなしではプログラムを作成することはできません。ただし、OO ルールに従うことで、ある程度の結合を最小限に抑えることができます (最も重要なのは、オブジェクトの実装は、それを使用するオブジェクトから完全に隠蔽される必要があるということです)。たとえば、オブジェクトのインスタンス変数 (定数ではないフィールド) は常にプライベートである必要があります。一定期間、例外なく、継続的にという意味です。 (保護されたメソッドを効果的に使用することもできますが、保護されたインスタンス変数は忌まわしいものです。) 同じ理由で、get/set 関数は使用すべきではありません。(return 修飾子があるにもかかわらず、ドメインに公開するには複雑すぎるように感じます)。プリミティブ値ではなくオブジェクトの関数にアクセスする理由は場合によってはあり、その場合、返されるオブジェクト クラスが設計における重要な抽象化になります。
ここで、私は本好きではありません。私自身の仕事の中で、OO アプローチの厳密さ、迅速なコード開発、および簡単なコード実装の間に直接の相関関係があることがわかりました。実装の隠蔽など、オブジェクト指向の中心的な原則に違反すると、そのコードを書き直すことになります (通常、コードがデバッグできないため)。コードを書き直す時間がないので、そのルールに従います。私が懸念しているのは純粋に実用的な理由ですか? きれいな理由には興味がありません。
脆弱な基底クラスの問題
さて、カップリングの概念を継承に適用してみましょう。 extends を使用する実装システムでは、派生クラスは基本クラスと非常に緊密に結合されており、この緊密な結合は望ましくありません。設計者は、この動作を説明するために「脆弱な基本クラスの問題」というニックネームを付けました。基本クラスは一見安全に変更できるように見えるため、脆弱であると考えられていますが、派生クラスから継承する場合、新しい動作により派生クラスが機能不全になる可能性があります。基本クラスを単独で検査するだけでは、基本クラスへの変更が安全かどうかを判断することはできません。むしろ、すべての派生クラスも確認 (およびテスト) する必要があります。さらに、基本クラス オブジェクトと派生クラス オブジェクトでも使用されているすべてのコードを確認する必要があります。このコードは新しい動作によって破損する可能性があるためです。基本クラスを単純に変更すると、プログラム全体が動作不能になる可能性があります。
壊れやすい基底クラスと基底クラスの結合の問題を調べてみましょう。次のクラスは、Java の ArrayList クラスを拡張して、スタックのように動作させるようにします。
class Stack extends ArrayList
{
private int stack_pointer = 0;
public void Push ( オブジェクトの記事)
{
add( stack_pointer ,article );
}
public Object Pop()
{
return remove( --stack_pointer );
}
public void Push_many(Object[]articles)
{
for( int i = 0; i
}
}
このような単純なクラスにも問題があります。ユーザーが継承のバランスをとり、ArrayList の clear() メソッドを使用してスタックをポップする場合を考えてみましょう:
Stack a_stack = new Stack();
a_stack.push("1");
a_stack.push ("2");
a_stack.clear();
このコードは正常にコンパイルされますが、基本クラスはスタック ポインター スタックについて認識していないため、スタック オブジェクトは現在未定義の状態です。次に Push() を呼び出すと、新しい項目がインデックス 2 に配置されます。 (stack_pointer の現在の値) したがって、スタックには実質的に 3 つの要素があり、下の 2 つはガベージです。 (Java のスタック クラスにはこの問題があるため、使用しないでください)。
この迷惑な継承メソッドの問題の解決策は、配列の状態を変更できる Stack のすべての ArrayList メソッドをオーバーライドすることです。スタック ポインタが正しく動作するか、例外をスローします。 (removeRange() メソッドは、例外をスローするのに適した候補です)。
この方法には 2 つの欠点があります。まず、すべてを網羅すると、基本クラスは実際にはクラスではなくインターフェイスになるはずです。継承されたメソッドを使用しない場合、継承を実装する意味がありません。次に、より重要なことですが、スタックですべての ArrayList メソッドをサポートすることはできません。たとえば、煩わしいremoveRange()は何も行いません。役に立たないメソッドを実装する唯一の合理的な方法は、メソッドが呼び出されるべきではないため、例外をスローさせることです。この方法は、コンパイル エラーを効果的に実行時エラーに変換します。問題は、メソッドが単に定義されていない場合、コンパイラはメソッドが見つからないというエラーを出力することです。メソッドが存在しても例外をスローした場合、プログラムが実際に実行されるまで呼び出しエラーがわかりません。
この基本クラスの問題に対するより良い解決策は、継承を使用する代わりにデータ構造をカプセル化することです。これはスタックの新しく改良されたバージョンです:
class Stack
{
private int stack_pointer = 0;
private ArrayList the_data = new ArrayList();
public void Push (オブジェクトの記事 )
{
the_data.add( stack_poniter , 記事 );
}
public Object Pop()
{
return the_data.remove( --stack_pointer ) ;
}
public void Push_many( Object[] 記事 )
{
for( int i = 0; i push( 記事 [ i] );
}
}
ここまでは順調ですが、基底クラスの脆弱性の問題を考慮して、スタックに変数を作成し、それをセクション Tracks で使用したいとします。サイクル中の最大スタック サイズ。考えられる実装は次のようになります。
class Monitorable_stack extends Stack
{
private int high_water_mark = 0;
private int current_size ;
public void Push( オブジェクト記事)
{
if( current_size > high_water_mark )
high_water_mark = current_size;
super.push(article );
}
publish Object Pop()
{
--current_size;
return super.pop();
}
public int minimum_size_so_far()
{
return high_water_mark;
}
}
この新しいクラスは、少なくともしばらくの間は問題なく機能しました。残念ながら、このコードは、push_many() が Push() を呼び出すことによって動作するという事実を悪用しています。まず第一に、この詳細は悪い選択とは思えません。これによりコードが簡素化され、Monitorable_stack が Stack 参照を通じてアクセスされる場合でも、push() の派生バージョンを取得できるため、high_water_mark が正しく更新されます。
以上がJava での継承例の分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。