OOP用語ひとめぐり
「素人でもわかりやすい」かどうか、素人には分からない。- うっくつさん本を読む。さんに なるほど、と思いました。
「わかりやすい」とは「素人でもわかりやすい」の意。
「わかりやすい説明」を求める人はその分野に関して素人である。これは「素人」という言葉の定義に由来するものだ。「素人」である以上、その対象を厳密に理解することはできない。だから「わかりやすい説明」は掻い摘んだ概要説明となる。
(中略)
「素人でもわかりやすい」かどうか、素人には分からない。- うっくつさん本を読む。「素人でもわかりやすい」かどうか、素人には分からない。
これが最大の問題だ。しばしば言われるのとは逆だが、素人には自分が正しく読み取れたかどうか判断できないのだから、同語反復と言っても良いほどに正しい。
そのため、素人が言うところの「素人でもわかりやすい」は「読みやすい」に過ぎない。
ならば、ただ読みやすさだけを求めた OOP についてのエントリだって、全く意味のないことではないのかな?、と思いました。そんなわけで 試しに作ってみたエントリ。
* * *
日本語では、抽象とは捨象の裏返し。何かを抜き出し何かを捨てる。英語の Abstraction の方は、もうすこし具体的で「特定の例や現実の出来事よりも、概念や原則に基づいているもの」を拾い上げます。プログラミング用語の 抽象化は これの訳語なので、コッチのニュアンスに近いんじゃないかな・・と思ってます。
で、共通化とは、共通であるモノを拾い上げ、共通でないモノを捨てる抽象化の一種です。「共通」というのは「特定の例や現実の出来事」に依らない所を判断する方法としては なかなかイけているのです。が、「プログラムの抽象化」の文脈では、共通化は手段であることに注意です*1。
プログラムを抽象化する動機は、プログラムが大きすぎること&人間の脳が大きい物が苦手だということ。
脳は 一説には一時記憶メモリが8つ前後しかない、なんて言われているスペックの低いコンピュータです。逆にそこがボトルネックになっているだけなので、たとえばソロバンのように、そのネックを解消(一時記憶を脳から外だし)すれば真のポテンシャルを発揮できちゃうのも人の脳の特徴です。
プログラミングにおいて、プログラムの具体的処理を全て考えると量が多すぎて頭がパンクしちゃうなら、大事なところだけを残して後を捨てる(というか後述するカプセルの内側に隠す)ことで数を減らせば、ソロバンと同じ理屈で 人間のプログラム把握能力を飛躍的に上げる事が出来ます。一言で言えば、見た目の規模を小さくする構造を持たせること。これがプログラミングと抽象化が切っても切れない理由です。
プログラミングにおける「抽象化」には、いくつか手段がありますが、メインとなるのは何かの箱(たとえばクラスとかサブルーチンとか)に プログラム片を封じ、その概念を示す名前をつけることです。このような箱に詰める抽象化手法を、カプセル化と言います*2。(というわけで、ここから先は「箱」を「カプセル」に置換)
カプセルにプログラムを詰めるとき、
の二つを選びます。まんま抽象と捨象ですね。と、言うことは、カプセルそのものが抽象であり、その内側に満ちるモノも外側に満ちるモノも(そのカプセル化行為から見れば)捨象したことになります。
せっかくのカプセルだけれど、「この処理は何を行うのか」について、いちいちカプセルを開けて確かめないと解らないのでは、抽象化した意味が無いので、中を見なくても名前を見ればズバッと それだけで、何を行うか解るようにするのがとても重要です。名前重要。大切です。
さて、カプセルの外側に晒らけ出したカプセル自体の膜面、「抽象」な方の顔を インターフェイスと呼びます。そして、カプセルで閉じこめられた物のことを(カプセル込みで)モジュールと呼びます*5。
誰かがモジュールを利用するときは、そのインターフェイスを介してしか利用しません(だってカプセルの中を開けちゃったら、「見えるモノを減らしたい」という抽象化した目的が台無しですもの)。そこで 誰かさんが利用したいインターフェイスと、利用されるモジュールの持つインターフェイスが一致することをチェックすれば、その使う-使われる関係(モジュールの接合)が正しいことを見極めることが出来ます。これが OOP における 型チェックです。ということは、インターフェイスは型そのものってこと!*6
また、使いたいインターフェイスが同じであれば、違うモジュールであっても差し替え可能です。こういう差し替え可能性のことをポリモーフィズムと言います。
共通化の観点からポリモーフィズムを見るとちょっと面白いのです。普通のモジュールは、使用コード(サブルーチン風に言えば「呼び出し先」)の共通化ですが、ポリモーフィズムを用いると、使用元コード(サブルーチン風に言えば「呼び出し元」)の共通化が可能になります。共通化は抽象化です。ポリモーフィズムを使うコトにより、ある類似コードから共通でない部分をカプセルでくるんで捨象することができ、それにより捨象した部分の使用元をも抽象化できます*7。
これはサブルーチン化における非共通部分の引数への括りだし(捨象)と似たものですが、データだけでなくコードも括りだし出来るのは強力です。具体的には、例えば条件分岐(if-else や switch みたいなの)の排除が可能になり、コードの可読性が上がり(見た目のコード規模が小さくなるから)、また保守性の高いコードを作ることが可能になります。
さて、ようやくクラスの登場です。クラスは OOP での モジュールです。しかしクラスは同時に型(インターフェイス)でもあります。大事なのでもう一度言うよ。クラスとは「モジュール」であり「型」であるです。この両面性は一見便利そうに見えますし、OOP 黎明期には、これぞOOPの神髄・・みたいな空気もあったのですが、今時の見解としては、「便利」以上に「厄介で危険」です。
この問題が解りやすく表面化するのが継承です。継承は、クラスを型として見たときは特殊化です。これを型継承と言います。クラスをモジュールとしてみたときは拡張です。これを実装継承と言います。また、差分プログラミングは実装継承を指します。
クラスを型と見るか、またはモジュールと見るかによってすべては決まる。型として見る場合、継承はis-a(……は……の一種である)という関係であり、明らかに特殊化である。"犬"は"動物"よりも特殊な概念であり、"長方形"は"多角形"よりも特殊化されている。この関係はすでに述べた部分集合の関係に対応する。(中略)
一方、モジュールという点から見ると、クラスはサービスの提供者としてとらえることができる。BはAのサービス(特性:feature)に独自の特性を追加したサービスを提供する。したがって、実現される特性という点で考えると、この関係は逆の部分集合となる。Aのインスタンスに適用される特性はBのインスタンスに適用され得る特性の部分集合である。したがって型という点から見ると、継承は特殊化であり、モジュールという点から見ると拡張である。
オブジェクト指向入門 (ASCII SOFTWARE SCIENCE Programming Paradigm) - はてなキーワード
継承は当初「概念が特殊化されれば、同時にその機能は拡張される」みたいに、型継承と実装継承が両方が同時に自然に成り立つと思われていました。だけれど、やってみたらそうじゃなかった。再利用性に乏しく、メンテナンスが大変なコードになってしまいました*8。ちよちゃん風に言えば「この生き方はダメです」をみんな身をもって知った感じ。
今時のOOPにおいては、継承は極力 型継承が必要なとき(= ポリモーフィズムの型チェックのとき、同一インタフェイスを継承しているかで チェックする)のみに用いるようにして、実装継承(差分プログラミング)は極力使わない、それをしたいときは 対象モジュールを持ち、それに処理を委譲するようにしよう、と言う風になっています。(なので、Java は 型だけを切り離した「インターフェイス」を クラスとは別に用意しています。)
このようにプログラムをモジュールに区分けるには、プログラミング特有のノウハウが必要です。他にも開放/閉鎖原則 ですとか、大事な原理・原則はいっぱいあるのですが、ここから先は、掻い摘み説明ではちょっと難しいので割愛です。
最後に老婆心ながら、クラスは非常に強力な概念なので、プログラミングの枠を超えて OO周辺にいろいろ飛び火しています。集合論的に捉えたり、哲学にしちゃったり。それ自体は良いのですが、それがプログラミングに紐付けしようとされるとき、そこで語っているクラスって、型的?モジュール的? どっちなの?(それとも両方?)・・・くらいは、注意して読んだほうがいいかもです。クラスにたとえ話を持ち込むとき、しばしばここら辺がいいようにボカされて「なるほど」と騙されることがあります。
* * *
おうし座とぎょしゃ座 とか ペガサス座とアンドロメダ座 のように、OOP関連の用語は端と端が一部共用になっている、そんなイメージがあったので、これを利用して、OOP用語しりとり のようなエントリを作ってみました。試験勉強みたいに、「オブジェクト指向とは、『カプセル化』『継承』『ポリモーフィズム』である」とか覚えやすいキーワードで覚えて、それぞれのキーワードで言葉遊びするよりは、まだ素人の為になるかしら、とも思います。
ただ、これを読んでも OOP でプログラムを作れるようにはなりません。そこらへんは「オブジェクト指向でなぜつくるのか」と五十歩百歩です。あと、用語の都合の良い面だけを拾い上げている(たとえば「カプセル化」あたりとか)ので、正確ではないですし、それが嘘の域に達しているものもあるかも。(ポリモーフィズムのあたりは、少し勇み足でおかしなコトを書いているような気もします)
*1:抽象化の手段じゃない「共通化」の例としては「同一コードが複数あったとき、拡張・修正の際、全部にやらなくちゃならない」(開放閉鎖原則にそうモジュール分割)というのがありますが、分けて考えないと混乱するので注意です。
*2:もっとも、カプセル化という用語も混乱の極みにあるので、この説明はなんとも微妙です。でも「好きに使えばいいと思うよ」とお墨付きをもらったので好きに使っちゃいます
*3:例:関数化の際、引数に括りだす=外に捨てる
*4:例:関数の中に隠すコードやローカル変数=内側に隠す / 関数のシグネチャ(名前+引数の型)=表面に晒す
*5:C言語界隈の「モジュール」(翻訳単位を一つのカプセルに見立てるプログラミング手法)と混同しないように、注意です
*6:なので、データ(内部表現)を「手続きの集合」というインターフェイスのカプセルに詰めて抽象化したものを 抽象データ型 と呼びます。で、こういうのが狭義の「カプセル化」(即ちデータ抽象)と呼ばれています。
*7:ポルナレフ風に言えば「あ…ありのまま 今 起こった事を話すぜ!『カプセルにくるんで内側を抽象化したと思ったら、カプセルの外側も抽象化されてた』 な… 何を言ってるのか わからねーと思うが(ry」、かな?