読書メモ:オブジェクト指向でなぜつくるのか

 

下記の本を読んだのでメモしておく。(読んだのは数年前であり既に2021年に第3版が出版されている)

www.amazon.co.jp

所感

  • ソフトウェア開発の歴史、特に、オブジェクト指向プログラミング(Object Oriented Programming)に焦点を当て解説してあり、3章~4章で、既存の高級言語の課題を明示した後になぜOOPが必要なのかという部分がしっかり言及されていたので説得力があった。
  • 5章でOOP の特徴の1つにはメモリの使い方にあることが分かった。歴史的にヒープメモリが既存の言語では利用されていなかったがOOPでは有効活用することが分かったので、普段のプログラミングでは静的領域、ヒープ領域、スタックメモリそれぞれを必要に応じて意識して使い分けを試みたい。
  • クラスライブラリ、フレームワークコンポーネントといった紛らわしい言葉は共通点や特徴、違いをそれぞれ述べてあり初心者にも分かりやすかった。
  • 関数型言語への時代の流れや用語が分かった。関数型言語OOPとは立場や考え方が異なるものとして記載されているが、実際異なる部分は大いにあるだろうが、考え方自体はメジャーなプログラミング言語にも取り入れられてきているような気がする。
  • 近年(2020~)の開発でもOOPのメリットがどれくらい享受されておりどのような技術が用いられているか気になった(例えばクラスはメジャーな言語は大体備えているイメージだが継承はあまり使うべきでないといった話も聞くため)。

以下、読書メモ


※ 1,2,7章は導入の章であり特にメモしていない&その他の章も適宜取捨選択

3章 OOPを理解する近道はプログラミング言語の歴史にあり

  • 分かりやすさを重視する構造化プログラミング
    • FortranCOBOL 等の高級言語の登場後、1960 年代後半には「20 世紀末にはソフトウェアの需要にプログラマの人口、通常の意味での人口ですら追いつかない」というソフトウェア危機が宣言

      → 様々なアイデアや新たなプログラミング言語が考案された

    • オランダ人学者ダイクストラが提唱した**構造化プログラミング(GOTO レスプログラミング)**はその 1 つであり、「正しく動作するプログラム作成のためのキーは分かりやすい構造のプログラムを作成することであり、それは例えばプログラムのロジックを順次進行、条件分岐、繰り返しの3つの構造(基本3構造)だけで表現することで実現される」というもの

  • サブルーチンの独立性を高めて保守に強くする
    • この当時、プログラムの保守性を高めるために工夫されたのが、サブルーチンの独立性を高めること。そのために、グローバル変数(複数のサブルーチンが共有する、メインルーチンとサブルーチンで共有する情報を格納した変数のこと) の使用を減らすことが考えられた。

      • その理由は、グローバル変数デバッグするとき、変更するときにはプログラム全てのロジックを確認する必要が生じるため。

        例えばあるサブルーチンが別のサブルーチンを呼び出しており、それぞれの間では情報をグローバル変数でやり取りしていたとすると、どのサブルーチンがグローバル変数をいつ変更/参照しているのかわかりづらくなってしまう

      → そこで 2 つの仕組みが考案された:

      • ローカル変数(サブルーチンの中だけで使われる変数であり、サブルーチンに入ったときに作られ、抜けるときに消える性質を持つ)
      • 引数の値渡し(call by value, サブルーチンに引数として情報を渡す際に、呼び出し側が参照している変数を直接使わずに、値をコピーして渡す仕組みであり、これに より、呼び出されたサブルーチン側で受け取った引数の値を変更しても、呼び出す側が参照している変数に影響を与えることがなくなるのが嬉しい)
  • GOTOレスプログラミングを実現する構造化言語
    • 構造化プログラミング理論の浸透に合わせて、Pascal, C言語などの構造化言語が登場したがC言語には2つの特徴がある:
      1. ポインタなど細かな機能を携えていることから、UNIX OS の記述言語に使われるなど、アプリケーション開発からシステムプログラミングまで幅広く使われた
      2. 必要な機能のすべてを言語仕様で提供せず関数ライブラリでくみ上げるようにした(言語コンパイラを改良せずとも言語仕様レベルの機能追加ができるようになった)
  • 残された課題はグローバル変数問題と貧弱な再利用
    • 構造化プログラミングでも、2つの問題が残った:
      • グローバル変数問題(何らかの事情でグローバル変数を変更する際には、影響範囲を確かめるためにロジックをすべて調べる必要が生じてしまう問題)
      • 貧弱な再利用問題(大規模化するアプリケーションに対し、再利用/共通部品として使えるものがサブルーチンだけだったという問題)

