なぜ伸びるのは悪

extendsキーワードは悪です。チャールズマンソンレベルではないかもしれませんが、可能な限り避けなければならないほど悪いです。Gang of Four Design Patternsの本では、実装の継承(extends)をインターフェイスの継承(implements)に置き換える方法について詳しく説明しています。

優れた設計者は、具体的な基本クラスではなく、インターフェイスの観点からコードのほとんどを記述します。この記事では設計者がそのような奇妙な習慣を持っている理由を説明しインターフェイスベースのプログラミングの基本をいくつか紹介します。

インターフェースとクラス

私はかつて、James Gosling(Javaの発明者)が注目の講演者であるJavaユーザーグループ会議に出席しました。思い出に残るQ&Aセッション中に、誰かが彼に「Javaをもう一度やり直すことができたら、何を変えますか?」と尋ねました。「クラスは省略します」と彼は答えた。笑いが収まった後、彼は本当の問題はクラス自体ではなく、実装の継承(extends関係)であると説明しました。インターフェイスの継承(implements関係)が望ましいです。可能な限り、実装の継承は避けてください。

柔軟性を失う

なぜ実装の継承を避ける必要があるのですか?最初の問題は、具体的なクラス名を明示的に使用すると、特定の実装に固定され、ダウンザラインの変更が不必要に困難になることです。

現代のアジャイル開発手法の中核は、並列設計と開発の概念です。プログラムを完全に指定する前に、プログラミングを開始します。この手法は、プログラミングを開始する前に設計を完了する必要があるという従来の知恵に直面していますが、多くの成功したプロジェクトでは、従来のパイプラインアプローチよりもこの方法で高品質のコードをより迅速に(そして費用効果的に)開発できることが証明されています。ただし、並列開発の中核となるのは、柔軟性の概念です。新しく発見された要件を既存のコードにできるだけ簡単に組み込むことができるように、コードを作成する必要があります。

必要になる可能性のある機能を実装するのでなく、確実に必要機能のみを実装しますが、変更に対応できる方法で実装します。この柔軟性がなければ、並行開発は不可能です。

インターフェイスへのプログラミングは、柔軟な構造の中核です。その理由を理解するために、それらを使用しない場合に何が起こるかを見てみましょう。次のコードについて考えてみます。

f(){LinkedList list = new LinkedList(); // ... g(リスト); } g(LinkedList list){list.add(...); g2(リスト)}

ここで、高速ルックアップの新しい要件が出現したため、LinkedListが機能していないとします。に置き換える必要がありHashSetます。既存のコードでは、変更する必要f()があるだけでなくg()LinkedList引数を取る)、すべてg()がリストをに渡すため、その変更はローカライズされません。

次のようにコードを書き直します。

