[ Eiffel Liberty | GUERL | sOOap ]
by
Alan A. Snyder
and
Brian N. Vetter
7 - Eiffel の利用: 例題とチュートリアル 7.0 概要 7.1 クラス VEHICLE 7.2 契約プログラミング 7.2.1 事前条件 7.2.2 事後条件 7.2.2.1 old 式 7.2.3 不変条件 7.2.3.1 ループ再訪 - 変化と不変 7.2.4 効率に関する注意事項 7.3 新しい VEHICLE クラス 7.4 例外 7.4.1 例外処理 - Rescue 句 7.4.2 Retry コマンド 7.5 陸上乗り物 と 水上乗り物 7.6 反復された継承 7.7 水陸両用乗り物
これまでの章は、特定の場合のいくつかの事例で、Eiffel の使い方を示してきました。この章は、私たちを、関連し合ったクラスの小さなグループのソフトウェア ライフサイクルの選択した段階を通じて導きます。VEHICLE(乗り物)と呼ばれるクラスとその子孫のいくつかが、私たちの企ての焦点になるでしょう。
私たちは、クラスの小さな階層を生成するために、私たちがこれまでの章で学んできた原則を適用することになります。また、私たちは、私たちが私たちのシステムを構築しているときに、これまでの章で言及されていない素材を扱うでしょう。記述すべきクラスの階層は、下にあります。
![]()
この節では、私たちは、VEHICLE と呼ぶクラスのための要件を、非形式的に記述していきます。仕様は、下に与えられており、読者のために出発点としての意味があるだけです。決して、技術的な仕様ではありません。
- このクラスは、各実例に唯一ではなく識別可能であることを可能にするファシリティを提供しなければなりません。
- 属性:
- Color(色)
- Number of passengers(乗員の数)
- ルーチン:
- 上記の属性を設定したり検索したりするルーチン群
- 同じく、構築子<constructor>ルーチンを必要とします。これは、その乗り物が引数として収容してもよい、乗員の有効な数を受け入れなければなりません。
上記の情報に基づくと、この VEHICLE というクラスのいくつかの実装は、異なる個々の解釈から明白であるかもしれません。ここに、私たちの実装があります。
class VEHICLE creation start; feature {NONE} color, num_passengers : INTEGER -- NONE へ公開した「私的」データ feature Red, Blue, Green : INTEGER is unique; -- すべてのクライアントへ利用可能な定数 start (new_num_passengers : INTEGER) is require new_num_passengers >= 1; do color := Red; -- 既定値 num_passengers := new_num_passengers; -- 既定値; io.put_string ( "Hi!. I'm a new VEHICLE" ); io.new_line; ensure color = Red; num_passengers = new_num_passengers; end; -- start set_color( new_color : INTEGER ) is do color := new_color; end -- set_color set_horse_power( new_power : INTEGER ) is do horse_power := new_power; end -- set_horse_power get_color : INTEGER is do Result := color; end -- get_color get_horse_power : INTEGER is do Result := horse_power; end -- get_horse_power get_id : INTEGER is do Result := object_id; end -- get_id end --VEHICLEあなたの頭に上がってくるかもしれない最初の質問は、多分、get_id での object_id 属性に関連しています。それは、何処からやって来たのでしょう? その答えは、クラス ANY から継承した、です。すべての利用者定義のクラス群は ANY から暗黙的に継承する、ということを思い出していただきたい。この項目<field>は、同じクラスのオブジェクトであっても、一意なものとしてオブジェクトを識別するという目的のために、明確に設計します。この属性は、実際、どんな所与の時点でも、そのシステム内のそれぞれのオブジェクトについて、一意です。
いま、私たちは、前に与えられた VEHICLE クラスの仕様を完全に満たしました。私たちは、要求された属性のすべてを、同じく、これらの属性を変更し試験するルーチンを持っています。
これらのルーチンと属性は実際に与えられた要件を完全に満たしているけれども、そのクラスはまったく安全ではありません。つまり、この時点でのクライアントは、意味の無い引数を、私たちのクラスからの結果無しにこのルーチンのすべてへ渡すことを許しているのです。私たちは、私たちのクラスを使用するどんなクライアントも、それと干渉することを許さないが、その特徴のすべてを完全に利用できるようにすることを保証するために、このクラス上に置く制限を必要としています。
Eiffel は、契約プログラミングと呼ばれるプログラミングのタイプを体現します。契約プログラミングは、クライアントとサプライヤに、それらが一致しなければならないある特定の要件を完全に認識し理解することを許す前提を提供し、それらが資格を与えられるということを保証します。これらの条件は、表明を通して、あるいは、もしクラスが他のクラスと相互作用したいのならばそのクラスが一致しなければならない条件を通して強制されます。振る舞いに関するこれらの規則は、事前条件、事後条件、そして不変条件で明白にします。
事前条件は、クラスのクライアント上に置く要件です。事前条件自体は、サプライア側のクラスによって規定し、どんなクライアントも一致しなければなりません。一つの例は、次のように、スタックのためのポップ操作が空でないスタックを要求するであろう、ということであったかもしれません。
class STACK [T] feature is_empty : BOOLEAN; pop : T is require non_empty_stack : not is_empty; do ... end --pop end --STACK複写しているルーチンは、複写されるべきオブジェクトが Void でない、ということを要求したかもしれません。
class ANY ... feature copy( other : like Current ) is require non_void_other : not other = Void; do ... end -- copy end -- ANY事前条件のための構文は、最初は、わずかに混乱しているように思えるかもしれません。基本的な構文は、キーワード require に一つ以上の表明を続けます。表明は、可能な識別子の後に、コロン":"を付け、それから真理値式を続けます。もしもっと多くの表明が必要ならば、セミコロン";"が必要です。
... feature remove_leaf is require non_empty_tree : not is_empty; non_void_tree : not tree = Void; do ... end --remove_leaf構文的には、これらの二つの事前条件は両方とも、表明を一致させるために、一致しなければなりません。それらの両方は、次のように、一致しなければならないので、私たちのルーチン remove_leaf の本体の中にある、これらの二つの式と AND することと等価です。
if (not is_empty) AND (not tree = Void) then precondition_met ... -- ルーチンの残り else precondition_not_met end;この違いは、もし私たちが上記のように remove_leaf の本体で事前条件をコード化すべきであったならば、私たちはまた、precondition_not_met を処理することに責任があったであろう、ということです。つまり、私たちは、私たちの事前条件が一致されなかった remove_leaf の呼び出し者を変更するための機構を用意しなければならなかったであろうし、そして、ふさわしい動作が取られなければなりません。幸いにも、Eiffel は、私たちのために、その能力を提供しています。
事前条件あるいはその問題のためのどんな表明でも違反されるとき、表明を宣言しているクラスにおいて、例外が生成されます。私たちは、例外を如何に処理するかについて、後の節で議論することにします。
remove_leaf の呼び出し者が事前条件に合っていた、と仮定してださい。いま、呼び出し者が、そのルーチンが暗に意味している葉の取り除きよりも少なく、何も期待しないであろう、と仮定することは安全です。事後条件がこれを保証します。より一般的には、事後条件は、事前条件(もしあれば)がひとたび合ったならば、そのルーチンはそのクライアントが期待する要件を完全に満たすことを約束するであろう、ということを保証します。私たちは、いま、次のように、remove_leaf を書くこともできます。
feature remove_leaf is local original_size : INTEGER; require non_empty_tree : not is_empty; non_void_tree : not tree = Void; do original_size := size; ... ensure one_less_element : size = original_size - 1; end -- remove_leaforiginal_size がルーチンへ入ったときの木のサイズを格納し、size がルーチンの終わりの時点での要素の数であると仮定して、事後条件は、正確に一つの要素が木から取り除かれた、ということを述べています。木の新しいサイズは、正確に、remove_leaf が記すように、要素が取り除かれたということを暗に意味して、ルーチンに入る前にそうであったものより、一つだけ少ないです。
これは、過去のものとなった式を意味しているのではありません。old 式は、次のように、キーワード old に続けて、識別子や式を置くことによって、明示します。
old size; old arg1 + size; old identification;old 式は、ルーチンの事後条件に現れるだけかもしれません。それは、ルーチンへ入った時点での、その識別子の値を明示します。上記で、old size は、私たちがこの式が現れる事後条件を持つルーチンに入ったとき、size の値へ参照します。この事の有用性は、次のように、remove_leaf ルーチンをわずかに変更することによって示すこともできます。
class SOME_CLASS feature Remove_leaf is require non_empty_tree : not is_empty; non_void_tree : not tree = Void; do ... ensure one_less_element : size = old size - 1; end --remove_leaf end -- SOME_CLASSこのことは、ルーチンの入り口での size の値を格納する一時性の明示的な目的のために、その他の局所的変数、私たちが持ったような original_size、を宣言する必要性を取り除きます。このキーワードの追加の利益は、それが可読性を促進するという事実です。古い size を読む方が、正確に original_size が意味するものを判定することを監視し続けるよりも、簡単です。
計算科学として、私たちは、最初に、アルゴリズムの振る舞いを如何に規定するかを学びます。これらの機構の一つが、不変条件であり、あるいは、何があっても、つねに真を保持しなければならない条件です(実際に、不変条件は、ある特定のクリティカルな時点で違反されることを許していますが、それをリストアしません)。
Eiffel は、一連の不変条件を規定する能力を提供します。これは、invariant というキーワードの使用を通じて行います。構文的には、それは、クラスの宣言において、少なくとも制約として現れなければなりません。
class FIXED_DATA_STRUCTURE [T] feature ... invariant size >= 0; size <= 99; end -- FIXED_DATA_STRUCTUREこのことは、私たちに、システム実行中の所与の時点で、FIXED_DATA_STRUCTURE として宣言したオブジェクトの size という属性が包含的に 0 と 99 の間でなければならない、ということを形式的に規定することを許します。
もう一つの構文的な注意事項は、さらなる可読性のために、invariant 句が、その実際の BOOLEAN 式の前に記述的な識別子を供給してもよい、ということです。
class FIXED_DATA_STRUCTURE [T] feature ... invariant stack_not_empty: size >= 0; no_overflow: size <= 99; end -- FIXED_DATA_STRUCTURE
4.1節で導入されたループの契約には、不変条件<invariant>と変化条件<variant>という、二つの選択的な句があります。これらの句は、Eiffel の契約プログラミングの結合のためのサポートを提供します。
invariant 句は、ループ本体のそれぞれの反復に関して、この句の中にある BOOLEAN 式が真を保持しなければならない、ということを記します。これは、設計と同様に実装の両面から、有用です。設計の観点から、invariant は、ループの正確さ -- 全時点で真を保持しなければならない条件 -- を規定するのを助けます。実装の観点から、この式は、もし False へ評価されるようなことが起きるならば、プログラマに、Eiffel の(7.4.1節で議論した)例外処理機構を用いて、この違反<breach>を容易に処理することを可能にするでしょう。
invariant と variant の使用と、それらがどう働くのかを詳しく示すために、私たちは、出力がプリンタへページを書く、この単純なループを提示します。
from num_pages := pages_to_be_printed. until num_pages = 0; invariant printer.on_line; variant num_pages; loop ... printer.output_next; ... endこのループは、from キーワードで明示された初期化から始まります。ここで、num_pages は、印刷されるべきページの番号である num_pages_to_be_printed に設定されます。次に、until というキーワードで明示された停止条件がテストされます。もしそれが真(印刷されるべきページの番号がゼロである)へ評価されるならば、ループ本体は実行されません。停止条件は、ループ本体のそれぞれの反復子の前に評価されることになります。
ここで導入された新しい句は、invariant と variant です。invariant 句は、ループのそれぞれの反復の前に、BOOLEAN 式がここで真へ評価されなければならない、ということを記します。もし不変条件のどんなテストの最中でも、その式が偽へ評価されるならば、例外が上がり、後で記述されるように処理されるかもしれません。ループ本体の最初の反復の前に、不変条件は、上記の条件に対してテストされます。
一方の variant 句は、ちょっとばかり異なります。その囲われている式の型は、INTEGER です。その式は、ループ本体の最初の実行の前に一度だけ評価され、(最初の反復を含む)それぞれの反復の前にテストされます。上記のテキストで、変化条件は、印刷されるべきページの番号である num_pages です。もし、実例として、それが最初に 3 であると評価されるならば、次の反復の前に、自動的に 2、1、といった具合に減算されます。このことは、ループが、次の規則に従って、とにかく最終的に打ち切られることになる、ということを保証します。その規則とは、変化条件が負の値に達するならば、例外が発生しループが打ち切られることになる、ということです。
不変条件と変化条件は、ループの正確性の容易な仕様化(ソースコードにおいてではない!)を許す言語の機構です。このことの利益は、ループの構成要素が呼ばれるあらゆる場面についてだけ見られることができます。
この点で、Eiffel の表明がある特定の事情で良いというよりも多くの害を引き起こすかもしれない、ということを現実化し始めます。特に、システムのパフォーマンスは、グラフィック集約的<intensive>あるいはリアルタイムなアプリケーションにおいてしばしば主要な要因であり、表明の負荷に応じて吸収されるかもしれません。Eiffel 言語のための形式的なリファレンスである「Eiffel: The Language」において、実行時の表明の監視について、いくつかの利用者選択可能な可能性を記述しています。それらの一つは、どんな種類の表明の検査も無い、ということです。このことは、表明を監視されることになるクラスが一つも無い、ということを意味します。その表明は、事前条件、事後条件、そして不変条件を含みます。
ある C については、表明監視のある特定の水準を実施し、別のクラス D については、異なる監視を行う、あるいは一切監視しない、ということを定義することもできます。このことは、開発者に対して、有効にさせられた表明の監視を持つクラスを開発するように強制し、ひとたび、クラスが完全にテストされ、動作するのに適正な程度まで保証されれば、表明の監視は無効にしてもかまいません。
私たちが表明の役割を理解した今、私たちの VEHICLE クラスを、表明をサポートするように修正するときです。このことは、セキュリティを、危険な引数をルーチンへ渡すことを許さないことによって、型 VEHICLE のオブジェクトの周囲へしっかり結び付けることになります。
class VEHICLE creation start; feature {NONE} color, num_passengers : INTEGER -- NONE へ公開した「私的」なデータ feature Red, Blue, Green : INTEGER is unique; -- すべてのクライアントへ利用可能な定数 start (new_num_passengers : INTEGER) is require new_num_passengers >= 1; do color := Red; --既定値 num_passengers := new_num_passengers; --既定値; io.put_string ( "Hi!. I'm a new VEHICLE" ); io.new_line; ensure color = Red; num_passengers = new_num_passengers; end; -- start set_color( new_color : INTEGER ) is require new_color >= Red; new_color <= Green; do color := new_color; end -- set_color set_num_passengers( new_num_passengers : INTEGER ) is require new_num_passengers >= 0; do num_passengers := new_num_passengers; end -- set_num_passengers get_color is do Result := color; end -- get_color get_num_passengers is do Result := num_passengers; end -- get_num_passengers get_id is do Result := object_id; end -- get_id invariant last_attrib >= 0; end -- VEHICLE新しい VEHICLE クラスは、いまや、その元のバージョンよりも、まったく安全です。このことは、それぞれのルーチンが、すべての引数が適正な境界内にあるということが必須の事前条件に適合された、ということの理由です。実例として、VEHICLE オブジェクトの色を変更したいと願うどんなクライアントも、Red から Green の範囲内にある値を引数として渡さなければなりません。同じく、set_num_passengers ルーチンを経由して乗員の数での変更は、負の乗員の数を指定できません。私たちはまた、限界を new_num_passengers の引数へ指定することによって、乗員の再考値の数を設定しようと企てていない、ということを要求しました。この追加の制約は、もし読者がそうすることを願うのならば、加えられることもできます。私たちは、そうしないことを選んだのです。
私たちは、私たちのルーチンとクラスのなかの場所に、適した制約を持っています。次のことをよく考えてください。
class DRIVER feature car : VEHICLE; some_routine( some_arg : SOME_TYPE ) is do ... car.set_color( -2 ); -- 警告 - これは違法です!! ... end -- some_routine end -- DRIVER-2 という引数を伴って set_color へ呼び出しは、明らかに、set_color の事前条件の違反です。このことについて異論はありません。それから、その質問は、今何がおきているか?、です。
「もしルーチンがコンポーネントを実行し、そのコンポーネントが失敗<fail>するならば、このことは、ルーチンの実行を計画された通りに進めることから妨げるでしょう。そのような事象<event>は、例外<exception>と呼びます。」 [Meyer, 1992]。
Eiffel における例外は、本質的に、要件にあった例外が発生したことをルーチンに告げるメッセージです。例外メッセージ自体は、契約違反が発生したルーチン内で生成されます。Set_color の事前条件は、some_routine が負の引数を渡そうと企てたときに、違反されました。違反されたのが set_color の事前条件であったので、set_color は、最初に、例外メッセージを受け取ることになります。例外をどのように処理するかを決めるのは、最大でも set_color の実装までです。
ルーチンがどのように例外を処理するかということを Eiffel で指定することができるのは、そのルーチンが必ず例外メッセージを渡されるからです。このことは、キーワード rescue で始まる rescue 句を通じて行われます。reacue 句を持つルーチンは、例外を処理するために取るべき、ふさわしい動作を指定することを許されます。動作中の rescue 句の例は、いま、次のように与えられます。
feature some_feature( some_arg : SOME_TYPE ) is require some_arg >= 0; do some_attribute := some_arg; ensure some_attribute = some_arg; rescue error_code_attribute := 1; -- デバッグ用の内部のエラーフラグを上げる end -- some_featureルーチンの下部にある resque 句に注意してください。これは、ここだけに rescue 句が現れてもよい場所 -- ルーチンの最後の句として -- です。ここで、rescue 句が記しているのは、もしかしたらルーチンの本体が、
do some_attribute := some_arg;正しく実行を失敗するか、あるいは、もし事後条件が、
ensure some_attribute = some_arg;合わされるならば、ふさわしい動作が取られるはずである、ということです。内部のフラグ error_code_attribute であるその動作は、some_arg へ設定されます。このルーチンの著者はまた、適合していると思われるように、上記の例外を異なって処理することを選ぶこともできました。
一般的に、例外を処理するのに、良いも悪いもありません。rescue 句で取られた動作は、その例外がどれだけきびしいものか、そしてプログラマがどれだけ例外へ対処したいと思っているか、ということに基づくはずです。
例外を処理することに関係する、もう一つの可能な手段は、オブジェクトを安定した状態へ戻し、可能ならルーチン本体を再実行することを企てることです。私たちは、このことをたとえ -- 電話を掛けること -- で説明しましょう。もし電話をダイアルした後で、私たちが話中音を聞くならば、この仕事を完了させ、私たちの損失を吸収し、生きて続けようと企てることを、あきらめる必要はまったくありません。実際、数分待ってから後でもう一度掛け直すことぐらいのことは、おそらくもっと簡単でしょう。
Retry コマンドは、契約プログラミングを用いて、Eiffel の close 結合をサポートするのを助けます。もしルーチンがその事後条件を合わせることができないのならば、損失を受け付け、そのルーチンの呼び出し者に通知するだけでは、十分ではありません。事後条件を合わせるための企てにおいて、もう一度だけルーチンを実行することは、もっともらしいことでした。retry コマンドが実際にどのように使用されるかを見るために、私たちは、次の例を提示します。
class PHONE_LINE feature dial( number : STRING ) is do modem.call( number ) ensure modem.is_connected; rescue time_out; -- wait a random period of time retry; end -- dial end -- PHONE_LINE注意すべきことは、retry コマンドが見られることができる場所は、rescue 句だけである、ということです。
rescue 句の存在は、retry 文があっても無くても、事後条件を要求する、ということを暗に意味しません。rescue 句が必要とされることがあったとしても、ルーチン内に事後条件を持つ必要はありません。
feature SOME_FEATURE is do ... rescue -- 何らかの動作 end -- SOME_FEATURE ...
この節では、LAND_VEHICLE(陸上乗り物) と WATER_VEHICLE(水上乗り物) という、二つの新たなクラスを導入することにします。これらは両方とも、上で提示したクラス VEHICLE の子孫です。私たちは、LAND_VEHICLE のためのテキストをすぐに与え、WATER_VEHICLE は間もなく下に続くでしょう。
この節の基本は、読者に対して、触れることができる例で使用される継承の機構において干渉したり関与したりすることを許すことです。この時点では、単一継承だけが採用されることになります。多重継承は、私たちが WATER_VEHICLE と LAND_VEHICLE に基づいて HYDRO_LAND_VEHICLE を開発するときに、後の節で使用することになります。
私たちの LAND_VEHICLE のための仕様は、次のようです。
- このクラスは、各実例に、一意に識別できることを可能とするファシリティを提供しなければならなりません。
- 属性:
- Color(色)
- Number of passengers(乗員の数)
- Number of Wheels(車輪の数)
- ルーチン:
- 上記の属性を設定したり検索したりするためのルーチン群。
- 同じく、構築子ルーチンもです。それは、引数として新たな Engine Type 属性を受け取らなければなりません。
読者が気付くであろう最初のことは、この仕様と、私たちがすでに VEHICLE クラスに対して持っている仕様との類似性です。実際に、ソフトウェアの利用方法、データ構造、そしてアルゴリズムの良いポーションは、それぞれお互いの後で様式化されます(LAND_VEHICLE と VEHICLE が類似しているという程度まで必要ありません)。
この類似性は、私たちを、継承を使用するのに完全な位置に置きます。LAND_VEHICLE の仕様の大部分はすでに VEHICLE で実装されているので、私たちは、新しい LAND_VEHICLE というクラスへ適合するために、以前に記述した特徴を VEHICLE から継承し使用できます。私たちは、このクラスがそのように実装されていることを見ています(またも、個々の解釈は、読者のために残しておきます)。
class LAND_VEHICLE creation start; inherit VEHICLE redefine start; end; feature {NONE} num_wheels : INTEGER; feature start (new_num_passengers : INTEGER) is require new_num_passengers >= 1; do num_wheels := 4; color := Red; num_passengers := new_num_passengers; io.put_string ( "Hi! I'm a new LAND_VEHICLE" ); io.new_line; ensure num_wheels = 4; color = Red; num_passengers = new_num_passengers; end; -- start set_num_wheels (new_num_wheels : INTEGER) is require new_num_wheels % 2 = 0; --even number of wheels new_num_wheels >= 2; --at least two wheels do num_wheels := new_num_wheels; ensure num_wheels = new_num_wheels; end; -- set_num_wheels get_num_wheels : INTEGER is do Result := num_wheels; ensure Result = num_wheels; end; -- get_num_wheels; end --LAND_VEHICLE;ここで導入した新たな特徴は、どのクライアントへも可視でない num_wheels、すべてのクライアントへ可視である get_num_wheels と set_num_wheels です。この実装は、LAND_VEHICLE の仕様のための要件を完全に満たしています。これらの特徴は、このクラスにおいて直接実装することはなく、color などは VEHICLE から継承します。気付いていただきたいのは、VEHICLE と比べて、LAND_VEHICLE というクラスのサイズが小さい、ということです。しかし、機能性という点では、LAND_VEHICLE は、より前進的で有用です。これは、オブジェクト指向プログラミングの主要な利益 -- 再利用 -- です。既存のコードを利用する能力は、実装される将来のプロジェクトの費用を大幅に減少させます。この能力の完全な利点を得るために、言語はこの能力を取込み、読者が利用するために、安全で容易に可読な構文を提供する機構を提供することが必要です。Eiffel は、まさにそれを実現します。
いま、私たちは、WATER_VEHICLE の(非形式的な)仕様を与えることによって、クラス群の小さな階層を開発することにおける私たちの冒険を続けます。
- このクラスは、各実例に一意に識別できることを可能にするファシリティを提供しなければなりません。
- 属性:
- Color(色)
- Number of passengers(乗員の数)
- Weight capacity(許容重量)
- ルーチン:
- 上記の属性を設定し検索するためのルーチン群。
- 同じく、構築子ルーチンも必要です。それは、乗員の新たな有効な数を引数として受け取らなければなりません。
この新たなクラスのためのテキストは、下に与えられます。
class WATER_VEHICLE creation start; inherit VEHICLE redefine start; end; feature {NONE} capacity : INTEGER; feature start (new_num_passengers) is require new_num_passengers >= 1; do num_passengers := new_num_passengers; color := Red; capacity := 500; io.putstring ( "Hi! I'm a new WATER_VEHICLE" ); ensure num_passengers = new_num_passengers; color = Red; capacity = 500; end; -- start set_capacity (new_capacity : INTEGER) is require new_capacity > 0; do capacity := new_capacity; ensure capacity = new_capacity; end; -- set_capacity get_capacity : INTEGER is do Result := capacity; ensure Result = capacity; end; -- get_capacity end -- LAND_VEHICLEこうして、私たちは、私たちが記述したいと願っている四つのクラスのうち三つを、ずっと構築してきました。いま、四つ目のクラス HYDRO_LAND_VEHICLE を記述し実装することが残っています。読者はすでに構想を持っているように、このクラスは、WATER_VEHICLE と LAND_VEHICLE から継承することになります。このことは、WATER_VEHICLE と LAND_VEHICLE を通じて暗黙的に二回も継承(反復された継承)される、VEHICLE からの特徴に関係するいくつかの質問を上げさせるかもしれません。私たちは、いま、この状況を検討することにします。
多重継承を利用したクラスは、一回以上(異なる経路を通って)特定の親から、暗黙的に、継承することもできます。次のことを良く考えてください。
![]()
ここで、STOP_WATCH は、それぞれ DISPLAY から継承している DIGITAL と ANALOG から継承します。DIGITAL と ANALOG の両方に存在する、DISPLAY の特徴に何が起きるでしょうか? 二つの複写は、STOP_WATCH で継承されますか? あるいは、一つの複写だけが継承されますか?
その答えは、Eiffel は、すべての特徴の一つの複写に、子孫クラスの中で終わるように許すことだけによって、どんな反復された継承の曖昧さも解決するであろう、ということです。上の場合に、DIGITAL と ANALOG から継承した DISPLAY の特徴のすべては、STOP_WATCH で一度だけ継承されます。実例として、もし DISPLAY が am_pm という特徴を提供したならば、そのような特徴の一つだけが、たとえそれが DIGITAL と ANALOG 経由で潜在的に重複されていたとしても、STOP_WATCH のねじを書き上げたでしょう。
私たちの VEHICLE クラス群へ参照して、いまや私たちの HYDRO_LAND_VEHICLE を書くときになったので、類似した状況が持ち上がりました。HYDRO_LAND_VEHICLE は LAND_VEHICLE と WATER_VEHICLE から継承することになるので、LAND_VEHICLE と WATER_VEHICLE から経由して重複することになるかもしれない VEHICLE の特徴に何が起きるでしょうか? 上の反復した継承の議論は、読者に答えを与えるはずです。
頭の中に浮かんできたかもしれない一つの質問は、start という特徴に何が起きるか、ということです。つまり、それは LAND_VEHICLE と WATER_VEHICLE で再定義されており、それが再定義されたから二回継承することになるということを意味するのでしょうか? これが答えです。つまり、私たちは、名前の衝突に遭遇したのです。
start を LAND_VEHICLE と WATER_VEHICLE で再定義したので、それはもはや、VEHICLE には由来せず、いまや、二つのバージョンにあるその子孫の両方から由来します。同じ名前を持つ二つの特徴、ここでは start は、二つのクラスから継承されるか、名前の衝突<collision>または名前の衝突<crash>になる、ということを5章から思い出してください。Eiffel は、この状況のために、いくつかの選択肢を提供します。私たちは、次の選択肢を選びました。
class HYDRO_LAND_VEHICLE creation start; inherit LAND_VEHICLE undefine start; end; WATER_VEHICLE undefine start; end; feature start is do io.putstring( "Hi! I'm a new HYDRO_LAND_VEHICLE!" ); io.newline; end -- start end -- HYDRO_LAND_VEHICLE私たちの選択は、私たちの祖先から継承した start という特徴の両方とも、定義を取り消すことでした。それから、私たちは、私たちの新たなクラスにおいて名前 start を総体的に自由にし、私たちが選んだようにそれを使用することができました。私たちは、それにその評判を生成ルーチンとして維持するように決めました。
いま、私たちは、陸上と水上の両方を旅行できる乗り物のための、完全に機能する分類を持っています。気付いていただきたいのは、それが私たちの部分として仮想的に余計な実装をとらなかったのは、その能力がすでに私たちの祖先クラスで生成されたからである、ということです。まさに、私たちは、それらがまったく新しい分類を提供し生成したものを構築しました。これは、オブジェクト指向パラダイムの主要なファシリティの一つ -- 再利用性 -- です。
この最後の言葉で、私たちは、この節の最後に来ています。次の章で、私たちは、この言語を分析と設計で安定的にさせる、Eiffel の先進的な機構を議論することにします。これで、Eiffel は、実装を通じるすべての道への、スムーズで継ぎ目の無い移行を提供できます。
[ 目次 ] [ イントロ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Copyright ] [ html (full) | ps | pdf ]
[ Eiffel Liberty | GUERL | sOOap ]