4章 OOPは無駄を省いて整理整頓するプログラミング技術

  • OOP が持つ構造化言語には無い 3 つの仕組み
    • OOP の 3 つの仕組みは構造化言語では解決できない2つの課題を解決するためのものであり、それを一言で述べると、「重複した無駄なロジックを排除し必要な機能を整理整頓する仕組み」と言える。
      • クラスは関連性の強いサブルーチン(関数)とグローバル変数を 1 つにまとめて粒度の大きいソフトウェア部品を作る仕組みである。この仕組みを使うことで、従来はバラバラに存在していたサブルーチンと変数をまとめて整理整頓できる。
      • ポリモーフィズム継承は、共通サブルーチンではうまく対処できない重複したコードを一本化する仕組みであり、これにより、ソースコードの無駄を徹底的に省くことができるようになる。
  • インスタンス変数は「仲間内だけのグローバル変数
    • クラスの仕組みにより、従来はグローバル変数として定義していたものをクラス内部のインスタンス変数として隠せるようになった
    • インスタンス変数の性質は 2 つある:
      1. 別のクラスのメソッドからアクセスできないよう隠すことができる
      2. 一度インスタンスが作られた後は、必要なくなるまでメモリ上に残される
    • インスタンス変数は、影響範囲を局所化できるローカル変数の嬉しさと、存在期間が⻑いグローバル変数の嬉しさを良いとこどりした仕組みであり、「⻑持ちするローカル変数」または「仲間内だけのグローバル変数」である

5章 メモリの仕組みの理解はプログラマのたしなみ

  • プログラムが動く上での概念 1:実行方式について

    • プログラムの実行方式には、基本的には下記の種類がある。コンパイラ方式は実行効率が良く、インタプリタ方式では同じプログラムを異なる環境で動かすことができる。

    • Javaマイクロソフト社の.NETは中間コード方式と呼ばれる実行方式と対応する。

      CPU がプログラムを実行するためにはプログラムを最終的に機械語に翻訳する必要があるが、中間コードの命令は特定の動作環境に依存しない形式になっているため、そのステップを仮想マシンが担っている。例えば JVM はプラットフォームごとに提供されており、実行時に Java の中間コードであるバイトコードを読み込んで、そのプラットフォーム用の機械語に変換してプログラムを実行する。

  • プログラムが動く上での概念 2:スレッド、プロセス、ジョブ、タスク

    • プロセス(メインフレームではジョブと呼ぶのが一般的である)はメーラーや Web ブラウザ、表計算ソフトとといったパソコン上で独立して動く各アプリケーションプログラムとよばれる単位
    • スレッドはプロセスよりも小さいプログラムの実行単位のこと(スレッドはメインフレーム全盛時代にはタスクと呼ばれていた)。
    • スレッドはあるプロセスの中に複数存在することが可能で、実際に多くのアプリケーションが複数スレッドから構成されている(例:Web ブラウザのリクエスト処理や「中止」ボタン)。
  • プログラムが動く上でのメモリ領域

    • プログラムのメモリ領域は基本的に「静的領域」「ヒープ領域」「スタック領域」の 3 つに分けて管理される。
      • 静的領域は、プログラムの開始時に確保されプログラムが終了するまで配置が固定される領域であり、ここにはグローバル変数とプログラムの命令を実行可能な形式に変換したコード情報が格納される。各アプリケーションごとに 1 つずつ確保される。
      • ヒープ領域はプログラムの実行時に動的に確保するためのメモリ領域のことである。空き領域をなるべく効率的に活用する必要があることや、複数スレッドから同時に割り当て要求が来た時に整合性を保つ必要があることから OS や仮想マシンが管理しており、プログラム実行中にアプリケーションから必要なサイズを要求することで割り当てを行ったり、不要になれば元に戻したりする。各システムまたはアプリケーションごとに 1 つ確保される。
      • スタック領域はスレッドの制御のために使うメモリ領域のことであり、ヒープ領域が複数スレッドから共用されるのに対し、スタック領域は各スレッドに 1 つずつ用意される。各スレッドはサブルーチン(OOP ではメソッド)呼び出しの繰り返しで動作するため、スタック領域はサブルーチン呼び出しの制御のために使われることになり、サブルーチンの引数やローカル変数、戻り先などの情報が格納される。スタック領域は LIFO(Last In First Out)という方式で使われることで、メモリ領域を効率的に使用できる。
  • メモリ領域の使い方からの OOP の特徴付け

    • クラス情報は各クラスごとに 1 つだけロードされる

      クラス情報(個々のインスタンスに依存しないクラス固有の情報)は、内容としては主にメソッドに書かれたコード情報になるが、これはインスタンスに依存しないため、各クラスごと 1つずつロードされる。その方式(タイミング)は大きく分けて2つ存在する

      • 事前に全てのクラス情報をロードする方式(例:C++)
      • 必要な時点でメモリに逐次ロードする方式(例:Java)
      • これらの方式の間には実行性能とメモリ使用量のトレードオフがある。クラス情報のロードで 2 つの方式に共通しているのはロード先であり、静的領域(Java ではメソッドエリアとよばれる)にロードされる。
    • インスタンス生成のたびにヒープ領域が使われる

      • インスタンスを作る命令(Java では「new」)が実行されるとき、そのクラスのインスタンス変数を格納するのに必要な大きさのメモリがヒープ領域に割り当てられる。このとき同時に、インスタンスを指定してメソッドを呼び出す仕組みを実現するために、インスタンスからメソッドエリアにあるクラス情報への対応付けも行う。各インスタンスはクラス情報を共用する。
      • OOP におけるメモリの使い方の最大の特徴はこのインスタンスの生成法にあり、従来のプログラミング言語で書いたプログラムはコードとグローバル変数を静的領域に配置し、サブルーチンの呼び出しの情報はスタック領域を使って受け渡すことでほとんどの処理を実現していた(つまり、ヒープ領域は積極的に使われていなかった)のに対し、OOP では作成したインスタンスはすべてヒープ領域に配置される。
    • 変数にはインスタンスの「ポインタ」が格納される

    • ポリモーフィズムは異なるクラスが同じ顔を見せる

      • ポリモーフィズムの仕掛けは、メモリにおいては、対象となるクラス間でメソッドテーブル(各クラスで定義された各メソッドのポインタを順番に格納したもの)の形式を統一することのみである。
      • 実際にメソッドを呼び出すときには、このメソッドテーブルを経由して目的のメソッドを特定して実行する。こうすることでクラスによってメソッドに書かれたコードが異なっていても、呼び出し方法を統一することができる。
    • 継承される情報の種類(メソッドかインスタンス変数か)によってメモリ配置は異なる

    • 独立したインスタンスはガベージコレクタが処分する

      • ガベージコレクションはガベージコレクタと呼ばれる専用のプログラムが行う。このプログラムはプログラミング言語の実行環境(Java の場合は JavaVM)が提供するもので、独立したスレッドとして動作する。
      • このプログラムは適切なタイミングでヒープ領域の状態を調べ、空きメモリ領域が少なくなったことを検知すると実際のガベージコレクション処理を起動する。そのタイミングとは、「孤立したインスタンスを見つけた」タイミングである。
      • OOP の場合は引数やローカル変数にインスタンスを指定することが可能であり、その場合スタックにはヒープ領域に存在するインスタンスのポインタを格納することに注意されたい。また、メソッドエリアからもヒープ領域のインスタンスを参照できる。
      • プログラムがメモリ不足で異常終了した時に、アプリケーションを見直すポイントとして覚えておくべきことは、不要になったインスタンスをスタックやメソッドエリアから参照し続けていないようにすることである

