$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

「オブジェクト指向入門 第2版 原則・コンセプト」を読み終えた

 まだ上巻だけどようやく読み終えた。大変だけど非常に有意義な本だった。

 この本はいろいろ話題があるけど、第3章 モジュール性、第6章 抽象データ型、第11章 契約による設計の3つの章が面白かった。これまでに感想を書いたブログは以下のとおりなので興味があれば参考にどうぞ。

 昔読んだ時は一度挫折した。挫折した理由としては、もともと静的型付け言語を使ったことがなかったため、理論を自分の経験を使って具体化できなかったためである。今回は以前Scalaを入門していたこともあって、型や総称性の話題もそこそこ理解できたので、問題なく読むことが出来た。

 あとはこつこつつけていた読書メモを置いておく。数字はページ数を表す。ちなみにメモの量が多い章ほど面白く感じたところで、少ない章はもともと知っていたり、そこまで理解できなかったところだった。

読書メモ

1章

  • 最終的に重要なのは外的品質要因だが、それを実現するためには内的品質要因を満たさなければならない 24

3章 モジュール性

5つの基準 50

「モジュール性がある」と呼ぶ価値のある設計手法は、5つの基本的な必要条件を満たす必要がある

  • 分解しやすさ
    • ソフトウェアの構造を、単純な構造を組み合わせた形に設計してあること
  • 組み合わせやすさ
    • モジュールが独立していて、いろんなパターンで組み合わせられる
  • モジュールの分かりやすさ
    • そのモジュールもしくは少ないモジュールの知識だけで、そのモジュールを理解できる
  • モジュールの連続性
    • 仕様に変更があっても、1つか少数のモジュールの変更しか行われない(DRYっぽい)
  • モジュールの保護性
    • 実行時にエラーが起こっても、モジュール内もしくは少数のモジュールにしか影響が広がらない
5つの規則 58

基準から、モジュール性を保証するために守らなければならない5つの規則が導かれる

  • 直接的な写像
    • ある問題領域を記述する良いモデルがある場合、モジュールとモデルとして表現される構造のマッピングが明快であること
  • 少ないインターフェース
    • 全てのモジュールができる限り少ない数のモジュールとの通信で済むように
  • 小さいインターフェース
    • 2つのモジュールが通信する場合、そこで交信される情報はできるだけ少なくする
  • 明示的なインターフェース
    • 暗黙的に同じデータを共有してそれに依存するなどをせず、二つのモジュールの通信は公に行う
  • 情報隠蔽
    • モジュールは必要な情報だけ公開し、あとは非公開とするべき
5つの原則 67

規則と、間接的には基準から、ソフトウェア構築の5つの原則が導かれる。

  • 言語としてのモジュール単位
    • モジュールは使用される言語の構文単位に対応していなければならない
  • 自己文書化
    • モジュールについての全ての情報をそのモジュールの一部として作るべき
    • コメントでドキュメント、というのはまちがってなかった
  • 統一形式アクセス
    • フィールドでもメソッドでも同じような形式でアクセスできるように
  • 開放/閉鎖の原則
    • モジュールは拡張可能で、かつ変更せずに追加のみで拡張できる
    • この原則は有名なので、ググったほうがわかりやすい
  • 単一責任選択
    • システムが選択肢を提供する時、そのシステムの1つのモジュールだけがその選択肢のすべてを把握すべき
    • enumは一つが責任取るべきみたいな

4章 再利用性へのアプローチ

なんのために再利用が必要か、再利用はどの単位で行う必要があるか、再利用のために必要な条件は何か、これまでの再利用手法について書かれた章

  • 再利用性において優れた方針は、ソースコードを再利用すること 94
  • 再利用の単位として適切なのは、よく定義されたインターフェースによって特定の機能をカプセル化する何らかの形式の抽象化モジュール 94
  • 再利用可能なモジュール群にするには、5つの条件を満たす必要がある 107
  • 型のバリエーション: 再利用する人がモジュールを書き換えなくても多くの様々な型の要素に適用できること(総称性とかがつかえること) 108
  • ルーチンのグループ分け: 操作であるルーチンをグルーピングしておけること 108
  • 実装のバリエーション: 108
  • 表現の独立性: 顧客が、そのモジュールの中身がどのように実装されているかを知らずに利用できること 108
  • 共通する振る舞いのふるいだし: 実装のグループやサブグループに存在する共通性を利用できること 110

5章 オブジェクト技術への道

  • トップダウン法の分解の欠点は、時間的な制約に注目する時期がはやすぎること 142
  • オブジェクト指向開発としては速すぎる順序付けを遠ざけている 143
    • 買い物メモアプローチ
    • 必要な操作をリストアップ -> 必要になる可能性のある操作をすべてあげる -> 操作の順序付けの制約はソフトウェア構築の過程のできるだけ後の方になるまで無視する
  • システム分析と設計の初期段階では順序の制約にあまり関心を払わないほうがよい時間的な制約の多くは論理的な制約として抽象的に記述することが可能である 154 144

