読書メモ:オブジェクト指向でなぜつくるのか
下記の本を読んだのでメモしておく。(読んだのは数年前であり既に2021年に第3版が出版されている)
所感
- ソフトウェア開発の歴史、特に、オブジェクト指向プログラミング(Object Oriented Programming)に焦点を当て解説してあり、3章~4章で、既存の高級言語の課題を明示した後になぜOOPが必要なのかという部分がしっかり言及されていたので説得力があった。
- 5章でOOP の特徴の1つにはメモリの使い方にあることが分かった。歴史的にヒープメモリが既存の言語では利用されていなかったがOOPでは有効活用することが分かったので、普段のプログラミングでは静的領域、ヒープ領域、スタックメモリそれぞれを必要に応じて意識して使い分けを試みたい。
- クラスライブラリ、フレームワーク、コンポーネントといった紛らわしい言葉は共通点や特徴、違いをそれぞれ述べてあり初心者にも分かりやすかった。
- 関数型言語への時代の流れや用語が分かった。関数型言語をOOPとは立場や考え方が異なるものとして記載されているが、実際異なる部分は大いにあるだろうが、考え方自体はメジャーなプログラミング言語にも取り入れられてきているような気がする。
- 近年(2020~)の開発でもOOPのメリットがどれくらい享受されておりどのような技術が用いられているか気になった(例えばクラスはメジャーな言語は大体備えているイメージだが継承はあまり使うべきでないといった話も聞くため)。
以下、読書メモ
※ 1,2,7章は導入の章であり特にメモしていない&その他の章も適宜取捨選択
3章 OOPを理解する近道はプログラミング言語の歴史にあり
- 分かりやすさを重視する構造化プログラミング
- サブルーチンの独立性を高めて保守に強くする
-
この当時、プログラムの保守性を高めるために工夫されたのが、サブルーチンの独立性を高めること。そのために、グローバル変数(複数のサブルーチンが共有する、メインルーチンとサブルーチンで共有する情報を格納した変数のこと) の使用を減らすことが考えられた。
-
その理由は、グローバル変数をデバッグするとき、変更するときにはプログラム全てのロジックを確認する必要が生じるため。
例えばあるサブルーチンが別のサブルーチンを呼び出しており、それぞれの間では情報をグローバル変数でやり取りしていたとすると、どのサブルーチンがグローバル変数をいつ変更/参照しているのかわかりづらくなってしまう
→ そこで 2 つの仕組みが考案された:
- ローカル変数(サブルーチンの中だけで使われる変数であり、サブルーチンに入ったときに作られ、抜けるときに消える性質を持つ)
- 引数の値渡し(call by value, サブルーチンに引数として情報を渡す際に、呼び出し側が参照している変数を直接使わずに、値をコピーして渡す仕組みであり、これに より、呼び出されたサブルーチン側で受け取った引数の値を変更しても、呼び出す側が参照している変数に影響を与えることがなくなるのが嬉しい)
-
-
- GOTOレスプログラミングを実現する構造化言語
- 残された課題はグローバル変数問題と貧弱な再利用
4章 OOPは無駄を省いて整理整頓するプログラミング技術
5章 メモリの仕組みの理解はプログラマのたしなみ
-
プログラムが動く上での概念 1:実行方式について
-
プログラムの実行方式には、基本的には下記の種類がある。コンパイラ方式は実行効率が良く、インタプリタ方式では同じプログラムを異なる環境で動かすことができる。
-
Javaやマイクロソフト社の.NETは中間コード方式と呼ばれる実行方式と対応する。
CPU がプログラムを実行するためにはプログラムを最終的に機械語に翻訳する必要があるが、中間コードの命令は特定の動作環境に依存しない形式になっているため、そのステップを仮想マシンが担っている。例えば JVM はプラットフォームごとに提供されており、実行時に Java の中間コードであるバイトコードを読み込んで、そのプラットフォーム用の機械語に変換してプログラムを実行する。
-
-
プログラムが動く上での概念 2:スレッド、プロセス、ジョブ、タスク
-
プログラムが動く上でのメモリ領域
- プログラムのメモリ領域は基本的に「静的領域」「ヒープ領域」「スタック領域」の 3 つに分けて管理される。
- 静的領域は、プログラムの開始時に確保されプログラムが終了するまで配置が固定される領域であり、ここにはグローバル変数とプログラムの命令を実行可能な形式に変換したコード情報が格納される。各アプリケーションごとに 1 つずつ確保される。
- ヒープ領域はプログラムの実行時に動的に確保するためのメモリ領域のことである。空き領域をなるべく効率的に活用する必要があることや、複数スレッドから同時に割り当て要求が来た時に整合性を保つ必要があることから OS や仮想マシンが管理しており、プログラム実行中にアプリケーションから必要なサイズを要求することで割り当てを行ったり、不要になれば元に戻したりする。各システムまたはアプリケーションごとに 1 つ確保される。
- スタック領域はスレッドの制御のために使うメモリ領域のことであり、ヒープ領域が複数スレッドから共用されるのに対し、スタック領域は各スレッドに 1 つずつ用意される。各スレッドはサブルーチン(OOP ではメソッド)呼び出しの繰り返しで動作するため、スタック領域はサブルーチン呼び出しの制御のために使われることになり、サブルーチンの引数やローカル変数、戻り先などの情報が格納される。スタック領域は LIFO(Last In First Out)という方式で使われることで、メモリ領域を効率的に使用できる。
- プログラムのメモリ領域は基本的に「静的領域」「ヒープ領域」「スタック領域」の 3 つに分けて管理される。
-
メモリ領域の使い方からの OOP の特徴付け
-
クラス情報は各クラスごとに 1 つだけロードされる
クラス情報(個々のインスタンスに依存しないクラス固有の情報)は、内容としては主にメソッドに書かれたコード情報になるが、これはインスタンスに依存しないため、各クラスごと 1つずつロードされる。その方式(タイミング)は大きく分けて2つ存在する
-
インスタンス生成のたびにヒープ領域が使われる
- インスタンスを作る命令(Java では「new」)が実行されるとき、そのクラスのインスタンス変数を格納するのに必要な大きさのメモリがヒープ領域に割り当てられる。このとき同時に、インスタンスを指定してメソッドを呼び出す仕組みを実現するために、インスタンスからメソッドエリアにあるクラス情報への対応付けも行う。各インスタンスはクラス情報を共用する。
- OOP におけるメモリの使い方の最大の特徴はこのインスタンスの生成法にあり、従来のプログラミング言語で書いたプログラムはコードとグローバル変数を静的領域に配置し、サブルーチンの呼び出しの情報はスタック領域を使って受け渡すことでほとんどの処理を実現していた(つまり、ヒープ領域は積極的に使われていなかった)のに対し、OOP では作成したインスタンスはすべてヒープ領域に配置される。
-
変数にはインスタンスの「ポインタ」が格納される
-
ポリモーフィズムは異なるクラスが同じ顔を見せる
- ポリモーフィズムの仕掛けは、メモリにおいては、対象となるクラス間でメソッドテーブル(各クラスで定義された各メソッドのポインタを順番に格納したもの)の形式を統一することのみである。
- 実際にメソッドを呼び出すときには、このメソッドテーブルを経由して目的のメソッドを特定して実行する。こうすることでクラスによってメソッドに書かれたコードが異なっていても、呼び出し方法を統一することができる。
-
継承される情報の種類(メソッドかインスタンス変数か)によってメモリ配置は異なる
-
独立したインスタンスはガベージコレクタが処分する
- ガベージコレクションはガベージコレクタと呼ばれる専用のプログラムが行う。このプログラムはプログラミング言語の実行環境(Java の場合は JavaVM)が提供するもので、独立したスレッドとして動作する。
- このプログラムは適切なタイミングでヒープ領域の状態を調べ、空きメモリ領域が少なくなったことを検知すると実際のガベージコレクション処理を起動する。そのタイミングとは、「孤立したインスタンスを見つけた」タイミングである。
- OOP の場合は引数やローカル変数にインスタンスを指定することが可能であり、その場合スタックにはヒープ領域に存在するインスタンスのポインタを格納することに注意されたい。また、メソッドエリアからもヒープ領域のインスタンスを参照できる。
- プログラムがメモリ不足で異常終了した時に、アプリケーションを見直すポイントとして覚えておくべきことは、不要になったインスタンスをスタックやメソッドエリアから参照し続けていないようにすることである
-
6章 OOPがもたらしたソフトウエアとアイデアの再利用
- フレームワークにはさまざまな意味がある
- フレームワーク(framework)はソフトウェア開発の分野に絞っても定義が曖昧であり、「包括的なアプリケーション基盤」といった比較的漠然とした意味で使う場合と、「特定の目的のために書かれた再利用部品群」を指す場合の大きく 2 つに分けることができる。
- 後者の定義を前提にすると、フレームワークとクラスライブラリは、いずれも再利用可能なソフトウエア部品群を指すが、目的や使い方で両者を使い分けるのが一般的。
- フレームワークの選定が適切な場合、複雑なアプリケーションを簡単に作ることができる。基本的な利用方法としては、フレームワークが用意するデフォルトのクラスを継承しいくつかのメソッドを記述するだけである。Java の Applet(アプレット)などはその典型であり、Applet クラスを継承し、init、start などのいくつかのメソッドを記述するだけで、動的な Web ページを手軽に作ることができる。こうした芸当はポリモーフィズムや継承の仕組みを備えた OOPだからこそできることである。
- 独立性の高い部品を意味するコンポーネント
- デザインパターンは優れた設計のアイデア
8章 UMLは形のないソフトウエアを見る道具
- シーケンス図とコミュニケーション図で動きを表現
- ユースケース図でコンピュータに任せる仕事を表現
- 仕事の流れをアクティビティ図で表現
13章 関数型言語でなぜつくるのか
- 特徴 1:関数でプログラムを組み上げる
- 特徴 2:全ての式が値を返す
- 特徴 3:関数を値として扱える
- 関数そのものを変数に格納したり、別の関数の引数や戻り値に指定することも可能であるようなプログラミング言語の性質、関数そのものを**第一級関数(first-class function)**と呼ぶ。
- 関数型言語では関数が第一級オブジェクトであるため、関数を引数として渡すことができ、それはポリモーフィズムと同様の仕組みを導入したいときシンプルな仕組みをもたらしてくれる。
- OOP では「クラス」があり、1 つのメソッドだけを入れ替えたい場合でもサブクラスとスーパークラスを定義し継承する必要があるが、関数型言語では単に関数を引数として渡すのみである。さらに、関数型言語では、関数を戻り値として返すこともでき、これは部分適用や関数の合成(後述)を可能にする。このように、関数を引数として受け取ったり、関数を戻り値として返したりする関数を**高階関数(higher order function)**とよぶ。
- 特徴 4:関数と引数を柔軟に組み合わせることができる
- **部分適用(partial application)**は、2 つ以上の引数を持つ関数に対し一部の引数だけを適用させ別の関数を作る仕組みである。
- 部分適用を施すこと、つまり、2 つ以上の引数を持つ関数に一部の引数のみを適用させた結果の、残りの引数としての関数として表現することをカリー化と呼ぶ。
- 特徴 5:副作用を起こさない
- 関数型言語で副作用というと、「引数から戻り値を求めること以外の仕事」を指す。
- 「副作用を起こさないプログラム」とは、変数を更新せず、画面やネットワーク、データベースやファイルなどの外部入出力も一切行わないプログラムという意味になる。
- 副作用が無い場合、引数が同じならば何回評価しても関数の戻り値は必ず同じになるが、このような性質を**参照透過性(referential transparency)**と呼ぶ。
- **遅延評価(lazy evaluation)**はプログラムの実行時の仕組みであり、式を上から順に評価するのではなく、実際に必要になった時点で個々の式を評価する仕組みである。
- 関数型言語で副作用というと、「引数から戻り値を求めること以外の仕事」を指す。