6章 OOPがもたらしたソフトウエアとアイデアの再利用

  • フレームワークにはさまざまな意味がある
    • フレームワーク(framework)はソフトウェア開発の分野に絞っても定義が曖昧であり、「包括的なアプリケーション基盤」といった比較的漠然とした意味で使う場合と、「特定の目的のために書かれた再利用部品群」を指す場合の大きく 2 つに分けることができる。
    • 後者の定義を前提にすると、フレームワークとクラスライブラリは、いずれも再利用可能なソフトウエア部品群を指すが、目的や使い方で両者を使い分けるのが一般的。
      • クラスライブラリと呼ぶ場合、OOP の仕組みを利用して作った再利用部品を指すだけで、目的や使い方までは限定しない。
      • フレームワークと呼ぶ場合、単に OOP を利用して作ったライブラリというだけでなく、特定の目的を果たすためのアプリケーションの半完成品を指す。加えて、アプリケーションからの使い方としては、従来の関数ライブラリのように単に呼び出すのではなく、その反対にフレームワークからアプリケーションを呼び出すように使うものを指す。つまり、基本的な制御の流れをフレームワーク側であらかじめ提供しておき、アプリケーションで個別の処理を組み込む仕組みである。
    • フレームワークの選定が適切な場合、複雑なアプリケーションを簡単に作ることができる。基本的な利用方法としては、フレームワークが用意するデフォルトのクラスを継承しいくつかのメソッドを記述するだけである。Java の Applet(アプレット)などはその典型であり、Applet クラスを継承し、init、start などのいくつかのメソッドを記述するだけで、動的な Web ページを手軽に作ることができる。こうした芸当はポリモーフィズムや継承の仕組みを備えた OOPだからこそできることである。
  • 独立性の高い部品を意味するコンポーネント
  • デザインパターンは優れた設計のアイデア

