up:: Programming
up:: Summareading
source:: 新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡 - Qiita
ソフトウェア危機
プロジェクトが複雑化する中、管理手法が無く複雑度に人間がついていけなくなり始めた時代。
構造化プログラミング
ステートメントを直に書くのでなく、抽象化、階層化して組み込み、ソフトウェア内で仮想機械の様なものを作成する。こうすることである階層に対する変更が他の階層に影響を及ぼさなくなる。
現代のレイヤリングアーキテクチャの様なもの。gotoに代わりforとifが台頭する。
モジュラプログラミング
凝集度、結合度
二つの概念が登場。この辺はOCPより遅かったりするが。
悪い結合は別々のモジュールが同じモジュールの内部データを使っていたり(内容結合)、同じグローバル変数(共通結合)を参照してたりすること。
良い結合は内部構造に依存せず、情報のやり取りが明示的。
悪い凝集は適当な根拠で集めてたり(暗号的凝集)、論理的に似ている処理を集めてたり(論理的凝集)、同じタイミングで集めてたり(時間的凝集)すること。大事なのはデータとの関係、メンテナンスのしやすさ。
良い凝集はあるデータに触れる処理を纏めたり(通信的凝集)、適切な概念、データ構造、アルゴリズムをひとまとめにしたり(情報的凝集)、一つの上手く定義されたタスクをこなせるようにしたり(機能的凝集)すること。
状態と副作用の支配
関数の複雑化によって想定外の状態、想定外の副作用が増え始める。
モジュラプログラミングではこうした想定外を別々に命名、可視化、疎結合化していた。
そのため、構造体に対して複数の専用関数が書かれるようになる。
pythonで第一引数にselfが来たりするのはその名残かも。
この発展は関数を主軸にしている関数型プログラミングで見られる傾向であり、後の状態や副作用をデータ構造に隠蔽して主軸にした手続き型(オブジェクト指向)プログラミングとは違う。
抽象データ型
状態と副作用を隠蔽し、データとアルゴリズムをひとまとめに。中身を知らなくても弄れるように。そのためにできた概念。今でいうクラス。
構造化プログラミングでステートメントを抽象化したが、入力されるデータの処理が抽象化されていなかった。それを解決する部分。
ここでインスタンス化の概念が生まれる。あるデータ型を実際に存在するメモリに割り当てる。
抽象データ型の肝は、「内部データへのアクセスは抽象データ型に紐づいた関数でしか行えない」という点。状態と副作用の支配で書いたように構造体に関数を連ねていくと、構造体への直アクセスが防げない。
なお、この問題に対してC言語はクラスを作るのでなく、ヘッダの公開非公開を分けて情報隠蔽を行っている。
これらのポイントは、抽象データを扱うレイヤでは、抽象化されていない生の階層を触らせない、という階層化の考え方。
抽象データ型の情報隠蔽とカプセル化
ヘッダファイルで分けるのは大変なので、変数関数単位でアクセス制限を実現。それがアクセス修飾子。
オブジェクト指向?
本題。
実は言語的にはなんと1960年代、ソフトウェア危機が叫ばれ始めるころから存在する。それがSimula。動的ディスパッチ(動的メソッド切り替え)や、何ならガベコレまである。
こういう過去の優れたコンセプトの再評価はよくある。
Simulaに「メッセージング」という概念を加え、全ての処理をメッセージ式として記述させたのがSmalltalk。純粋オブジェクト指向言語。
オブジェクト指向は名称発明者アランケイによると、メッセージングを軽視しない。
Simula & C++ オブジェクト指向
C++開発者ビャーネ・ストロヴストルップ「オブジェクト指向は継承と多態性を付与した、抽象データ型のスーパーセット」
C++ではメソッドをメンバー関数と呼ぶ。
メソッドはSmalltalkの用語で、メンバー関数はSimulaがメンバープロシージャと呼ぶことに起因。
多態性
多態性=ポリモーフィズム。
引数の型によって呼ばれる関数が変わる。
オーバーロードに似てるが、内部は違う。特にインターフェースを引数にした時が分かりやすい。
関数のオーバーロードは内部的に関数+型情報で区別されている。
そのため、インターフェース型で呼べばインターフェース型のオーバーロード関数を呼ぶことになる。当然そんなものは無いので呼べない。
動的なポリモーフィズムでは内部でデータごとに別の関数を呼んでいる。
switchで処理を変えているようなもの。たとえインターフェース型が引数でも、それが何なのかによって内部で個別に分類する。
言い換えれば、データ自身にどの関数を呼ぶか覚えさせる。
まんま動的ディスパッチ。(あるいは動的分配)
これ論理的凝集では? って感じだが、switchで変えた先での実際の処理はそれぞれデータ型の中にあるので大丈夫。その処理を呼ぶ、ということさえすればいい。インターフェースや抽象メソッドと同じノリ。
動的ディスパッチ
動的ディスパッチを実現するためにvirtualを使用した際。
内部では、データにvtableという仮想関数テーブルへのポインタを埋め込み、それをたどって解決している。
もちろんそれだけのコストはかかる。それを無視して常にvirtualにしているのがJava、必要な時しか利用しないのがC++とかC#。ゼロオーバーヘッドポリシーっていうらしい。
まとめると、ポリモーフィズムには三つの要素が必要。
- データに自分が何者か教える
- メソッドを呼び出した際に探索する
- オブジェクト自身を参照できるよう、引数に束縛する
3は多態性で説明したように、switchで分けるためにオブジェクトの型が必要だからちゃんと引数に渡しといてね、という話。
継承と移譲
継承
継承が無い世界線では、共通箇所の構造体を作り、派生する構造体に共通構造体を入れ込む、という形をとる。
しかしこれだと派生構造体が増えるたびに入れ込む必要がありめんどい。そこで継承が追加された。
移譲
継承から親子関係を無視すると、「クラスに呼ばれた処理が無かったら別クラスを見に行く」機能になる。となると、クラスに参照する別クラスの情報を埋め込めばこれは実現できることになる。
継承などがクラスを重視しているためクラスベースのオブジェクト指向と呼ばれるのに対し、こちらはプロトタイプベースのオブジェクト指向と呼ばれる。言うなれば共通構造体がポインタで埋め込まれてるような形。そのため共通部分は動的に変更可能。
この機能を持つ代表がjavascript。プロトタイプチェーンと呼ばれている。Luaではmetatableと呼ぶらしい。
移譲に依るメソッド探索を、動的継承とも呼ぶ。
オブジェクト指向では、こういう動的探索にどんな機構を付けるのか、というのが重要な構成要素。
オブジェクト指向の要素
- 抽象データ型
- データと処理を紐づける
- 情報の隠蔽を行うことが出来る
- →カプセル化
- オブジェクト
- データ自身が何者か知っている
- 動的多態
- →ポリモーフィズム
- オブジェクト自信のデータと処理を自動探索
- 探索先設定
- 継承
- 移譲
- 探索先設定
- 動的多態
- データ自身が何者か知っている
Smalltalk & Objective-C オブジェクト指向
過去の遺物だろ、と思いきや実は再評価ポイント。
アランケイ「オブジェクト指向は『オブジェクト』とその間の『メッセージ送信』で表現すること」
つまりUEに似ている。
仮想機械としてのオブジェクト
アランケイ「
機械はメモリ、CPU、命令で出来ている。
これを抽象化するなら、データ、処理、命令セットを持つ仮想機械である
」
これはダイクストラの構造化プログラミングと被る概念。
メッセージ
通信から類推されたオブジェクト同士の情報疎通概念。
これそのものもオブジェクトなので、動的作成可能。
Smalltalkには複数のメッセージを同時にまとめて送るカスケード式という機能がある。
またメッセージを受け流す転送機能もある。
メッセージという類推から生まれた発想。
非同期送信も可能。
これを中心に発展させモデル化させたのが、まんまアクターモデル。
まとめ
オブジェクト指向の3つの要素と言えば、カプセル化、継承、ポリモーフィズム。
だが元のオブジェクト指向では、それと同等かそれ以上にメッセージ送信が重要視されていた。
(仕組み的には、そのメッセージがどう処理されるかはオブジェクトに任されるので、カプセル化に入りそうな気はする)
始めにオブジェクト指向をもたらしたのはSimulaであり、C++はそこから抽象データ型や多態性、継承などを引き継いだ。
一方でSmalltalkはSimulaの方向性をメッセージとオブジェクトで纏め直し、動的な性質を表した。現在再注目中。
なおぶっちゃけて言うと、C++Smalltalk問わず言語ごとにオブジェクト指向が指すものが微妙に異なるので完全理解は無理。
そんなことより書いて動かして遊ぼうぜ!