誰もが知っているけど誰も知らない「オブジェクト指向」
そもそもオブジェクト指向とは
プログラミングを始めると必ず耳にする謎の言葉「オブジェクト指向」。
しかし、「オブジェクト指向とは何か?」を
調べてみても、
"オブジェクト同士の相互作用としてシステムの振る舞いを捉え…"
"現実世界をモデリングした…"
ふわあっとした説明ばかりで全然分かりませんね。
なぜかというとオブジェクト指向の明確な定義が存在しないためです。曖昧な理解のまま広がっていったため、誰もはっきりとした説明が出来ません。Web上の記事や書籍にあふれるオブジェクト指向の説明はあくまでも筆者の解釈で、各々の理解がばらばらなんです。
そんなわけでこの記事も私の解釈になりますが、実務で使う上で1番しっくり来て、役に立った解釈を説明します。
巷で使われるオブジェクト指向の意味
巷で使われている「オブジェクト指向」は、C++やJavaっぽい文法のことでしかないことが多いです。具体的には、クラスがあればオブジェクト指向言語だとされます。巷の「オブジェクト指向」は「クラス指向」と言い換えてもいいでしょう。(クラスを使わなくてもオブジェクト指向で書くことは出来ますが…)
なぜオブジェクト指向と呼ぶのか
プログラムはデータ(数字や文字列など)と手続き(画面の表示など、コンピュータへの命令)で出来ています。プログラミング手法とはデータと手続きをどうやって並べれば見やすく・書きやすくなるかという話なんです。
COBOLやC言語など手続き型プログラミングはデータ定義と手続きを上から下に並べます。同じような手続きはサブルーチンや関数としてひとまとめにできます。手続きを単純につなぎ合わせてつくるから手続き型プログラミングです。
オブジェクト指向ではデータ(キャラクターの座標など)と手続き(移動処理、攻撃処理など)をオブジェクトとしてひとまとめにできます。オブジェクトをつなぎ合わせてつくるからオブジェクト指向プログラミングです。このオブジェクトの(型)定義をクラスと呼びます。
手続き型はデータと手続きを分けますが、オブジェクト指向ではデータと手続きを同じ場所に書いて1つの部品として扱えます。
オブジェクト指向は銀の弾丸ではない
オブジェクト指向はどんな問題も解決してくれる銀の弾丸ではありません。なんですが、オブジェクト指向は全てを解決してくれる魔法の手法で、問題が発生するのはオブジェクト指向が出来ていないからに違いない…と考える人はものすごく多いです。
プログラミング手法はオブジェクト指向だけではなく、場面によっては手続き型プログラミングや関数型プログラミングといった他の手法が有効な場合もあるのであまりオブジェクト指向ありきで考えないようにしましょう。
特に「オブジェクト指向が出来ている」とか「オブジェクト指向が出来ていない」という言葉には要注意です。オブジェクト指向は全てを解決する…と考えている人が使いがちな表現なので、本当にオブジェクト指向で書けば解決する問題なのかよく考えましょう。
オブジェクト指向における三大要素の本質
オブジェクト指向で大事だとよく紹介される三大要素というものがあります。これらを言い換えると以下の通りです。
- カプセル化
- 内部に複雑なものを閉じ込め、外部から見たときの挙動をシンプルにすること
- 継承
- インターフェース(抽象)に対してプログラミングするということ
- ポリモフィズム(多態性)
- 異なるものを同じ様に扱えること
カプセル化
「内部に複雑なものを閉じ込め、外部から見たときの挙動をシンプルにすること」
プログラミングに限らず、モノ作りには必須の要素です。
カプセル化が出来ていると、中身を知らなくても扱えます。InputとOutputさえ分かっていればいいです。カプセル化が出来ていないと、動作機構まで理解していないと扱えません。
エレベーターを例にとると、
- カプセル化が出来ている場合
行きたい階数のボタンを押すだけで動く→誰でも簡単に操作できる。安全! - カプセル化が出来ていない場合
アクセルとブレーキが搭載され、行きたい階数まで手動で操作する→職人技が必要。危険!
カプセル化が出来ているパターン
- クラスや関数の宣言部分だけ読めば何をするのか、どう使うのか分かる。
カプセル化が出来ていないパターン
- 引数によく分からないものを大量に渡さなければならない。
Win32API… - 中身を読まないと何をするのか分からない。
- 関数を呼ぶと、関係なさそうな変数が書き換わる。
継承
「インターフェース(抽象)に対してプログラミングするということ」
継承元のインターフェース(or クラス)は共通規格を表します。同じインターフェースを継承している場合(ある程度)同じ様に扱うことが出来ます。
勘違いされがちですが、継承の目的は、機能の受け継ぎや重複の排除ではありません。機能の受け継ぎや重複の排除がしたいだけなら、別の方法を取るべきです。(後述)
AがBを継承する(ClassA extends ClassB)場合、「AはBである」が成立するか確認しましょう
OK: 「トラックは自動車である」
NG: 「ゴムはタイヤである」
親クラスには、どのクラスでも使う基本的な機能を宣言しておきます。
(宣言だけで、実装は書かなくてもいい)
ポリモフィズム
「異なるものを同じ様に扱えること」
例えば、ガソリンで動く自動車と電気自動車では動作原理が全く異なりますが、アクセルを踏めば同じ様に動きます。
何故か?
"乗用車"という共通規格を両者が継承しているからです。
「継承」はポリモフィズムを実現するために使います。
3大要素と説明しましたが、実はオブジェクト指向で1番の肝はポリモフィズムです。「カプセル化」は手続き型だろうがオブジェクト指向だろうが、プログラミング手法に関係なく大事な要素です。「継承」はポリモフィズムを実現するために欲しい要素ですが、継承を使わずにポリモフィズムを実現することも可能です。
共通規格として、オブジェクトのインターフェースを揃えましょうというのがオブジェクト指向の発想なんです。処理の共通化なら共通関数をつくればいいだけですからね。
プログラミングにおけるポリモフィズム
インターフェース(or クラス)の継承を使ってポリモフィズムを実現するには、2つの方法があります。
- 親クラスに処理を実装する→どの場合でも処理が同じ場合
- 親インターフェースで宣言し、子クラスで処理を実装→抽象的な意味は同じでも、実際の処理に差異がある場合
継承はインターフェースを揃えて処理を抽象化するために使います。処理が共通化されるのは実装を簡単にするためのおまけ的な機能であって、共通化が目的になってはいけません。
何度も言いますが処理の共通化がしたいなら関数として外に切り出してください。
なぜオブジェクト指向で書くのか?
よくある説明
- 変更に対して柔軟に対応するため
- 再利用性を高めるため
私の解釈
- シンプルなプログラムを書くため
再利用性というファンタジー
変更に対して柔軟なコードを書きたい場合重要なのは、
- コード量を減らす
- 疎結合にする(クラス同士の結びつきを弱くする)
将来の変更まで完璧に予測して再利用されるクラスをつくるのは不可能であり、
無理に再利用すると炎上します。
現実正解とコンピュータは違う
よくあるオブジェクト指向の説明
- 現実世界はオブジェクト指向でモデリングできる→出来ません。現実世界ではありえない動作がたくさんあります
- クラスの継承を生物で例える(Animalクラスを継承するDogクラス、Catクラス等)→コンピュータと生物は全然違います
オブジェクト指向言語の入門書を見ると、動物に例える例がよく出てきますが、誤解のもとなので忘れましょう。
最初読むと「なるほど、理解したぞ!」と思いがちなんですが実は全然理解してないんですね…。この記事のようにオブジェクト指向を自分なりにはっきりと説明できるようになるまで私は何年もかかりました。
継承よりも委譲
「AはBである」(ClassA extends ClassB)が成り立たないが、クラスAの中でクラスBの機能を使いたい場合有効なのは以下の方法です。
- 委譲
クラスA内で"new ClassB()"して、使いたいメソッドだけ呼び出す方法 - 汎用的な関数に切り出す
両者の処理を、共通化した関数として再定義する方法
まとめ
- オブジェクト指向の明確な定義は存在しない
- オブジェクト指向の三大要素
- カプセル化
内部に複雑なものを閉じ込め、外部から見たときの挙動をシンプルにすること - 継承
インターフェース(抽象)に対してプログラミングするということ -
ポリモフィズム(多態性)
異なるものを同じ様に扱えること
- カプセル化
- オブジェクト指向を無理に例えない
参考文献
「オブジェクト指向と10年戦ってわかったこと」https://qiita.com/tutinoco/items/6952b01e5fc38914ec4e
「オブジェクト指向と20年戦ってわかったこと」 https://qiita.com/shibukawa/items/2698b980933367ad93b4
「オブジェクト指向と24年くらい戦ってわかったこととか」 https://qiita.com/Air_Hold/items/85c1794f3c42be481f21
「オブジェクト指向にdogやanimalを持ち込むと混乱する話」 https://qiita.com/tutinoco/items/8592f3432c5293b52566
Discussion