MJ は広い意味でのアスペクト指向言語です。 複数のクラスを横断するコードを、別のモジュールに分離して記述できます。
しかし、 MJ は代表的なアスペクト指向言語であるAspectJとはかなり考え方が異なります。 AspectJ はデバッグ、同期、性能向上など、プログラム本来の機能とは異なるアスペクトを分離することに主眼を置いていますが、MJ はそれらを重視していません。
MJ が解決しようとする問題は、どちらかといえば、Hyper/Jに近いものです。 従来の手続き指向言語あるいはオブジェクト指向言語は、1種類のモジュールの切り分け方(手続きまたはクラス)をプログラマーに強制するものですが、Hyper/J や MJ は自由な切り分け方を可能にするシステムです。
また、MJ は、言語仕様のシンプルさと、モジュール結合時の安全性を第一に考えて設計しています。この点は、他の AOP システムと大きく異なる点です。
オブジェクト指向言語でなくても、手続き型言語、論理型言語、ハードウエア記述言語などにも適用できると思います。 多くのプログラミング言語は、なんらかの記述単位に「名前」を付けます。 MJ のモジュール機構のアイデアは、オリジナルのプログラムの「名前」に対応付けられたコードに、なんらかの形で差分を追加する、というシンプルなものです。 したがって、多くのプログラミング言語に適用することができます。
例えば、差分ベースモジュールを emacs-lisp に適用したものが、 dm-elisp です。
MJ のモジュール機構を適用できない言語もあります。 例えば項書換え系は、記述単位に名前を付けないので、適用できません。
多重継承の問題のうち、名前の衝突に関しては、フィールド・メソッドの完全限定名の導入によって完全に解決しています。
もう一つの問題、意味的衝突については、 Design by Contract および behavioral subtyping の考え方を差分ベースモジュールに応用することで解決できます。(詳しくは この論文 を参照してください。)
しかし、 tree 構造の方が一般のグラフ構造よりも人間にとって扱いやすいことは確かです。 MJ におけるプログラミングでも、複雑なモジュールの多重継承を推奨しているわけではありません。 モジュールの継承グラフは、できるだけシンプルな形になるようにプログラミングした方がよいと思います。
理想的なモジュールの継承グラフは、継承の深さが浅く、横に広い形をした継承グラフです。 継承グラフがそのような形をしていれば、プログラムの保守が容易になります。
Java には class という構文とは別に package がありますが、役割分担が完全にできていません。 package も class も、ともに名前空間の役割を果たしています。 MJ は、クラスの役割とモジュールの役割を整理して分けることで、言語仕様を Java よりもシンプルにしています。
XP が言っているように、最初から拡張性・再利用を考えたモジュールの切り分けに時間を割くことはしない方がよいと思います。
この言語に限らず、拡張性・再利用性を高くするには労力が必要です。 先々のことを考えて拡張性・再利用性の高いモジュール分けを行っても、実際に拡張や再利用が行われなければ、その苦労は報われません。 プログラマーは、そのトレードオフを考えて時間配分を行わなければなりません。
OOP のいいところは、記述対象をクラスにモデル化して記述さえすれば、それだけで、自動的にある程度拡張性・再利用性の高いプログラムができるという点です。 つまり、「クラス=モジュール」は、近似的には成り立つ場合が多いので、クラスの設計さえすれば、それだけで、そこそこよいモジュール分けができてしまうわけです。 これもオブジェクト指向パラダイムのメリットの1つだと思います。
このメリットは、 MJ にも引き継がれています。 完璧なモジュール分けに頭を悩ます時間がなければ、従来言語と同様、1クラス1モジュールで書いておけばいいのです。 開発・保守が進んで行くうちに問題ができたら初めて、モジュールの分け方を見直せばよいのです。
モジュールの分け方を変更する作業はリファクタリングと呼ばれています。 将来的には MJ でのリファクタリングをサポートするツールができれば便利だと思います。
そのとおりです。 オブジェクト指向設計とリファクタリングは、 MJ でのプログラミングでも重要です。
同様の批判が、従来のオブジェクト指向言語にも言われていたことを思い出して下さい。 「1つのクラスの定義が、スーパークラスとサブクラスに分離されているため、プログラムが読みにくい」という批判です。 この批判に対する答えは2つあります。
1つは Design by Contract の実践です。 個々のクラスやメソッドの外部仕様を明確にしておけば、そのクラスやメソッドの内部実装のコードをいちいち見に行く必要がありません。 つまり内部実装が複数の場所に分散していても問題にはなりません。 むしろ、情報隠蔽によって不必要なクラスやメソッドの名前が見えなくなることにより、プログラムの可読性は増すことになります。 この答えは、差分ベースモジュールにもそのまま当てはまります。
もう1つは、 UML などを使った外部ドキュメントです。 オブジェクト指向言語が扱う問題は従来の言語が扱う問題よりもはるかに複雑で、もはやソースコードを読むだけで全体像を理解することは不可能です。 そこで、 UML などを使った理解が必要になります。 この答えも、差分ベースモジュールにそのまま当てはまります。 UML のクラス図を差分ベースモジュール用に拡張した、レイヤードクラス図を使えば、モジュールがプログラムを拡張する様子を視覚的に分かりやすく表現することができます。
クラスの多重継承はオブジェクト指向言語に必要な機能だと思います。 しかし、 MJ が JavaVM の上で実行される言語である限りは、クラスの多重継承をサポートする予定はありません。 現在の JavaVM 上でクラスの多重継承を実装すると、どうしても実行速度が遅くなります。
Szyperski, C.A.: "Import is Not Inheritance, Why We Need Both: Modules and Classes", In Proc. of ECOOP'92.
という論文では、クラスの継承をライブラリの import の変わりに使うべきではないと主張しています。
しかし、本来は異なる概念を1つの言語機構で実現することは、プログラミング言語を設計する上でよく行われます。 例えば、 Java では class を情報隠蔽の単位、型定義の機構、排他制御の単位、メモリの動的割当の単位などに用いています。 このように1つの言語機構に多くの機能を unify することで、言語の多機能さと簡潔さという矛盾する要求を同時に満たすことが可能となります。
MJ は、クラスから名前空間の機能を分離する代わりに、名前空間と差分プログラミングの機構を unify しました。 この試みは成功しているようです。
import と inheritance は、もともと機能的に良く似ています。 Eiffel 言語の文化では、継承機構を積極的に「ライブラリの取り込み機構」として利用しています。 例えば、クラスの中から標準入出力のライブラリを使う時には、その機能を提供するクラスを継承します。 (ただし、 Eiffel ではライブラリのインターフェースは、暗黙のうちに継承されることはありません。)
「sandbox の外側」、つまり「安全でないコードを動的にロードして実行するアプリケーション」の記述には、現在の MJ は使えません。 セキュリティの観点からは、 MJ で定義されたクラス名・フィールド名・メソッド名はすべて public であると思ってください。
名前の衝突の問題は、メソッド完全限定名の導入により解決できています。 しかし、メソッド完全限定名だけでは解決できない種類の衝突があります。 例えば、 module m1 で定義されている abstract method foo を、m1 の sub-module である m2, m3 でそれぞれ実装していて、m2, m3 を同時に継承する sub-module m4 があるという状況を考えます。 このとき、 m4 から見ると、m2 か m3 のいずれかの foo の実装が、 override によって消えてしまう、という問題があります。
もし m4 が、 m2 と m3 の両方の foo の実装を生かしたいと思ったら、Eiffel 言語のようなメソッドの rename の機構が必要になるでしょう。 この機構については、将来、導入したいと思っています。
現在のようなシンタックスになっているのは、Java 言語の「パッケージ名の区切りにもメンバーの参照にも "." を使っている」という問題に起因します。 この問題が解決できれば、もっとまともなシンタックスにできると思います。 ( XML のように名前空間の別名を prefix に使うようにすれば、シンタックスがすっきりするでしょう。これは将来のバージョンで実装しようと思います。)
MJ では依存する名前空間を最小にできるので、名前の衝突が起きる確率は従来のオブジェクト指向言語よりもかなり低くなると予想しています。 実際のプログラミングの際に、プログラマーが FQN を指定しなければならなくなることは、あまりないと思います。
MJ において「 m1 を m2 が extends する」ということは、Java の nested class で表現すれば「クラス m1 の内部に m2 が定義されている」に相当する、と理解してください。 内側からは、外側で定義されているすべての名前にアクセスできます。
MJ では、すべての名前のアクセス修飾子は "protected" である、と見ることもできます。 普通の Java で super class の protected field が subclass から見えるのと同様、MJ では super-module の中のすべての名前が、 sub-module から見えます。 そのかわり、 m1 を extends するすべてのモジュールのプログラマーは、m1 の利用者ではなく、実装者の立場になることを意識してプログラムしなければなりません。
現在の言語仕様にはありませんが、将来的には "final module" のような機構を入れるかも知れません。 これは Java の final class のように、sub-module の定義を許さないモジュールです。 final module の内部で定義される名前はすべて "private" である、と言えます。
final module 機構は、foolproof のための機構です。 あるプログラマーが、クラスの外部インターフェースだけでなく内部実装にも依存したプログラムを書くと、ペナルティを受けるのは明らかにそのプログラマーです。 将来そのクラスの内部実装が変更になった時に、そのプログラマーは自分が書いたプログラムを書き直さなければなりません。
将来的には Eiffel のような assertion 機構を MJ に入れるつもりですが、それが実現されれば、実装に依存するプログラマーは、さらにペナルティを受けることになります。 クラスの外部インターフェースのみを利用するプログラマーは事前条件だけを満たせば良いのですが、実装に依存するプログラマーは、不変条件と事後条件も満たすようにプログラムしなければなりません。 つまり、より制約の強いプログラミングを強いられることになります。
CLOS などの言語では、 c extends a, b と宣言すると、a が b よりも優先されると解釈されます。 これを local precedence order と呼びます。 これらの言語では linearize の際には、local precedence order も保存されます。
MJ では、エンドユーザがモジュールを組み合わせるときに、できるだけリンク時エラーが起きないようにしたいと考え、現在は local precedence order を言語仕様に入れていません。 次のような例を考えましょう。
module c extends a, b {...} module d extends b, a {...}
エンドユーザが c と d のモジュールを組み合わせる場合、 local precedence order を保存するような linearize は不可能です。 CLOS などでは、このような場合はエラーになります。
リンク時エラーの原因になるというデメリットを上回るメリットがあれば、将来は、 local precedence order を言語仕様に入れるかもしれません。
実装の継承とインターフェースの継承が別の概念であることは確かですが、言語機構として別のものを用意すべきだという意見には賛成できません。 subtype は super type と振る舞いを共有する場合が多いので、プログラマーの便宜を図るためには、 subtype を作ったら自動的に実装も継承される方がよいと思います。
また、2つの機構を用意するよりも1つの機構(クラスの継承機構)ですませれば、言語仕様が単純になるという大きな利点もあります。
確かに、「実装は継承したいが、 subtype にはなりたくない」「subtype を作りたいが実装は継承したくない」という場合もあると思います。 前者の場合については、C++ の private inheritance のようなものがあるとよいと思います。 後者の場合については、あらかじめフレームワークの実装者が仕様モジュールと実装モジュールを分けて定義しておくのが、そもそも望ましいと思います。 もしフレームワークがそういう風にかかれていなかったとしても、すべてのメソッドを override することで、 super class の実装を無視することができます。
これは、モジュール間のどういう関係に着目するかで、答えは違って来ます。
Java の nested class のイメージで言うなら、 sub-module は super-module の内側にあります。 sub-module からは super-module のすべての名前にアクセスできますが、これは nested class において、 inner class が outer class のすべての名前にアクセスできることに相当します。 一般に、外側のモジュールは抽象的な外部インターフェースであり、内部のモジュールは、より具体的な実装です。 つまり、「内部を隠蔽するという関係」で言えば、 super-module は sub-module を隠蔽しています。
これは実世界におけるモジュールの入れ子関係の対比でも理解できます。 例えば、パソコンのケースの内部にはハードディスク、マザーボードなどのモジュールがあり、それぞれはさらに細かいモジュールから構成されています。 外部のモジュールほど、内部を隠蔽して、抽象度の高い機能を、外部のユーザに提供します。 このように、 super-module と sub-module は、それぞれ実世界における外側のモジュールと内側のモジュールに対応します。
ところが、「随伴関係」と「依存関係」に着目すると、MJ のモジュールと実世界との対応は違ってきます。
実世界のモジュールでは、外側のモジュールの場所を移動すれば、同時に内部のモジュールも移動します。 つまり、内部のモジュールは外部のモジュールに随伴します。 一方 MJ では、 リンク時に sub-module を指定すると、super-module も自動的にリンクされます。 つまり、 super-module が sub-module に随伴するのです。 これはモジュールの入れ子関係とは逆転しています。
最後に「依存関係」という観点で見ると、どうでしょうか。 実世界のモジュールで、例えばマザーボードというモジュールの中に、CPU というモジュールがあります。 マザーボードとCPU は、どちらがどちらに依存しているのでしょうか? (ただし、マザーボードも、CPU も、異なるメーカーから互換性のある複数の製品が利用可能な場合を考えます。) MJ 的な理解では、特定のマザーボードの1製品と、特定のCPUの1製品は、お互いに全く依存していません。 それぞれを接続可能なのは、「CPU のソケットの規格」を両方が正しく実装しているからです。 MJ の構文を使うなら、こういうことになります。
module cpuSpec {...} module motherBoardSpec {...} module cpuImplementation extends cpuSpec {...} module motherBoardImplementation extends cpuSpec, motherBoardSpec {...}
この「CPU のソケットの規格」は、 MJ では1つの仕様モジュールとして表現します。 ところが実世界では規格は紙の上の存在であり、実体を持ったモジュールとしては通常存在しません。 したがって、 MJ での依存関係は、実世界のモジュールどうしの関係とは対応しません。
まとめるとこうなります。
隠蔽関係:super-module が外側のモジュール、 sub-module が内側のモジュール
随伴関係:super-module が内側のモジュール、 sub-module が外側のモジュール
依存関係:super-module に sub-module が依存。実世界には対応しない。
genericsのような、モジュールに型や静的な値を引数として与えて別なモジュールを生成する機能は、現在のMJにはありません。
現在の MJ ではできません。 将来は、クラス階層のコピーを作る機構を導入する予定です。
現在の MJ では、差分を追加する対象となるクラスのクラス名が固定であるため、そのようなことはできません。 将来は、差分の追加対象のクラス名を変更するための機構を導入する予定です。
現在の MJ ではできません。クラスの dynamic loading ならば、 Java 同様にできます。
クラス継承は is-a 関係がある場合にのみ使うべきであり、差分プログラミングの道具として安易に使うべきではありません。 この点では MJ は C++ や Java と同じです。 ( Smalltalk は静的型チェックがないせいか、クラス継承を差分プログラミングの道具として使う文化があるようですが。)
今の MJ にはありませんが、必要だと思っています。 次のプログラムは before の代わりになると思う人もいるかもしれません。
module m1 extends m0 { class C { void foo() { m1BeforeFoo(); original(); } } }
m1 を書いたプログラマーの意図は、「foo の本体を実行する前に、 doBeforeFoo()を実行したい」ということでしょう。これは実現可能です。 しかし、これだけでは不十分な場合があります。 別のプログラマーが、「『自分の super module が追加した foo 本体直前の処理』と『本来の foo 本体』の間に、処理を追加したい。」と思ったとします。 (実際にこのような要求は起こります。) このプログラマーが次のように記述してもうまくいきません。
module m2 extends m1 { class C { void foo() { m2BeforeFoo(); original(); } } }
これでは処理 m2BeforeFoo() は、処理 m1BeforeFoo() の前に実行されてしまうからです。
この問題は、あらかじめフレームワーク側が、 before hook を用意しておくことで解決可能です。
module m0 { define class C { define void beforeFoo(){} define void foo() { beforeFoo(); defaultFooBehavior(); } } }
しかし、フレームワーク構築時点では before, after, around のための hook があとで必要になるかどうか予測することはできない場合があります。 また、ひょっとして必要になるかもしれないから言って、すべてのメソッドに before, after, around hook を入れておくことは、現実的には不可能です。 このような理由から、 before, after, around hook の問題に対処するなんらかの言語機構が必要だと考えています。
確かに言語研究者は多くの場合、既存の言語への上位互換の形で新しい言語を提案します。 新しい言語を Java の上位互換にすれば、 Java からの移行も容易ですし、記述力に関しては少なくとも Java より悪くなることがないことが保証されるわけです。
しかし、 MJ のモジュール機構のアイデアを、 Java のモジュール機構(package, public/protected/private, nested class)と整合性をたもったまま取り込むのは極めて困難です。 もし、そのような言語を実際に設計したとしても、言語仕様が複雑になり、実装も非常に難しく、実用的言語にすることは不可能だと思います。
sub-module 単独では機能は持ちませんが、super-module と合わせて考えれば、機能を持ったクラスの集合を提供します。
これは OOP の subclass と同じことです。 subclass のコードだけでは意味を持ちませんが、superclass のコードと合わせて初めて、意味のあるクラス定義になります。
Last updated:
Nov 26, 2004
|