オブジェクト指向プログラミングが始まって以来、1つの基本的な問題がオブジェクト指向言語設計の悩みの種でした。ドメイン分析において開発する存在論は、複数の親から継承されるクラスを含む傾向があります。これは単に、実際のオブジェクトがシンプルな単一継承の階層に適合しないという理由によるものです。お気に入りのおいしいビールも、おなかを満たしてはくれないといったところでしょうか。一方で、プログラミング言語の多重継承を可能にすると、セマンティクスがひどく複雑なものになります。
この複雑さを言語に取り入れるとバグが増える可能性があるため、Java言語は、単一継承に固執したアプローチを採用してきました (セマンティクスが非常に単純なインターフェース継承を除く)。その結果、Javaプログラムの多くのクラス構造には、継承階層の複数の分岐に従ってコピーされたコードか、または、Chain of Responsibility、Command、あるいはStrategyのデザイン・パターンを使用した、間接アクセス用に追加されたコードが含まれます。
たとえば、GUIライブラリーのスクロール可能なペインに関するUML分析図の次の例について考えてみましょう。
図1. 選択GUI要素の分析図
この図を直接Javaプログラミングのクラス階層に変換できれば理想的ですが、単一継承であるためできません。多重インターフェース継承を使えば、対応するインターフェースのセットを作成できますが、これらのインターフェースを実装するクラスは、直接Javaの継承構造に従えません。代わりに、継承階層の複数のパスに渡ってコードをコピーするか、コードのコピーを回避するには、Strategyパターン (または、包含を使用した他の何らかの方法) を使用しなければなりません。ただし、どちらのアプローチも完全ではありません。
多重継承はエラーが発生しやすく、単一継承は制限的すぎるとしたら、双方の利点を提供し、Javaプログラミングに追加できる何らかの言語機能はないのでしょうか。実はあるのです。それは、ミックスインとして知られています。
ミックスインは、親クラスによってパラメーター化されたクラスです。または、クラスを新しいサブクラスへマッピングする関数とも考えることができます。ミックスインは、特定のコンテキストの要件ごとに、異なる親クラスでインスタンス化することができます。
たとえば、図1のScrollPaneのクラス階層は、次のようにミックスインを使用して実装することができるでしょう (矢印付きの破線は、ミックスインから親クラスへのインスタンス化の関係を表す)。
図2. ミックスイン継承図
図2では、Scrollable クラスを、異なるコンテキストの異なるクラスを拡張できるミックスインに変えました。このコンテキストでは、Scrollable をインスタンス化してPane を拡張し、ScrollPane を作成します。また、Scrollable をインスタンス化してDialog を拡張したり、さまざまなコンテキストに応じて、あらゆる種類の他のGUIコンポーネントを拡張することができます。
ミックスインという言葉が初めて使用されたのは、CLOSコミュニティーでした。このコミュニティーでのミックスインとは、実は、この言語の多重継承の扱いにくさを制御するためのデザイン・パターンでした。ミックスイン・デザイン・パターンは、C++ コミュニティーでも、同じ目的で適用されました。
そのようなクラスを、さまざまな方法で他のクラスと一緒にミックスできるため、ミックスインという名前が使用されました。ミックスインは、これらの言語では単なるデザイン・パターンですが、ミックスインを言語レベルでサポートできない理由はありません。ミックスインをJava言語に追加するための多くの提案がありましたが、これまでのところ、最も人気のある提案は、Jamです。これは、イタリアの研究者である、Davide Ancona氏、Giovanni Lagorio氏、Elena Zucca氏が提案した、ミックスインによるJavaの拡張です。
Jamは、Javaプラットフォーム、v1.0の下位互換性のある拡張です (mixin とinherited という2つの新しいキーワードを持つ)。.NETに対するJavaプログラムを書いているのでない限り、Javaのかなり古いバージョンが使用されている可能性があることは明らかですが、基本的な設計は、より現代的なバージョンに拡張することができるでしょう。
実装は、Jam - Java言語変換プログラムとして提供されます。jamc 実装は、プログラムの完全な型チェックを実行しないことに注意してください。代わりに、この実装は、Javaソースへの変換を行い、Java型チェッカーによって型エラーをキャッチします。これにより、Jam実装はよりシンプルになりますが、同時にコンパイラーから返されるエラー・メッセージの診断がより難しくなることを意味します。その理由は、実際に作成したソース・コードから1段階除かれたものに対するエラーメッセージになってしまうからです。結局は、独立したJamの型チェッカーが本番用に不可欠となるでしょう。
Jamでは、inherited <signature> のように、親クラスに必要なメソッドが、ミックスイン・クラスのdef 内部の宣言によって宣言されます。
ミックスインのインスタンス化は、class NAME = MIXIN extends CLASS {CONSTRUCTOR*}のように記述されます。
CONSTRUCTOR 作成の終わりの* は、その作成のオカレンスがないかまたはそれ以上のオカレンスがあることを示します。コンストラクターがミックスインのインスタンス化で指定されない場合は、デフォルトの0引数 (zeroary) コンストラクターが想定されます。
たとえば、UML図 (図2) に採用されるミックスインは、次のように作成できるでしょう (Panes にsetVisible() メソッドが含まれ、ミックスインScrollable にmaxScrollSize フィールドが含まれる)。
リスト1. Jamでミックスインをインスタンス化する
class Pane {
...
void setVisible(boolean value) {
...
}
}
class DialogBox {
...
}
mixin Scrollable {
int maxScrollSize;
inherited void setVisible(boolean value);
}
class ScrollDialog = Scrollable extends DialogBox {
ScrollDialog() {
this.maxScrollSize = 10;
}
}
class ScrollPane = Scrollable extends Pane {
ScrollPane(int maxScrollSize) {
this.maxScrollSize = maxScrollSize;
}
}
|
Jamは、「ミックスインのコピーの原理」として知られている次の原則に従います。
親クラスP でミックスインM をインスタンス化することによって取得されるクラスは、M で定義されるすべてのコンポーネントのコピーを本体に含む、P の通常の継承者と同じ振る舞いを持つ。
ミックスインの概念は多くの言語に適用されていますが、Jamは、強く型付けされた言語のコンテキストで厳密にミックスイン・ベース・プログラミングを導入するという点で新しいものです。Jamのミックスインは、普通のクラスと同じように型を定義します。ミックスインのインスタンス化には、ミックスインの型と親の型の両方があります。ミックスインは、複数のインターフェースを実装することができます。
コンストラクターはミックスインでは宣言できませんが、ミックスインのインスタンス化に対してのみ宣言することができます。Jamステートの設計機能として、コンストラクターは、「その固有のクラスの実装と緊密に結合しているため、署名はあまり汎用的でない」という理由から、設計の選択肢として許容されませんでした。
この言語について注意すべきいくつかの一般的な性質があります。
- フィールド・メンバーは、標準のJava言語と同じ規則によってアクセスできます。
- 静的メンバーは、ミックスインをインスタンス化したものに対応しています。ミックスインの静的メンバーが「共有される」ことはありません。
さらに、Jamでは、ミックスインのインスタンス化に5つの制約があります。
- 不法オーバーライド / 隠蔽。 対応する「コピーされた」クラスが合法である (つまり、メソッドに、オーバーライドされた親クラスのメソッドと、同じ引数型で異なるreturn 型がある、異なるthrows 文節がある、もしくは、static対インスタンスのような互換性のない修飾子がある、といったことがない)場合は、親クラスのメソッドの偶然のオーバーライド (「偶発的な」オーバーライドとしても知られる) が許容されます。
- あいまいな多重定義(オーバーロード)。 メソッドの引数がミックスイン型である可能性があるため、あいまいな多重定義は問題となります。これにより多重定義された2つのメソッドが共に適用可能で、どちらがより適切であるか判断できないという状況がありえるからです。この問題は、2つのメソッドが同じ数の同じ型の引数を持っている場合に、多重定義を許容しないことによって解決されます。これらのメソッドで異なる参照型の引数を持つ場合は除きます。
- メソッドの注釈。 継承されたメソッドには、「親の」型の注釈が付きます。
- クラスのみのインスタンス化。 Jamミックスインは、クラスでのみインスタンス化することができます。Jiazziのコンポーネントと異なり、ミックスインには合成という概念はありません (ただしJamチームは、そのような拡張について検討してみたいと考えています)。Jiazziおよびコンポーネント・ベース・プログラミングの詳細については、参考文献を参照してください。
- 「this」を渡さない。 これは、Jamのうまいところだと思うのですが、ミックスイン内部から、メソッドまたはコンストラクターの引数として「this」を渡すことが禁止されています!Jamのこのような特質はタイプ・システムの健全性を保護するのに必要です。これがないと、Jamミックスイン型があらゆるインスタンス化に有効であることを保障する方法がありません。しかしながら、これは、ミックスインになる資格のあるクラス・セットを制限してしまうという点で、非常に残念な制限ではあります。
Java言語に変換するために、Jamミックスイン型はインターフェースとして表され、インスタンス化(すべてコンパイル時に静的に解決されます)によって実装されます。ミックスインに導入されたフィールドを処理するために、ゲッター/セッターメソッドのM_$get$_f とM_$set$_f が、インターフェースに導入されます。次に、f が、すべてのインスタンス化したクラスのフィールドとして宣言され、メソッドが適切に実装されます (また、外部コードからの静的型M の式のすべてのフィールド・アクセスが、ゲッター/セッターへの呼び出しに変換されます)。ミックスインの静的フィールドは、ミックスインのインスタンス化したクラスの間で共有されず、したがって、単に各インスタンス化に別々に挿入されます。
ミックスインの各インスタンス化は、個別のJavaクラスにコンパイルされます。バイトコードの共有はコピー間で発生しません。また、ミックスインの親に対してもインターフェースが作成されます。この親インターフェースは、(親クラスのインスタンス化によってではなく) ミックスインのインターフェースによって拡張されます。
ミックスインは、落とし穴がまったくない、言語の多重継承の力を回復する方法として、プログラマーに一般的に使用されています。しかし、(GUI要素やRMIプロキシー・クラスのように) 親クラスを直接にテストすることが困難であるような場合は特に、ミックスインが、既存のクラスの新しい拡張をテストする強力な方法を提供するという重要な点にも注目すべきです。
実際に、Jiazziが、インポートするパッケージから独立してパッケージをテストする方法を提供するように、Jam (または、Java言語の他のミックスイン・ベースの拡張) によって、親が同じパッケージ内に存在している場合でも、その親から独立してクラスをテストすることができます。リスト3の例に従うと、スーパー・メソッドのすべての呼び出しを単に記録する、親クラスのRecorderによって、ミックスインをインスタンス化できるようになります。
リスト2. インスタンス化から独立してミックスインをテストする
class TestLog {
private StringBuffer recording = new StringBuffer("");
public void record(String message) {
recording.append(message);
}
public String toString() {
return recording.toString();
}
}
class WidgetRecorder {
public TestLog testLog;
public void setVisible(boolean value) {
testLog.record("setVisible(" + value + "); ");
}
}
class ScrollableWidgetRecorder = Scrollable extends WidgetRecorder {
public TestScrollable() {
this.maxScrollSize = 10;
}
}
|
こうして、次のように、呼び出しの期待されるシーケンスに対してこのログをチェックできるようになります。
リスト3. ミックスインのためのJUnit TestCases
import junit.framework.*;
public class ScrollableTest extends TestCase {
public ScrollableTest(String name) {super(name);}
public void testSetVisible() {
ScrollableWidgetRecorder test = new ScrollableWidgetRecorder();
test.initialize();
assertEquals("Scrollable initialization should've called setVisible(true)",
"setVisible(true); ",
test.testLog.toString())
}
...
}
|
このように、親クラスのロケーションに関係なく、それだけではテストが困難なクラスをテスト用に拡張できるようになります。テストが困難である中心的な機能性を親クラスの小さなセットに分離できるようになり、また、それに依存する機能性が、十分にテストされたミックスイン・クラスに依存できるようになります。
最後に、少なくとも簡単に、Javaに汎用型を追加するJSR-14提案にミックスインがどのように関連するかについて説明しなければ、Javaプログラミングのミックスインの説明としては怠慢というものでしょう。
汎用型を使用すると、参照する型によってクラスをパラメーター化できるようになるため、Java言語の汎用型に対する本当に優れたサポートは、必然的に、ミックスインの形式をサポートするものでなければなりません。そうすれば、クラスを定義して、型変数を拡張できるようになります。
しかし残念ながら、汎用型は静的コンパイルの間に消去されるので、SunのJSR-14プロトタイプ・コンパイラーによって使用されるアプローチは、そのような(実行系によって直接サポートされる)ファースト・クラスの言語要素を導入する拡張が不可能になっており、汎用型の情報は、実行時には存在さえしません。ミックスインの場合は、ミックスインの親クラスが、型変数の共通親クラスにまで消去されてしまいますが、これは明らかに望ましくありません。
対照的に、NextGen方式による汎用型 (2002年12月にRice JavaPLTからベータ・リリースの予定) は、汎用型の情報を実行時に利用できるようにします。したがって、それは、ミックスインなどの優れた汎用型をサポートするよう拡張できるでしょう。実際、最初のベータ・リリース後すぐの拡張は、正にそのような機能性が含まれるでしょう。拡張言語の設計については、参考文献を参照してください。
この記事と前回の記事で説明したように、今日存在するJava言語は、言語設計の最終段階にあるものではありません。特に、テスト・ファースト・プログラミングのスタイルを採用する場合はなおさらです。より迅速に、また、より完全にプログラムをテストすることができる、強力で自然な言語拡張が数多くあります。
しかし、前回と今回の2つの記事で、Java言語が可能とする非常に大きな柔軟性と拡張性も示せたのであればうれしいです。このような拡張性は、言語とJVM設計の安全と移植性に関する直接的な結果です。元の設計者の将来への見通しのおかげで、Java言語は、これからも長い間、非常に強力で適切な言語として、ますます複雑なアプリケーションを作成していくプログラマーを支えつづけるでしょう。
- ミックスインをサポートするJava言語の拡張であるJam言語について簡単にご覧になり、ダウンロードしてください。
- Rice JavaPLTのこの拡張された要約 (PDFファイル) は、Javaが、既存のコンパイルされたバイナリーと完全に互換性のある方法で本当に優れた汎用型によってどのように拡張できるかについて説明しています。
- 以前のJavaコードの診断の記事「正しいメソッド呼び出しのためのRecorderによるテスト」 (developerWorks、2001年6月) をご覧になり、強化単体テストのためのRecordersの使用について学んでください。
- 11月のJavaコードの診断の記事「パッケージの依存関係の分離」は、Jiazziと、テストをより容易に行うためのコンポーネント・ベースのコードの作成について取り上げています。
- 汎用型の詳細については、著者のJavaWorld の記事「Behold the power of parametric polymorphism」 (2000年2月) をご覧ください。
- 「Javaコードの診断: 「テスト可能なアプリケーションの設計」 (developerWorks、2001年9月) で取り上げた、コード設計ベース作成のための7つの原理について、テストを念頭に考えてみてください。
- バグ・パターンからテスト容易性、設計戦略などの技術的な知識については、『Javaコードの診断』 の記事をご覧ください。
- バグ・パターン: Javaプログラムで頻発しがちなバグの分析と修正
- 「宙ぶらりん複合型」バグ・パターン: ヌル・ポインター例外の最もよくある原因を鎮圧する
- 「ヌル・フラグ」バグ・パターン: 例外状況を表すフラグとしてヌル・ポインターを使うことを避ける
- 「二段たどり」バグ・パターン: 再帰的なクラス・キャストという概念上のエラーを最初から克服する
- うそつきビューのバグ・パターン: GUIの最良の友になってうそつきビューを暴き出しましょう
- 破壊工作データのバグ・パターン: 隠れたデータ爆弾が奇妙なクラッシュの原因かもしれません
- 破綻したディスパッチのバグ・パターン: 引き数のアップ・キャストによって、不正確なメソッドの呼び出しを修正する
- Javaコードのパフォーマンスを向上させる: 末尾再帰変換はアプリケーションの速度を向上させる可能性はあるが、すべてのJVMで可能な操作ではない
- 正しいメソッド呼び出しのためのRecorderによるテスト: メソッドの呼び出しを順序正しく行うために、ユニット・テストのためのRecorderを記述する
- 型詐欺師のバグ・パターン: タグを使用したオブジェクトの型の区別は、ラベルの貼り違えにつながる可能性がある
- クリーンアップ・コード散在バグ・パターン: リソースの獲得および解放を同時に実行する
- 虚偽の実装というバグ・パターン: 第1回: 前提とした不変条件がインターフェースの破壊を招くこともある
- 虚偽の実装というバグ・パターン: 第2回: 表明とユニット・テスト - バグを除去するための実行可能なドキュメンテーション -
- 「みなし子スレッド」バグ・パターン: マスター・スレッドが自滅し、その他のスレッドが生き残っていると、どうなるか?
- 「テスト可能な」アプリケーションの設計: 以下に示す7つの原則は、テストを念頭においてコード設計を行う際のもとになるものです
- 拡張可能アプリケーションの設計 第1回: ブラック・ボックス、オープン・ボックス、またはガラス・ボックス: どんな場合にどれがふさわしいか?
- 拡張可能アプリケーションの設計 第2回: ガラス・ボックスはどんな時、どこで、どのように最もよく機能するかを調べる
- 拡張可能アプリケーションの設計 第3回: ブラック・ボックスはどんなとき、どこで、どのように最もよく機能するかを調べる
- 拡張可能アプリケーションの設計 第4回: S式がどのように軽量のブラック・ボックス拡張性を実現するか
- 深さ優先visitorと、破綻したディスパッチ: このVisitorパターンの変形を使えば、コードをより簡潔にできます
- 仕様という綱渡り: 明確に定義された仕様が、ソフトウェア・システムにとってなぜ重要か
- replによる対話式評価: ソフトウェアを効率的かつ対話式に診断するためのテクニックとツール
- 「付け足し初期化コード」バグ・パターン: 引数の足りないコンストラクターを避ければ、このバグを撃退できる
- プラットフォーム依存を引き起こす"犯人": プラットフォーム依存のバグ・パターンにスポットライトを当てる
- 静的型の場合: 好きでも嫌いでも、静的型チェックでコードをもっと堅固にできる
- Javaプログラミングにおけるアサーションと時相論理: テストを補足するものとしてアサーションに時相論理を取り入れる
- 時相論理をバグ・パターンに適用する: 時相論理アサーションによる、一般的なバグの防止
- 単体テストと自動コード分析の連携: テストを工夫して、ツールによるコード分析を手助けする
- パッケージの依存関係の分離: コンポーネント・ベース・プログラミングにより、テスト可能なコードをより柔軟に接続
- Java言語に汎用型を追加する議論に関しては、Java Community Processの提案であるJSR-14 をお読みください。
- IBM Researchの「Automatic Code Generation from Design Patterns」 (PDFファイル) は、デザイン・パターンの実装を自動化するツールのアーキテクチャーと実装について説明しています。
-
developerWorks Java technologyゾーンで他のJava関連記事やチュートリアルをご覧ください。
Eric Allen氏は、コーネル大学でコンピューター・サイエンスと数学の学士号を取得しています。現在は、ライス大学の博士課程の大学院生としてJavaプログラミング言語チームに加わっています。学位を終了するためにライス大学に戻るまでは、Cycorp, IncでJavaソフトウェア開発主任として勤務していました。彼は、JavaWorldで「Java Beginner」ディスカッション・フォーラムの司会者も務めています。主な研究対象は、Java言語のセマンティック・モデルと静的分析ツールの開発であり、いずれもソース・レベルとバイトコード・レベルで研究しています。Ericは、NextGenというプログラミング言語 (汎用ランタイム型によるJava言語の拡張版) のためのライスのコンパイラーの開発にも携わってきました。連絡先は、eallen@cs.rice.edu です。