f(){コレクションリスト= new LinkedList(); // ... g(リスト); } g(コレクションリスト){list.add(...); g2(リスト)}

単に置き換えることによって、ハッシュテーブルにリンクされているリストを変更することが可能になるnew LinkedList()new HashSet()。それでおしまい。他の変更は必要ありません。

別の例として、次のコードを比較してください。

f(){コレクションc = new HashSet(); // ... g(c); } g(Collection c){for(Iterator i = c.iterator(); i.hasNext();)do_something_with(i.next()); }

これに:

f2(){コレクションc = new HashSet(); // ... g2(c.iterator()); } g2(Iterator i){while(i.hasNext();)do_something_with(i.next()); }

このg2()メソッドはCollection、から取得できるキーリストと値リストだけでなく、導関数もトラバースできるようになりましたMap。実際、コレクションをトラバースする代わりに、データを生成するイテレーターを作成できます。テストスキャフォールドまたはファイルからプログラムに情報を供給するイテレーターを作成できます。ここには非常に大きな柔軟性があります。

カップリング

実装の継承に関するより重大な問題は、結合です。つまり、プログラムのある部分が別の部分に依存するという望ましくない問題です。グローバル変数は、強い結合が問題を引き起こす理由の典型的な例を提供します。たとえば、グローバル変数のタイプを変更すると、その変数を使用する(つまり、変数に結合される)すべての関数が影響を受ける可能性があるため、このすべてのコードを調べ、変更し、再テストする必要があります。さらに、変数を使用するすべての関数は、変数を介して相互に結合されます。つまり、変数の値が厄介なときに変更された場合、ある関数が別の関数の動作に誤って影響を与える可能性があります。この問題は、マルチスレッドプログラムでは特に恐ろしいものです。

設計者は、結合関係を最小限に抑えるように努める必要があります。あるクラスのオブジェクトから別のクラスのオブジェクトへのメソッド呼び出しは疎結合の一形態であるため、結合を完全に排除することはできません。カップリングなしでプログラムを作ることはできません。それでも、オブジェクト指向(オブジェクト指向)の原則に従うことで、結合を大幅に最小限に抑えることができます(最も重要なのは、オブジェクトの実装を、それを使用するオブジェクトから完全に隠す必要があることです)。たとえば、オブジェクトのインスタンス変数(定数ではないメンバーフィールド)は、常にprivate。である必要があります。限目。例外なし。これまで。私は真剣です。 (protectedメソッドを効果的に使用できる場合もありますが、protected インスタンス変数は忌まわしいものです。)同じ理由でget / set関数を使用しないでください。これらは、フィールドを公開するための非常に複雑な方法です(ただし、基本型の値ではなく本格的なオブジェクトを返すアクセス関数は返されるオブジェクトのクラスが設計の重要な抽象化である状況では妥当です)。

私はここで衒学者ではありません。自分の作業には、OOアプローチの厳密さ、迅速なコード開発、および簡単なコード保守の間に直接的な相関関係があることがわかりました。実装の非表示などの中心的なOOの原則に違反するたびに、そのコードを書き直すことになります(通常、コードをデバッグできないため)。プログラムを書き直す時間がないので、ルールに従います。私の懸念は完全に実用的です—私は純粋さのために純粋さに興味がありません。

壊れやすい基本クラスの問題

それでは、結合の概念を継承に適用しましょう。を使用する実装継承システムではextends、派生クラスは基本クラスに非常に緊密に結合されており、この密接な接続は望ましくありません。設計者は、この動作を説明するために「壊れやすい基本クラスの問題」というモニカを適用しました。基本クラスは一見安全な方法で変更できるため、脆弱であると見なされますが、この新しい動作が派生クラスに継承されると、派生クラスが誤動作する可能性があります。基本クラスのメソッドを分離して調べるだけでは、基本クラスの変更が安全かどうかを判断することはできません。すべての派生クラスも確認(およびテスト)する必要があります。さらに、基本クラス基本クラスの両方を使用するすべてのコードをチェックする必要がありますこのコードも新しい動作によって壊れている可能性があるため、派生クラスオブジェクトも同様です。キー基本クラスを変更するだけで、プログラム全体が動作しなくなる可能性があります。

壊れやすい基本クラスと基本クラスの結合の問題を一緒に調べてみましょう。次のクラスは、JavaのArrayListクラスを拡張して、スタックのように動作させるようにします。

class Stack extends ArrayList {private int stack_pointer = 0; public void push(Object article){add(stack_pointer ++、article); } public Object pop(){return remove(-stack_pointer); } public void push_many(Object [] articles){for(int i = 0; i <articles.length; ++ i)push(articles [i]); }}

これほど単純なクラスでも問題があります。ユーザーが継承を利用し、ArrayListclear()メソッドを使用してすべてをスタックからポップするとどうなるかを考えてみましょう。

Stack a_stack = new Stack(); a_stack.push("1"); a_stack.push("2"); a_stack.clear(); 

The code successfully compiles, but since the base class doesn't know anything about the stack pointer, the Stack object is now in an undefined state. The next call to push() puts the new item at index 2 (the stack_pointer's current value), so the stack effectively has three elements on it—the bottom two are garbage. (Java's Stack class has exactly this problem; don't use it.)

One solution to the undesirable method-inheritance problem is for Stack to override all ArrayList methods that can modify the array's state, so the overrides either manipulate the stack pointer correctly or throw an exception. (The removeRange() method is a good candidate for throwing an exception.)

This approach has two disadvantages. First, if you override everything, the base class should really be an interface, not a class. There's no point in implementation inheritance if you don't use any of the inherited methods. Second, and more importantly, you don't want a stack to support all ArrayList methods. That pesky removeRange() method isn't useful, for example. The only reasonable way to implement a useless method is to have it throw an exception, since it should never be called. This approach effectively moves what would be a compile-time error into runtime. Not good. If the method simply isn't declared, the compiler kicks out a method-not-found error. If the method's there but throws an exception, you won't find out about the call until the program actually runs.

A better solution to the base-class issue is encapsulating the data structure instead of using inheritance. Here's a new-and-improved version of Stack:

class Stack { private int stack_pointer = 0; private ArrayList the_data = new ArrayList(); public void push( Object article ) { the_data.add( stack_pointer++, article ); } public Object pop() { return the_data.remove( --stack_pointer ); } public void push_many( Object[] articles ) { for( int i = 0; i < o.length; ++i ) push( articles[i] ); } } 

So far so good, but consider the fragile base-class issue. Let's say you want to create a variant on Stack that tracks the maximum stack size over a certain time period. One possible implementation might look like this:

class Monitorable_stack extends Stack { private int high_water_mark = 0; private int current_size; public void push( Object article ) { if( ++current_size > high_water_mark ) high_water_mark = current_size; super.push(article); } public Object pop() { --current_size; return super.pop(); } public int maximum_size_so_far() { return high_water_mark; } } 

This new class works well, at least for a while. Unfortunately, the code exploits the fact that push_many() does its work by calling push(). At first, this detail doesn't seem like a bad choice. It simplifies the code, and you get the derived class version of push(), even when the Monitorable_stack is accessed through a Stack reference, so the high_water_mark updates correctly.

One fine day, someone might run a profiler and notice the Stack isn't as fast as it could be and is heavily used. You can rewrite the Stack so it doesn't use an ArrayList and consequently improve the Stack's performance. Here's the new lean-and-mean version:

class Stack { private int stack_pointer = -1; private Object[] stack = new Object[1000]; public void push( Object article ) { assert stack_pointer = 0; return stack[ stack_pointer-- ]; } public void push_many( Object[] articles ) { assert (stack_pointer + articles.length) < stack.length; System.arraycopy(articles, 0, stack, stack_pointer+1, articles.length); stack_pointer += articles.length; } } 

Notice that push_many() no longer calls push() multiple times—it does a block transfer. The new version of Stack works fine; in fact, it's better than the previous version. Unfortunately, the Monitorable_stack derived class doesn't work any more, since it won't correctly track stack usage if push_many() is called (the derived-class version of push() is no longer called by the inherited push_many() method, so push_many() no longer updates the high_water_mark). Stack is a fragile base class. As it turns out, it's virtually impossible to eliminate these types of problems simply by being careful.

インターフェイスの継承を使用する場合は、この問題が発生しないことに注意してください。継承された機能に悪影響を与えることはありません。StackがaSimple_stackとaの両方で実装されたインターフェイスの場合Monitorable_stack、コードははるかに堅牢です。