継承に関係したメカニズム、中でも暫定クラスは、ソフトウェア構築の問題(この本の はじめに示した)解決に大きな役割を果たす。
- 暫定クラス + 表明 ≒ 抽象データ型(に近い)
- 暫定クラスを使ったスタックの例: p638
- スタックの抽象データ型仕様(ADT:第六章)と(実際のプログラムコードが)かなり類似している
- ADTは以下の四つの要素から構成される (右側は暫定クラスでの対応物)
- TYPES(型): クラス
- FUNCTIONS(関数): クラス特性
- PRECONDITIONS(事前条件): ルーチン事前条件
- AXIOMS(公理): ルーチンの事後条件とクラス不変表明
スタックのADT(6章からの抜粋)
TYPES
=====
- STACK[G]
FUNCTIONS
=========
- put: STACK[G] x G -> STACK[G]
- remove: STACK[G] -/-> STACK[G]
- item: STACK[G] -/-> G
- empty: STACK[G] -> BOOLEAN
- new: STACK[G]
AXIOMS
======
- A1: item(put(s,x)) = x
- A2: remove(put(s,x)) = s
- A3: empty(new)
- A4: not empty(put(s,x))
PRECONDITOINS
=============
- remove(s: STACK[G]) require not empty(s)
- item(s: STACK[G]) require not empty(s)
スタックの抽象データ型と暫定クラスの違い
- 暫定クラスには change_top, count, wipe_out といった操作がある
- 今はないがADTに追加しても問題ない
- 暫定クラスにはADTのnewに対応するものがない
- 暫定クラスの性質上当然。実際のインスタンス生成は子孫の役目。
- 暫定クラスにはfullファンクションがある
- 実装側(ex. 配列スタック)からの制約だが、ADTと暫定クラスの間の本質的な違いではない
- ADTを有限スタックをカバーするものに変更することも可能
- その場合でも(fullで常にfalseを返せば)無限スタックも表現できるので一般性は損なわれない
- ADT仕様は純粋に適用的(関数的)
- 表明メカニズムではADTの公理が表現できないものがある
- 上の例でいえばA2には表明の対応物がない
- この制約の理由および解決策のヒントに関しては 11.14.2節 が詳しい
全ての暫定クラスが、例のスタックのように抽象データ型に近いわけではない
- 図14.15: 表の例
- クラスと抽象データ型の関係 (仕様よりか実装よりかでさまざまな段階があり得る)
- 完全暫定クラス(TABLE)はADTに一致
- 部分暫定クラス(SEQUENTIAL_TABLE)はADTに関連した実装群に一致
- 完全有効クラス(ARRAY_TABLE)はADTの実装に一致
SEQUENTIAL_TABLEの例
- 完全暫定クラス(TABLE)と完全有効クラス(ARRAY_TABLE)の中間
- 暫定クラスではあるが、アルゴリズムによっては、実装するのに十分な情報を備えている
- ex. has特性 (p.642)
- start,forth,item,afterという他の暫定特性群に依存
- forthに事前条件と事後条件を定義しておくことは非常に重要
- 将来の有効化の際に仕様が守られることが保障される
- 継承の安全性を担保するのは表明の役目!!
- SEQUENTIAL_TABLEのようなクラスを 振る舞いクラス(behavior class) と呼ぶ
- いくつかのADT異形態に共通する動作を捕らえたもの
- 振る舞いクラスには、オブジェクト指向ソフトウェア構成の基本デザインパターンを示すものがある
SEQUENTIAL_TABLE(のhas特性)は、振る舞いクラスの概念を通して、オブジェクト指向技術が再利用性の主要な問題点の中の最後の問にどう答えるか(第4章での検討事項以降、未解決 のままである共通動作を取り出す方法)を表している
俺たちを呼び出すな、俺たちが呼び出す
つまり、これは、再利用可能なプリミテイブを呼び出すアプリケーションシステムではなく、アプリケーシヨン開発者が戦略的な場所に自分自身の変形を「埋め込む」ための汎用的なスキーマである
後の章の設計例のいくつかは、この技術に依存している この技術は、再利用可能なソ フトウェアを構築するためにオブジェクト指向技術を使う際、中心的な役割を果す
いわゆるフレームワーク的なコードの再利用方法:
- 共通部分は暫定クラスで実装して、カスタマイズが必要な部分だけを有効クラスで定義する
- 妥当な実装があるなら暫定特性の代わりにデフォルト実装を提供しておくのもあり
- コールバックスキーマとしても知られている (p.644)
- WebアプリとかGUIアプリとかを開発していると当然のように使う技術 (最近だと当たり前すぎる感はある)
振る舞いクラスのおかげで、クラス、継承、型チェック、暫定クラスおよび暫定特性を通し、オブジェクト指向は、技術の組織的かつ安全なサポートを提供することができるようになった。 これは、表明が、開発者がプログラムを修正するときに常に満たさなければ ならない性質を記述しているのと同じである。
14.8.3の続き
たったいま話題にしてきた技術は、オブジェクト指向法が再利用性のために果たす中心的な役割を示す。 (サブルーチンライブラリのような)凍結された構成要素だけではなく基本スキーマを与えるような柔軟な解決を提供することで、たくさんの異なるアプリケーション要求に適合させることができる
再利用性を検討する際の中心テーマの1つは、再利用性と適合性を結合することである(「再利用か作り直しか」のジレンマを抜け出すために)。 そのためには、「穴の開いたプログラム」という名前のコ インを作ればよい。 渡すことのできる実際の引数の値以外はすべて決まっているサブルーチンライブラリとは違って、「穴の開いたプログラム」には、部分的にユーザが中身を決める余地がある。
- レゴブロックとプレイボード(or 電気コンセント)の例
- 部品を組み合わせてもの(ソフトウェア)を作るのが従来のC的な方法
- OOPなら構造をライブラリが提供して、ユーザは一部だけを(仕様に合わせて)変更して利用することが可能
- どちらが良いかはケースバイケースだが、設計の選択肢が増えるのは歓迎
以下蛇足:
- Monadは?
- 環境の提供(?)
- 自分の関心がある一連のコードを書けば勝手(Monadの種類に応じて)に、
- 例えば、失敗時にその箇所でブレークしてくれる(Maybe)
- 例えば、並列実行してくれる
- 例えば、リトライ実行してくれる
- コールバックスキーマとは異なり、記述するのは完全な一連のコードだが、それがどう実行されるかはMonad次第
- アプリケーションコードは、ある仮定のもとで記述できるので、シンプルになる (と期待したい)
- ex. Maybeモナドなら、その文脈内の一連の関数は(自分より前方の関数の)処理が失敗しない、ものと仮定できる
分析、設計、実装で別々の表記法を使うのは(一般的にはそう進められることが多いが)良くない
- 各段階でギャップがあり翻訳が必要 (その度に誤りが入る可能性がある)
- 一貫性の維持が大変なので、メンテナンスや進化にとっても有害
- たいていの場合、(本書の表明が提供するような)記述力が不十分
Effielなら分析・設計から実装まで全て一本で賄えます!
分析、設計に暫定クラスを使うことで、抽象的でかつ厳密に表すこと、そして、ソフトウェア開発プロセスを通して同じ言語を使うことが可能となった。
概念的なギャップ(い わゆる「インピーダンスミスマッチ(impedancemismarches)J)をなくすことにより、いまや、上位モジュールの記述から実装までの移行を同一形式でスムーズに行うことができるだけでなく、設計モジュールの未実装の操作でさえも、いまや暫定ルーチンによって、事 前条件、事後条件、不変表明によって非常に厳密に定義することができるようになった。
これで表記法は、完成の域に近付いた実装と|可じように分析、設計にも適用することができる。 同じ概念と構成のものを各段階で用いることができる。それらは記述する抽象レ ベルに応じて、詳細さの差があるだけなのである。
この章では、継承の基本的な概念を紹介してきた。 ここでは、紹介したいくつかの慣習 について、そのメリットを検証してみよう。
Effielの言語仕様の正当性の主張
- redefine句が必須な理由: 誤りを避けるために再定義は明示的に示した方が良い
- Javaでいう@override推奨の話と同様
- Precursor
- 再定義ルーチンで親ルーチンを参照できる
- オリジナルの実装への依存が明確に分かる
- 多重軽傷の際には、参照する親の名前を指定することであいまいさもなくせる
- 静的型付け言語 + 賢いコンパイラなら動的束縛でも(定数オーダーでしか)遅くならない
- 動的束縛がないなら、非OOPの関数呼び出しと同様のコスト(に最適化可能) - 14.9.5節
- 動的束縛がある場合でも、定数オーダーの追加コストで実行可能
- コンパイル/リンク時に型情報は全て分かっているので、事前にテーブルを作っておけば良い
- C++のvtable
- (実行時の)追加コストは配列アクセスが一回増えるくらい
- 継承が深くても、多重継承でも同様
- 何もしないプロシージャの場合、動的束縛は静的束縛に比べて約30%性能が下がった
- 当然、プロシージャの本体が複雑になるにつれて、(相対的に)動的束縛のオーバーヘッドはより小さくなる
-
動的束縛をサポートする言語でも、静的束縛と同様のコストで特性が呼び出せることがある
- 特性が再定義されていない
- オブジェクトが多相ではない (実行時の型が一意に定まる)
-
自動インライン化の話
- インライン化はコンパイラが自動で判別して行うべき、云々
- 規模が大きくなるにつれて手動インラインは制御できなくなる、云々
- 全体的に(規模が大きいなら)最適化はコンパイラに任せるべき、という主張
- その方がコードが自然になり、誤りが入りにくく、大域的な最適化も行いやすいはず、とのこと
-
これらの最適化を組み合わせれば、大規模システムでは、CやFortranコードよりも性能がでることすらある
-
OOPだとif分岐がなくせる(代わりに動的束縛用の配列アクセスになる)ので、かえって効率的なこともある
動的束縛の原則
==============
静的束縛は、その結果が動的束縛の結果に等しくないならば意味的に正しくない。
静的束縛は最適化かバグかのどちらかである
動的束縛と同じ意味 を持つ(SIとS2の場合のように)ならば最適化であり、それはコンパイラによって達成きれる。もし別の意味ならばバグである
型Aである宣言済みのxがあるとする。xは、実行時に型Bのオブジェクトにアタッチされたが、Bにおいてrは再定義されている。このような場合、呼び出し x.r で、本来のバージョン(つまり、rA)が呼び出されることはない。もしそうなったら、それはバグである!
C++での静的束縛がデフォルトなのが良くない理由を延々と
- コンパイラに任せられることは任せてしまうべき
- コンパイラは「メソッドが再定義されていないなら静的束縛にする」ということができてしかるべき
- プログラマがやるのは手間 & 間違いのもと & (大規模システムでの)最適化機会の損出
- 事前に静的束縛か動的束縛化を選択しなければならないのは「開放/閉鎖の原則」に逆行する
- 後から動的束縛にしたくても(既存コードに修正が必要なので)変更が困難 or 不可能なケースがある
- せめてデフォルトを動的束縛にすべき
- OOPにおいて静的束縛は、動的束縛と意味が違った場合、常に誤った選択となる (14.9.6節)
省略 (口頭で軽く本の内容を読むくらい)
省略
省略