継承ではいろいろなメカニズムが使用可能:
- 改名
- 再定義
- 未定義化(undefinition)
- 結合(join)
- 選択(selection)
- 子孫の隠蔽
しかし、これらを活用すると継承構造は込み入って見にくいものになってしまう。
そのため、フラット形式(flat form)、という特殊な継承のないクラスバージョンが必要となる。
継承は顧客の技術ではなく 供給者 の技術:
- クラスを効果的に組み立てるために使用される内部的なツール
- 原則として、顧客はクラスの継承構造を知る必要はない
- 多相性や動的束縛を使いたい場合は例外
フラット形式:
- 顧客向けに、祖先の知識とは全く無縁に、自己完結したやり方でクラスを表すための形式
- クラスの全特性が(直接定義されたものか継承されたものかの区別なく)同じレベルで示される
- コマンドスクリプト(or IDE)を使って自動生成が可能
- フラット形式は意味的には、通常の形式(クラステキスト)と等価
- クラス作成者が継承を使えなかった場合に、クラスがどう見えるかを表している
- 多相的に使われる場合を除けば、顧客からはオリジナルと全く同じ意味を持つ正しいクラステキストに見える
フラット形式を作成するということは次のことを意味する:
- inherit句は削除される
- クラスの特性の宣言と再宣言はそのまま
- 継承された特性すべてに対しては、親からコピーして宣言が追加される
- inherit句に記述された継承上の変換(改名,未定義化,選択,特性結合)はすべて反映される
- それぞれの継承された特性には
from ANCESTOR
という形式のコメント行が追加される - 継承されたルーチンの完全な事前条件と事後条件が再構築される
- すべての親の不変表明を and することで完全な不変表明が再構築される
- その他
feature -- アクセス
といった特性句のラベルは残される- 同じラベルを持つ特性句は併合される
- 特性句内での特性はアルファベット順になる
図15.14: フラット形式の例
- フラット形式は開発者にとって貴重なツール
- 経継の潜在的な弱点(クラステキストを読んでもすぐには特性名の意味がつかめない)を解決する
- クラスの単独バージョンをデリバリする場合にも有用
- ただしこのバージョンは多相的には使えない
- フラット形式は正しいクラステキスト
- 供給者側にはそれで有用だが、顧客側には外部仕様だけを表現するもっと抽象化されたものが必要
- 11章で紹介した short 形式を組み合わせると(顧客にとって)良い感じ
- フラット-ショート形式
- ショート形式と同様にクラスの公開情報だけを含む & 実装の詳細(ex. do句)は削除される
- フラット-ショート形式は、再利用可能なライブラリクラスを仕様化する場合にも重要なメカニズム
- あるクラスが複数の方法で別のあるクラスの子孫となっているときには必ず反復継承(repeated inheritance)が起きている
- これは、潜在的なあいまいさが生じる原因ともなるので、対策を考えなければならない
多重継承が行える言語では常に反復継承が起こりえる
図15.15 反復継承の図
直接的な反復継承は利用ケースが実際に存在する? => 15.4.10
反復継承の実例
- DRIVER: 属性{age, address, violation_count}, ルーチン{pass_birthday, pay_fee}
- US_DRIVER
- FRENCH_DRIVER
- FRENCH_US_DRIVER
図15.16 ドライバの種類
反復継承の最初の主要な問題が、この大陸間移動ドライバの例にはっきりと現れている。
反復子孫(この例では、FRENCH_US_DRIVER)において、反復祖先(DRIVER)から継承された特性にはどういう意味があるのか
特性ageの例:
- US_DRIVERとFRENCH_DRIVERの両方から同じフィールドを継承している
- 衝突しているように見えるので、改名が必要か?
- 不要: 両方とも元々はDRIVERから継承されているもので、実体は同じなので衝突は起きていない
- 国を跨いだからといって年齢が変わるのはおかしい(はず)
- 名前の衝突規則(15.2.1)にも違反してはいない
- 規則: 「別々の親から名前は同じで中身の違う特性を継承するクラスは間違いである」
- pass_birthdayプロシージャも同様
規則:
反復祖先から継承されてきた特性が複数の親から同じ名前で継承された場合には、反復子孫では同一特性として継承されなければならない
この状態は 共有(sharing) と呼ばれる。
全てが共有と考えても良いか?
- いけない
- FRENCH_US_DRIVERの例では address, pay_free, violation_count は国を跨げば変わるはず
- 別々の内容を持った二つ特性として扱う必要がある
- このようなケースを 複製(replication) と呼ぶ
共有と複製の使用方法:
- 共有に関しては何もしなくても良い (両方の親から同じ名前でオリジナル版を継承すればよい)
- 複製したいなら、別々の名前に(継承経路のどこかで)改名された特性を継承すればよい
反復継承規則:
反復子孫では、同じ名前で継承された反復継承特性は同一特性を表す。 異なる名前で継承された特性は、共通する祖先のオリジナルから複製された別個の特性を表している。
複製を使ったクラスFRENCH_US_DRIVERの定義:
class FRENCH_US_DRIVER inherit
FRENCH_DRIVER
rename
address as french_address,
violation_count as french_violation_count,
pay_fee as pay_french_free
end
US_DRIVER
rename
address as us_address,
violation_count as us_violation_count,
pay_free as pay_us_fee
end
feature
...
end -- クラス FRENCH_US_DRIVER
図15.17 共有と複製:
- ageとpass_birthdayは継承経路のどこでも改名されていない => 共有
- address等は反復子孫で改名されている => 複製
- 中間祖先であるUS_DRIVER等で改名を行うこともできる
図15.18 属性の複製:
- 複製の場合のオブジェクトのメモリレイアウト(概念図; 詳細は実装依存)
- 共有属性は物理的にも共有可能
- 反復継承を多用した場合の空間効率を下げないために重要
- 効率要求云々 (総称性..., 動的束縛コスト...)
- C++は常に物理的に複製されるからダメ
- 「大陸間移動ドライバ」のように共有と複製の両方を使用する反復継承は、まれにしか必要とされない
- 無理に反復継承を使うと、必要以上に複雑になってしまう可能性がある
- 初心者向けの技術でもない
よくある初心者が犯しやすい典型的な誤りは 図15.19 に示されているような 冗長継承:
- 不本意だけど無害
- 無害: 効率的なオーバヘッドはなく、Aの継承は実質上は単に無視されるだけ
- クラスBがクラスAの特性を再定義している場合は、コンパイラがエラーを吐く
- ANY, GENERAL, その他汎用的な標準ライブラリの継承時に起こり得る
この節では新しい概念は出てこない。これまで見てきた規則をより厳密に公式化しそれを説明するための例を示している。
定義: ファイナルネーム:
クラス特性のファイナルネームには次のケースがある
- 直接特性(クラス内で宣言される特性)の場合、宣言された名前がファイナルネーム
- 継承された特性が改名されない場合、親クラスにおけるその特性のファイナルネームに等しい(再帰的に)
- 特性が改名された場合、改名によってついた名前がファイナルネーム
シングルネーム規則:
同じクラス内で異なる2つの有効特性が同一のファイナルネームを持ってはならない。
補足説明や実例など。今までの内容の繰り返しなので省略。
中間祖先で特性を再宣言した場合は、動的束縛が反復子孫であいまいになる可能性がある:
- 図15.21 潜在的にあいまいな再定義句
- 未定義化(15.4.7)と選択(15.4.8)が問題の解決に使える
特性の2つのバージョンが同じ名前で(共有)継承されている場合には、次の三つのケースが考えられる:
- S1: どちらか or 両方 のバージョンが暫定特性 => これは問題なし
- S2: どちらのバージョンも有効だが、両方ともredefineサブ句で再定義されている => これも問題なし
- 反復子孫が定義(redefine)したバージョンに統合される
- S3: どちらのバージョンも有効かつ再定義されていない => シングルネーム規則に違反しており名前の衝突
解決策:
-
- 2つのバリエーションの片方を rename する (よく使う手)
- 共有ではなく複製が得られる
- 複製における衝突に関しては、詳細は次節で取り上げる
-
- 2つのバリエーションの片方のみを引き継がせたい場合もある
- 引き継ぎたくない方の特性を暫定にすることができる (S3 => S1への変形)
- 通常の再定義の仕組みを流用することも可能だが、間に一クラス挟む必要があるので重たい
- undefine という言語メカニズムが使用可能
undefineの使用例:
class D inherit
B
C
undefine f end
feature
...
end
undefine句は他の問題の解決にも利用できる(望ましい言語メカニズムの特徴):
- 例えば、多重継承時における複数の特性の 結合(join) に利用可能
- 図 15.22 2つの親の特性を結合する
- C.gをB.fに結合したい
- C.gを rename してから undefine することで結合が表現できる
- rename => redefine の組み合わせでも結合は可能
- 名前の衝突回避のために導入された改名メカニズムは、あえて衝突を起こさせて結合を実現するためにも使える
- 応用力が高いので優れた言語メカニズムである
図15.23 選択の必要性
複製において再定義が衝突するケースがある:
- 継承経路の途中で「改名 + 再定義」が入った場合
- 別個の特性となるので名前の衝突はない
- ただし共通の特性から派生(?)しているので、動的束縛時に問題が生じる
- 反復継承の最後の知見
- D.f が呼び出された時に A.f と B.bf のどちらが実行されるべきか?
- どちらを選択すればよいかを、コンパイラが機械的に決定することはできない
- __select__句を使用して、開発者が指定する必要がある
選択(selection)の規則:
あるクラスが、反復祖先の特性の、別個の有効バージョンを複数継承していて、
しかもどれも再定義しない場合、select句でその中の1つを必ず選ばなければならない。
- 再定義衝突時に常に片方の祖先を優先したい場合は
select all end
が使用可能 - 「政略結婚」の継承で起こる
- STACKとARRAYの場合は、STACKが優先され「高貴な親(noble parent)」と呼ばれる
- allキーワードを使った場合でも、個別の特性に関して、別の親の方の特性を採用することは可能
! 特殊な技術の説明
たいていの場合はPrecursor {クラス}
で十分。
redefine,select,rename,exportを組み合わせることで反復継承を使っても(オリジナル版のキープが)実現可能。
再定義された特性のオリジナル版をキープするのに反復継承を使うこのやり方は、どんな場合に役にたつのだろうか。普通はその必要はない。予約語 Precursor を使えば十分である。反復継承を使うと良いのは、新しい特性の1つとして、再定義版の他にオリジナル版もキープしたいが、再定義版を実装するのに古いバージョンを使う必要はない場合である。
再定義関数以外で、オリジナル版の関数を参照したい場合に有用?
実例:
-
WINDOW
- WINDOW_WITH_BORDER, WINDOW_WITH_MENU
- WINDOW_WITH_BORDER_AND_MENU
- WINDOW_WITH_BORDER, WINDOW_WITH_MENU
-
WINDOW定義: displayプロシージャ
-
WINDOW_WITH_BORDER: display=Precursor + draw_border(internal)
-
WINDOW_WITH_MENU: display=Precursor + draw_menu(internal)
-
WINDOW_WITH_BORDER_AND_MENU
- 間違い: display=Precursor{WINDOW_WITH_BORDER} + Precursor{WINDOW_WITH_MENU}
- Window.displayが二回呼び出されてしまう
- 正解: WINDOWも継承して display=Presusor{WINDOW} + draw_border + draw_menu
- 間違い: display=Precursor{WINDOW_WITH_BORDER} + Precursor{WINDOW_WITH_MENU}
発展: 各WindowインスタンスにIDをつけたい
- renameとselectを使えば実現できる
反復継承の祖先の特性が仮総称パラメータを含む場合、継承先で曖昧性を引き起こすことがある。
本の例
曖昧さ防止用の規則がある。
反復継承での総称性規則:
反復継承規則によって共有される特性の型、もしくはその特性がルーチンの場合には、
引数の型すべては、反復継承元であるクラスの総称パラメータであってはならない。
この曖昧さは継承先で特性を改名することでなくすことが可能。
名前の衝突: 定義と規則
多重継承によって得られてクラスでは、異なる親から継承された2つの特性が
同じファイナルネームを持つとき、名前の衝突が起こる。
次のケースのいずれかの場合以外は、名前の衝突を起こしたクラスは違法となる。
N1: 2つの特性は共通の祖先から継承していて、その祖先のバージョンからの継承経路において、どちらも再宣言されていない
N2: 2つの特性は互換性のあるシグネチャを持っており、そのうちの1つは少なくとも暫定形で継承されている
N3: 2つの特性は互換性のあるシグネチャを持っており、どちらもそのクラスで再定義される
Precursorを使う場合には、N3の場合にだけ参照する親クラス名の指定が必須となる。
- 多重継承が可能な言語には、名前の衝突問題が常に付きまとう
- この章で提示した以外に可能なのは、次の二つのやり方だけ
-
- 顧客に全てのあいまいさを取り除くように要求する
- 最終的な顧客が使用したいフィールドがどの親に由来するかを指定する
- 次の(本章で強調していた)原則に反する
- 「あるクラスに関わる継承構造はそのクラスやその祖先のプライベート事であり、多相的に使われた場合にどうなるかという点以外は顧客には関係のないことである」
-
- デフォルトの解釈を選ぶ
- ex. CommonLispのCLOS
- 静的な型付け言語には合わない
- 「別個の親に存在する同名の2つの特性には型的な互換性があるはずだとするのは、根拠のないこと」
-
- 改名ならこのような問題はなし
- 継承した特性を顧客にとってより意味のある名前に変更できる、というメリットもある
- 特性の構文的な多重定義と意味的な多重定義
- シグネチャ情報によってコンパイラ時に解決されるのは前者
- 動的束縛を用いて実行時に解決するのは後者 (こちらはオブジェクト指向的)
- 後者には、別々のクラスでも、基本的に同じ操作のバリエーションには同じ名前を使用可能という利点がある
オブジェクト指向技術では、構文的な多重定義は不要では?
- クラスを持たない言語(ex. Ada83)なら必要というのは分かる
- オブジェクト指向言語では、開発者が 2つの別個の操作 に対し同じ名前を使えるというのは混乱のもと
- 構文的な多重定義と意味的な多重定義の両方が使えると、どちらが優先されるのか分かりにくいケースがある (本に例有)
- 「読みやすさのシンプルな原則に従うならば、同一モジュール内では、名前とその名前が持つ意味とは、何のためらいもなく結び付けることができるものでなければならない」
- 個人的には特性のIDの単位が「名前のみ」なのか、それとも「名前+シグネチャ」なのかくらいの違いなだけのようにも思う
def addString(String x)
のような宣言は情報量的には明らかに冗長
- 個人的には特性のIDの単位が「名前のみ」なのか、それとも「名前+シグネチャ」なのかくらいの違いなだけのようにも思う
- STRING同士の結合とCHARACTER同士の結合を両方とも
++
を使うのはよろしくない(両者の意味合いは異なるので) - 引数情報だけだと処理内容が区別・推測できないことがある (intとかのchar*とか組み込み型を使う場合は良くある)
- 概念が異なるものには別の名前をつけるべき
- JavaとかC++はコンストラクタの名前が選べない云々
まとめ:構文的(クラス内)多重定義をオブジェクト指向の文脈で使うと、メリットらしいことは何もないのに、多くの問題が生じる(C++、Java、Ada95といった言語を使う人に、方法論的なアドバスを1つしよう。多重構築関数といった、その言語で他に選択の余地のない場合以外には、この機能を全く使わない方がよい)。 オブジェクト指向技術の一貫した、そして生産的なアプリケーションにおいて、「クラス内では、すべての特性が名前を持ち、それぞれの名前が1つ1つの特性を指す」という(シンプルで、教えやすく、使いやすく、覚えやすい)規則を貫くべきである。
本を参照
省略