8章 UMLは形のないソフトウエアを見る道具

  • シーケンス図とコミュニケーション図で動きを表現
    • クラス図はソースコード情報を表現するのに対し、シーケンス図とコミュニケーション図はプログラムの実行時の動きを表現する。従来のプログラミング言語(構造化言語)ではプログラムが動くこのような図の必要性はさほど高くなかった
      • その理由は、構造化言語では、サブルーチンの単位でプログラムを記述しその単位で動くことから、構造化チャートやフローチャートで静的な情報と動的な情報をほぼ表現できたため
  • ユースケース図でコンピュータに任せる仕事を表現
    • ユースケース図は、コンピュータの仕事の範囲を明確に表現するために使われる。具体的には対象とするシステムと外部(利用者や他のシステム)との境界を定め、コンピュータにまかせる仕事の内容を簡潔に表現する。
    • ユースケースの中心となるユースケース(Use Case)とは、「実際に使う例」といった意味で、コンピュータが利用者に提供する機能を指す
  • 仕事の流れをアクティビティ図で表現

13章 関数型言語でなぜつくるのか

  • 特徴 1:関数でプログラムを組み上げる
    • 従来のプログラミング言語において、「関数」という用語は一般的に「ひとまとまりの手続き」の意味で使われてきた。プログラミング言語によっては、戻り値を持つ手続きを「関数」、戻り値を持たない手続きを「プロシージャ」のように用語を使い分けるものもある。
    • 関数型言語の「関数」は数学の関数とほぼ同じ仕組みを表す。関数型言語における関数は、引数と戻り値を必ず持つ。
  • 特徴 2:全ての式が値を返す
    • 従来のプログラミング言語関数型言語では、プログラムの基本的な構成要素についても呼び方の違いがある。前者が**命令文(statement)と呼ぶのに対し、後者は式(expression)**と呼ぶ。

      • 命令型言語は、命令文(例:変数のメモリ領域への確保、計算結果のメモリ領域への格納、手続きの呼び出し)から成るプログラミング言語のこと

        命令型言語の例:アセンブリ言語高級言語、構造化言語、オブジェクト指向言語

      • 関数型言語ではプログラムの構成要素を式と呼ぶ。式は必ず値を返すものであり、式は「(命令を)実行する(execute)」ではなく「(式を)評価する(evaluate)」と表現する。

        関数型言語は「式」から成り立ち、関数もデータも値を返す「式」とみなされる。

    • 命令型言語は手続き的であり、関数型言語は宣言的であると表現されることがある。

  • 特徴 3:関数を値として扱える
    • 関数そのものを変数に格納したり、別の関数の引数や戻り値に指定することも可能であるようなプログラミング言語の性質、関数そのものを**第一級関数(first-class function)**と呼ぶ。
    • 関数型言語では関数が第一級オブジェクトであるため、関数を引数として渡すことができ、それはポリモーフィズムと同様の仕組みを導入したいときシンプルな仕組みをもたらしてくれる。
    • OOP では「クラス」があり、1 つのメソッドだけを入れ替えたい場合でもサブクラスとスーパークラスを定義し継承する必要があるが、関数型言語では単に関数を引数として渡すのみである。さらに、関数型言語では、関数を戻り値として返すこともでき、これは部分適用や関数の合成(後述)を可能にする。このように、関数を引数として受け取ったり、関数を戻り値として返したりする関数を**高階関数(higher order function)**とよぶ。
  • 特徴 4:関数と引数を柔軟に組み合わせることができる
    • **部分適用(partial application)**は、2 つ以上の引数を持つ関数に対し一部の引数だけを適用させ別の関数を作る仕組みである。
    • 部分適用を施すこと、つまり、2 つ以上の引数を持つ関数に一部の引数のみを適用させた結果の、残りの引数としての関数として表現することをカリー化と呼ぶ。
  • 特徴 5:副作用を起こさない
    • 関数型言語副作用というと、「引数から戻り値を求めること以外の仕事」を指す。
      • 「副作用を起こさないプログラム」とは、変数を更新せず、画面やネットワーク、データベースやファイルなどの外部入出力も一切行わないプログラムという意味になる。
    • 副作用が無い場合、引数が同じならば何回評価しても関数の戻り値は必ず同じになるが、このような性質を**参照透過性(referential transparency)**と呼ぶ。
    • **遅延評価(lazy evaluation)**はプログラムの実行時の仕組みであり、式を上から順に評価するのではなく、実際に必要になった時点で個々の式を評価する仕組みである。