6章 抽象データ型

  • オブジェクトを適切に表現する記述を作りたい 158
    • 3つの条件がある
    • あいまいさがなく厳密であること
    • 記述が完全でなければならない
    • 記述は過剰仕様であってはならない
  • 特定の表現(ARRAY_UP, ARRAY_DOWN, LINKEDなど)をしようとして使わないようにしたい 162
    • ソフトウェアのコストの17%以上がデータ形式の変更を考慮する必要から発生
  • 操作は3つのカテゴリに分割されている 175, 164
    • オブジェクトの生成
    • オブジェクトに関する情報を返す問い合わせ処理(query)
    • オブジェクトを修正する処理(command)
  • 抽象データの仕様の記述は4つのパラグラフから構成される 168
    • TYPES(型)
    • FUNCTIONS(関数)
    • AXIOMS(公理)
    • PRECONDITIONS(事前条件)
  • TYPES -> STACK[G]のような記述 170
  • FUNCTIONSはADTのインスタンスに適用可能な操作の集合 172
  • FUNCTIONSでは関数を完全に定義するわけではなく、関数のシグネチャ(引数と結果の型のリスト)を記述するだけ 174
  • AXIOMSはその抽象データ型が必ず満たす条件。属性を記述する 175, 176
  • 操作の中でソース集合の全ての要素に適用可能であれば全関数、そうでなければ部分関数 178
  • 部分関数であればそのソース集合の定義域を指定する必要があり、それがPRECONDITIONSの役割 179
  • この抽象データ型に実装をしたもの(ただし実装は部分的でも良い)がクラスである 184
  • ADTから作られるクラスなら、ADT仕様記述は外部に公開し、表現の選択とその表現によるADT関数の実装は非公開にするべき 187
  • ensure句で表現されるルーチンの事後条件 188
スタックのADT仕様記述 180
  • 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
    • 任意のx:G, s:STACK[G]に対して
    • item(put(s,x)) = x
    • remove(put(s,x)) = s
    • empty(new)
    • not empty(put(s,x))
  • PRECONDITIONS
    • remove(s: STACK[G]) require not empty(s)
    • item(s: STACK[G]) require not empty(s)

7章 静的な構造 クラス

  • クラスは(部分的であっても良い)実装を伴う抽象データ型である 211
  • クラスという概念において最も顕著な属性は、モジュールの概念と型の概念を包括し、1つの言語的構造に併合したこと 218
  • クラスのインスタンスに適用可能な操作を特性と呼ぶ 221
  • 特性の分類 222 ☆
    • インスタンスに関係づけることによって表現されたものを属性(データとして持つものと思えばいい)
    • 計算によって求められる特性をルーチン
      • ルーチンの中で結果を返すのをファンクション
      • 結果を返さないものをプロシージャ
  • 特性の分類を木構造で表した図 223
  • 属性であろうとファンクションであろうと同じ形式でアクセスできることを統一形式アクセスと呼ぶ 224 ☆
    • そのクラスの顧客はその特性が属性で実装されてるか、ファンクションで実装されているかについて興味はないはず
    • ScalaとかPerlとかは統一形式アクセスになっているはず、JavaScriptは属性は括弧なし、ファンクションは括弧を付けないといけないのでそうではないはず
  • クラスを使う方法は継承する方法か顧客となる方法しかない 233
  • infix, prefixなどの定義はScalaにも同じようなものがあって、これにより全てをオブジェクトとして扱っても、伝統的な表記と互換性があるのが良いように見える 241
  • クラスを書く人は個々の属性について5つのレベルのアクセス特権を顧客に自由に与えることができなければならない 265
    • アクセス不可 : 顧客は属性にアクセス出来ない
    • 読み出し専用 : 全く修正は許されていない
    • 制限付き書き込み可 : 特定のアルゴリズムによる属性の修正を顧客に許す
    • 保護付き書き込み可 : 値を設定できるが、特定の制約を満たす場合(x > 3とか)のみ許す
    • 無制限
  • エンティティとは実行時の値を表す名前。変数と思っておけば良さそう 273
    • エンティティとは次のうちのいずれかである
      1. クラスの属性
      1. ルーチンのローカルなエンティティ
      1. ルーチンの仮引数

8章 実行時の構造 オブジェクト

  • オブジェクトの定義 : オブジェクトは何らかのクラスの実行時のインスタンスである 280
  • 参照の定義 : 参照とは実行時にvoidかアタッチ状態かのいずれかの値を取る。アタッチ状態の場合、参照は1つのオブジェクトを示す 288
  • オブジェクト、参照、エンティティの違い 309
  • 拡張型というものが必要な時がある。拡張型はいわゆる値渡し的な使い方になる 326
  • 拡張型の役割は3つ 328
    • 効率性を向上させる
    • より良いモデル化を可能にする
    • 統一されたオブジェクト指向型体系の中で基本型をサポートする

9章 メモリ管理

自由モードの意味がよく分かってない

第11章 契約による設計

抽象データ型をさらに具体的に実装する話、特に事前条件・事後条件を中心として説明してくれる。非常に参考になる章。設計時に必ず参考にしたい。

  • ソフトウェアの正しさとは、対応する仕様書通りに動くかである 432
  • 正しさの公式 {P} A {Q} 433
    • 任意のAの実行は、Pの状態になったときに始まりQの状態になった時に終了する
    • もしPがfalseなら、Aは実行されない
  • より強い(より限定される)事前条件はAの処理で取り扱いやすく、より弱い事後条件はAの責任が緩和される 434-435
  • 事前条件と事後条件は、それぞれrequireとensureというキーワードを使って、ルーチン宣言の中に中に明確に示される 438
  • 事前条件と事後条件による義務と利益 442 ☆
    • 事前条件はルーチン呼び出しが正当である状態が定義される。顧客にとっては呼び出しの仕方に義務を持ち、提供者にとっては利益(特定の仮定をもとにシンプルな処理をできる)となる
    • 事後条件はリターン時にルーチンが保証すべき状態が定義される。顧客にとっては保証された状態を必ず得られるという利益を持ち、提供者にとっては状態を必ず提供するという義務が発生する
  • 非冗長性の原則/どんな事情があっても、ルーチンの事前条件にあたるテストを、ルーチンの本体で行ってはならない 444
    • 防御的プログラミング(すべての場所で検査しすぎる方が良い)とは逆
    • 冗長性のある検査は損傷を与える
    • アーキテクチャ全体にわたる観点を持つと、シンプルさが重大な判断基準となり、すべてを検査すると複雑化するため
  • 表明違反規則(2) 448
    • 事前条件違反は顧客側にバグがある証拠である
    • 事後条件違反は供給者側にバグがある証拠である
  • スタックを表明込みで実装した例 450-452
    • 具体的な例として参考に
  • ルーチンをうまく関数化できなくなるような整合性の条件全てに対して、顧客か供給者のどちらに責任を割り当てるべきか 456
    • 顧客に責任を割り当て、条件をルーチンの事前条件の一部として表す -> 要求型
    • 供給者に責任を割り当て、条件はルーチン本体にif文や同等の制御構造で表す -> 保護型
  • 再利用性が重要であるソフトウェアの場合は、要求型のほうが良いとされている 457
    • 顧客だけが、その関数が使えない条件でルーチンをよんでしまった時に、それがどういう意味があるのかを決定する十分な情報を持つ
    • もしエラーにするなら、例外処理を行うのか、状況を正して再度トライするのか、ユーザに見えるエラーメッセージを出力するのか
  • 妥当な事前条件の原則 459
    • (「要求型」設計アプローチにおいて)全てのルーチンの事前条件は次の要求を満足しなければならない
      1. 顧客モジュールに配布される公式文書に、事前条件が示されていなければならない
      1. その事前条件が必要かどうかは、仕様の観点からでしか判断してはならない
  • 事前条件には顧客に公開している情報しか使ってはならない 460
  • 事後条件では非公開の特性を含めてもエラーにならない 461
    • 顧客に有用な条件ではないが、クラス全体のテキストを読む人にとって意味がある
  • 全てのルーチンで維持されなければならない、クラスインスタンスに共通する全体的な特性をクラス不変表明(class invariant)という 467
  • 事前条件違反は顧客のエラーであり、事後条件違反は供給者のエラーであり、不変表明違反は事後条件と同様供給者のエラーである 471
  • ADT特性とクラス表明の対応 480
    • 仕様の1つの関数に対する事前条件は、対応するルーチンの事前条件句として再現される
    • 命令関数を含む公理は、対応するプロシージャの事後条件句として再現される。その際、複数の問い合わせ関数を含むかもしれない
    • 問い合わせ関数のみを含む公理は、対応する事後条件句、もしくは不変表明句として再現される
    • 構築関数を含む公理は、対応する生成プロシージャの事後条件に再現される
  • 不変表明に現れる表明で、抽象データ型には直接対応しない「実装不変表明」というものがある 482
    • 隠し属性入りの表明
    • 抽象データ型に対する実装の中から、選択されたクラスの一貫性を表す
  • 表明の使いみち 497
    • 正しいソフトウェアを書くのを助ける
    • 文書化支援
    • テスト、デバッグ、品質保証をサポートするため
    • ソフトウェアの耐障害性をサポートするため
  • 実行時に別のファイルで設定を書くことにより、表明を監視することが出来る 502 503
    • これにより表明で定義されたものが満たされない時にエラーを発生せることが出来る
    • 表明をどこまで監視するかは設定できる必要がある(no, require, ensure, invariant, loop, check)
  • 表明をどこまで監視すべきか 504-505
    • システムの開発時では、最高レベルで表明を監視するべき
    • 製品稼働時には事前条件のみを監視すると良い
    • 事前条件を監視しないなら、システムを十分に信頼することはできない
ブログに書きたい

信頼性の高いソフトウェアを記述するために、表明という概念について説明した章。ソフトウェアが正しいとは何かについての議論や、事前条件・事後条件・クラス不変表明のような表明の種類の説明、表明の使いみち、表明の監視などについて解説してくれる

  • 事前条件はそのルーチンが呼ばれる時は必ず保持されなければならない性質を、また事後条件はルーチンがリターンする時に保証しなければならない性質を表す 437
  • 事前条件と事後条件による義務と利益 442 ☆
    • 事前条件・事後条件についてと、義務と利益の対応、そしてバグの責任
    • クラス不変表明というのもあるが、これは全てのルーチンの事後条件と考えれば、事後条件と責任が似ている
  • 表明を満たさない時にどうするべきか
    • 設定に応じてどこまでチェックするか
    • 本番では事前条件だけ、というのはなるほど(Arrayの境界値の問題)
    • さらにチェックに引っかかった場合にどうするかは次の章で説明されるだろう
疑問点
  • 事前条件を満たさない場合にそのルーチンを呼んだらどうなるべきか
    • <- どのレベルまでチェックするかを設定し(開発中は全て、本番では事前条件のみとか)、実行時例外にする気がする?

第12章 契約が破られるとき:例外処理

  • 例外の起きるケース 529
    • 事前条件、事後条件、クラス不変表明などの表明が満たされない時にも例外が起こる
  • ルーチンの実行中に起こった例外を処理する正しいやり方 534
    • リトライ:例外となる状態を変更し、ルーチンを最初から実行し直す
      • この時、クラス不変表明と事前条件を満たした状態でretryする必要あり 549
    • 失敗:環境をきれいにし、実行を終了して呼び出し側に失敗を報告する
      • この時、クラス不変表明は満たした状態で失敗を報告(raise)する必要あり 549
    • OSのシグナルから生じる例外には、まれに例外は無害として実行を再開するだけで良い場合がある
  • rescue句の正当性規則 549
    • 失敗を発生させるなら、 {True} Rescue {INV}
    • リトライさせるなら、{True} Rescue {INV and pre}
    • 例外が発生するならどのような状況も起こりうるので、事前条件で絞り込むことはできない
  • 例外の処理(rescue句)では、シンプルで、かつ、受け手のオブジェクトを安定な状態に戻す(クラス不変表明の状態に戻す?)というたった1つの目的を目指すものでなければならない 555
  • 例外処理とは、予想外の実行時状態に対処するメカニズムである 558
    • ということは予想内のinputなどに対応するValidationなどは例外で処理しない?

第15章

  • 多重継承が有効な例 665-667
    • 社用飛行機は飛行機であり、資産であるとか
    • 腕時計に電子計算機を合わせたものは多重継承でうまくいくとか

第16章

  • ルーチンを再宣言するとき、表明部分は次のようにすることもできる 732
    • 事前条件を弱いものに置き換える
    • 事後条件を強いものに置き換える
  • 表明再宣言の規則(論理的な宣言バージョン) 739
  • 開放/閉鎖の原則の「開放」にあたるのは、子孫で特性を変更する能力である。その際、仕様と矛盾しないかぎり、再定義により実装を変更することが許される 745
  • 総称パラメータを制限する 752
    • Scalaであったようなものだと思ったらいい
  • 試行代入によって代入しようとしているものが正しい型かチェックできる 758-759
    • これはfind_type_constraint()->checkみたいな動的なチェックと考えたらいい
  • 型再宣言規則 764
    • 特性の再宣言により、特性(属性またはファンクションの場合)の型、もしくは仮引数の型(ルーチンの場合)を、もとの型と適合する型に置き換えることができる。
    • 再宣言が許されるのは特化(つまりより特殊化)の方向のみである
  • エンティティの型を絶対的なものとして宣言するのではなく、他のエンティティに対し相対的に宣言する仕組みを、アンカー宣言という 768
    • 型の再宣言の時に使いやすい?