抽象化 (計算機科学)
抽象化(ちゅうしょうか、Abstraction)は、計算機科学において詳細を捨象し、一度に注目すべき概念を減らすことおよびその仕組みである。
この概念は数学における「抽象化」からのアナロジーである。数学での抽象化技法の起源は数学的定義である。例えば、コンピュータでも数学でも、数はプログラミング言語上の概念であり、数学上の概念でもある。数の計算概念は数学の概念に基づいているため、実装の詳細はハードウェアとソフトウェアに依存したとしても、それが制約とはならない。
大まかに言えば、抽象化は制御抽象化とデータ抽象化に分けられる。制御抽象化は動作の抽象化であり、データ抽象化はデータ構造の抽象化である。例えば、構造化プログラミングでの制御抽象化とは、サブプログラムや定式化された制御フローの使用を意味する。データ抽象化とは、本来ビット列であるデータを意味のある方法で扱うことを意味する。例えば、データ型の背景にある動機は抽象化である。オブジェクト指向プログラミングはデータとコードを同時に抽象化する試みと見ることもできる。
コンピューティングの大部分は実世界とは独立している。ハードウェアは計算モデルを実装したものであり、他のモデルとの入れ替えが可能である。ソフトウェアはソフトウェアアーキテクチャで構築され、人間が一時に一部の問題に集中することで巨大なシステムを生み出すことを可能にしている。このようなアーキテクチャは特定の抽象化を選択して利用している。グリーンスパンの10番目の規則は、そのようなアーキテクチャがいかに必然的で複雑であるかを示す格言である。
コンピューティングでの主な抽象化は言語の抽象化である。新たな人工言語はシステムの特定の観点を表現するために開発される。モデリング言語は計画立案を補助する。コンピュータ言語はコンピュータで処理できる。このような抽象化プロセスの例として、プログラミング言語の世代的開発が挙げられる。各世代は次の世代の基盤となった。言語の抽象化は現在も続いており、例えばスクリプト言語やドメイン固有言語でこの進化が著しい。
プログラミング言語では、一部機能によってプログラマが新たな抽象化を生み出せるようにしている。例えば、サブルーチン、モジュール、ソフトウェアコンポーネントなどがそれである。プログラミング言語自体の機能ではないが、設計技法上の抽象化としてデザインパターンやソフトウェアアーキテクチャがある。
抽象化によっては、次々に構築される概念を完全に隠蔽することでプログラマが把握しなければならない概念の幅を制限しようとする。Joel Spolskyは、あらゆる抽象化は破綻しやすいと主張して批判した。つまり、抽象化によって下部構造が完全に隠蔽できたためしがないというのである。一部の抽象化は他との相互のやりとりのために設計されている。例えばプログラミング言語には外部関数インタフェースを持つものもあり、低レベルな言語を呼び出すことができる。
プログラミング言語は、その言語の応用分野によってそれぞれ異なる抽象化を行う。例えば、次のような抽象化がある:
オブジェクト指向プログラミング言語(C++、Javaなど)での抽象化の概念は、必要に応じて virtual や abstract といったキーワードを使って宣言することである。このような宣言をした後、プログラマはその宣言に沿ってオブジェクトをインスタンス化するためにクラスを実装しなければならない。
関数型言語では、関数に関連した抽象化が一般的である。例えばラムダ計算、高階関数などである。
Lindaという言語では「サーバ」や「共有データ空間」といった概念を抽象化し、分散プログラミングを実現する。
仕様記述言語は一般に何らかの抽象化に基づいている。仕様はプロジェクトの初期に定義されるもので、最も抽象的なレベルであり、それが最終的に実装される。例えば仕様記述言語であるUMLは「抽象」クラスを定義でき、プロジェクトの仕様設計段階ではそれらは「抽象」のままである。
制御抽象化はプログラミング言語を使う主たる目的の1つである。コンピュータが理解する操作は極めて低レベルであり、メモリのある場所から別の場所へ何ビットかを移動させ、2つのビット列を加算するといったことでしかない。プログラミング言語を使うことでこれをもっと高いレベルに変換する。例えば、次のようなプログラム内の文(式)があるとする。
a := (1 + 2) * 5
人間にとっては、これは非常に単純で明らかな計算である(1 足す 2 は 3、これに 5 をかけて 15)。しかし、これを評価するには低レベルの実行ステップに落とし込まねばならないし、計算結果である 15 を変数 "a" に代入するという作業も複雑である。数値は二進数表現に変換され(これも大方の予想よりも複雑な作業である)、(コンパイラやインタプリタが)計算をステップに分解して機械語の命令列に直す(機械語あるいはアセンブリ言語は一般のプログラマにとっては直観的に理解可能なものではなく、その内容は通常の加算や乗算といった人間の考える算術とは趣きが異なる)。そして、計算結果の "15" を "a" というラベルの付いた変数に格納するが、実際には物理メモリか仮想メモリ上のあるアドレスのメモリ位置がそれに対応しており……などなどである。
制御抽象化なしでは、プログラマは機械語レベルでレジスタやメモリアドレスを指定してプログラムを書かねばならない。その場合2つの深刻な結果を招く。第1に似たような機能を毎回コーディングしなおさなくてはならなくなる。第2にプログラマは特定のハードウェアや命令セット向けにプログラムを書くしかなくなる。
構造化プログラミングでは、複雑なプログラム作業を小さい部分に分割し、コンポーネント間に明確なインタフェースと制御フローを導入し、副作用的に複雑さを低減させる。
単純なプログラムでは、ループからの脱出点を明示的に1つにするようコーディングするとか、関数や手続きからの脱出点を1つにするといった工夫である。
大きなシステムでは、複雑なタスクを多数のモジュールに分割することになるだろう。例えば、港湾事務所での船員への給与支払い業務システムを考えてみよう。
最も高いレベルには、典型的なエンドユーザー操作のメニューがある。
その下に、船員の新規採用や退職、小切手の印刷といった作業のための独立した実行ファイルやライブラリがある。
各独立コンポーネントは多数のソースファイルから構成され、それぞれのファイルには問題の一部に対応するプログラムコードが含まれ、他のプログラムとある決まったインタフェースを持つよう設計される。新規採用プログラムには、データを画面表示するプログラムやデータベースとのインタフェースが含まれるだろう(それらは独立したサードパーティーライブラリや静的にリンクされたライブラリルーチン群かもしれない)。
また、港湾と船の間でデータを交換する処理が必要とされる場合も考えられ、その場合はさらに様々なコンポーネントを必要とする。
このような階層によってコンポーネント毎に実装の詳細を隔離する効果が生まれる。そして、その考え方を重視し、拡張して生まれたのがオブジェクト指向プログラミングである。
データ抽象化とは、データ型の「抽象的」属性と「具体的」実装詳細の明確な分離を強制することである。抽象化された属性はデータ型(あるいはそのインタフェース)を利用するクライアントコードとして明確化され、具体的実装は完全にプライベートな状態でかつ必要に応じて変更できる形で存在する。概念的には、そのような変更は抽象的振る舞いには変化をもたらさないので、クライアントコードには全く影響を与えない。
例えば、「参照テーブル」という抽象データ型を定義したとする。参照テーブルには「キー」とユニークに対応する「値」があり、キーを指定することで値を操作できる。このような参照テーブルの実装方法はハッシュテーブルや2分探索木や線形リストなどいくつかある。クライアントコードからすれば、データ型の抽象化された属性はどの場合でも同じである。
もちろん、以上の話は最初にインタフェースを正しく詳細化することにかかっており、そうでないと実装の変更がクライアントコードに影響を及ぼしてしまう。別の見方をすれば、インタフェースをデータ型とクライアントコードの間で合意された振る舞いの「契約」を形成すると考えることもできる。契約にない部分は予告なく変更される可能性がある、というわけである。
データ抽象化を実装した言語としてはAdaやModula-2がある。オブジェクト指向プログラミング言語も一般にデータ抽象化を提供すると言われているが、継承の概念によって実装側の情報がインタフェース側に持ち出される傾向がある。したがって、継承関係の変更はクライアントコードに影響を与えることがあり、脆弱な基底クラス問題につながる。
オブジェクト指向プログラミングの理論では、抽象化とは抽象的「アクター」であるオブジェクトを定義する機能であり、アクターは作業を行い、状態を変化させ、状態を報告し、システム内の他のオブジェクトと「通信」する。カプセル化は、状態の詳細を隠蔽することを意味する用語だが、従来のプログラミング言語でのデータ型の概念の拡張である。カプセル化はデータに強く結びついた「振る舞い」をそのデータと関連付け、他のデータ型との相互作用を標準化し、抽象化の起点となる。抽象化をさらに進めて、異なる型のオブジェクト間で同じ操作を定義することをポリモーフィズムと呼ぶ。また、逆方向に抽象化を進め、データ型やクラスの内部を抽象化してそれらの複雑な関係を単純化し構造化することを委譲または継承と呼ぶ。
オブジェクト指向言語は様々あるが、似たような抽象化手法を提供している。ポリモーフィズムはほぼ全てのオブジェクト指向言語でサポートされており、類似あるいは同じ役割を持つデータ型の置換なども含まれる。それほど一般的ではないが、構成・イメージ・パッケージによってコンパイル時/リンク時/ロード時にオブジェクト間の関係を決定することがあり、この場合実行時に関係を決定する必要性がほとんどなくなる。
CLOSやSelfなどの言語では、クラスとインスタンスをあまり区別せず、ポリモーフィズムの実現に委譲をよく使う。また、個々のオブジェクトや関数はより柔軟に抽象化され、LISPの機能により適するようになっている。
C++では、テンプレート、演算子オーバーロード、その他のコンパイル時の静的バインディングなどが特徴であり、これらがC++固有の柔軟性に関する問題を生んでいる。
同じ抽象化にも様々な戦略があるが、基本的に抽象名詞をコード内でサポートする必要性に違いはない。いずれのプログラミング言語も動詞を関数として、名詞をデータ構造として、そして両者を合わせてプロセスとして抽象化する機能に基づいている。
例えば、以下のサンプルコードはJavaによる動物の抽象的表現である。ここでは、飢えと食事という観点で抽象化している。Animalクラスは動物の状態と機能を表現するよう定義されている。
この定義で、Animal型のオブジェクトを生成し、以下のようにメソッドを呼び出すことができる:
この例では、Animalクラスが動物を抽象化したもので、LivingThing は Animal よりさらに高い抽象度の抽象化(この場合汎化)である。
もっと違った抽象化も考えられる。例えば中間的な抽象化として、毎日ミルクを生み出してくれる動物と、最後に肉となってくれるだけの動物とに分類することができる。DailyAnimal (雌牛や山羊)はミルクを生み出すのに適した餌を与えられ、Animal (豚や雄牛)は肉の質を高める餌を与えられる。
このような抽象化をすれば、アプリケーションを書く人は餌の種類を指定する必要がなくなり、給餌スケジュールに集中することができるようになる。この2つのクラスは継承関係であっても、全く独立していてもよく、それによってポリモーフィズムの度合いも変わってくる。このような機能は言語によって大きく違うが、大体においてある言語でできることは他の言語でも可能である。演算子オーバーロードや抽象データ型を多用することで、継承や他のポリモーフィズムを実現する手法と同様の効果が得られる。クラスを使った記法は単にコードを書くものの利便性のために存在するに過ぎない。
何を抽象化して、何をコードを書く者の制御下に置くかの判断は、オブジェクト指向設計とドメイン分析の主要テーマである。実世界での適切な関係を見極めることは、オブジェクト指向分析のテーマでもある。
一般に、適切な抽象化を決定するには、範囲/ドメイン分析/連携すべきシステムの決定/様々な制約の分析など、様々な判断を必要とする。そして、オブジェクト指向分析をプロジェクトの外的条件(時間と費用)を考慮して行う。上の単純な例では、ドメインとは庭であり、豚や牛やそれらの食習慣は制約である。詳細な解析により、コード作成者は利用可能なものなら何でも餌として与える柔軟性を持つ必要があると判断する。そのため、クラス自体に餌の型をコード化する必要がないと判断され、結果として豚であっても雌牛であっても同じ Animal クラスとなる。DairyAnimal を別のクラスとする判断を下した場合には詳細は変化するが、ドメインや制約は変わらない。従って全てはプログラマの制御下にある。オブジェクト指向プログラミングでの抽象化と、ドメインや制約の抽象化は区別されている。
プログラム意味論、形式手法、抽象解釈などを論じるとき、抽象化とは観測されたプログラムの動作の定義をより大雑把にかつ安全に検討する作業を意味する。例えば、実行途中の全段階を追うことなく結果だけを検討する場合などである。抽象化とは具体的実行モデルと反対の概念として定義される。
抽象化は、属性が具体的モデルと同じとなるよう属性に関して「正確」または「忠実」でなければならない。例えば、加減算と乗算だけを使った式が n で割り切れるかを知りたければ、n で割り切れる値を全て計算すればよい(この抽象化は九去法と類似の形式である)。
抽象化は必ずしも「正確」である必要はないが、「健全」であることを要求されることもある。つまり、決定不能な結果を生むとしても、抽象化から正当な結果が得られるべきだという意味である。例えば、クラスの生徒たちを年齢の最大と最小で抽象化した場合、ある人物がそのクラスに属するかどうかを判定するためにその年齢を最大値・最小値と比較することが考えられ、年齢がその範囲外だった場合は、その人物がクラスに属さないと確信を持って言うことができる。範囲内だった場合、「わからない」としか答えようがない。
コンピュータプログラムの特性は本質的に決定不能であるため、抽象化はコンピュータプログラムを扱う場合に有効である(ライスの定理参照)。同様にプログラムの動作によって情報を引き出す自動化手法は、停止性(場合によっては失敗したり、いつまでたっても結果が出ないことがある)、健全性(誤った情報を提供することがある)、正確性(問題に対して答えられない場合がある)のいずれかを諦めざるをえない場合がある。
抽象化は抽象解釈の中核的概念である。モデル検査は一般に対象システムを抽象化したものに対して行われる。
計算機科学における「抽象化レベル」または「抽象化層」の概念では、各レベルは同じ情報やプロセスの異なったモデルとなっているが、特定領域に適用可能なオブジェクトや構成のユニークな集合に関する表現体系を使う。レベルの高いほうはより抽象的であり、レベルが下るに従ってより細かく詳細になっていく。例えば、電子回路による論理ゲート、論理ゲート上のバイナリ、バイナリ上の機械語、機械語上のプログラミング言語、プログラミング言語上のアプリケーションソフトウェアやオペレーティングシステムといった階層である。各レベルには実体があるが、ある程度自己完結的な言語とすることで、下位レベルに完全に依存しないようにできる。
データベースシステムのユーザーの多くは、コンピュータのデータ構造には詳しくない場合が多いので、データベース開発者は以下のような階層化によって複雑さを隠蔽することが多い。
最も低レベルの抽象化であり、データが実際にどのように格納されるかを記述する。物理レベルでは低レベルで複雑なデータ構造の詳細が記述される。
次の抽象化レベルではデータベースに格納されているデータが「何」であるかを記述し、それらのデータ間にどのような関係があるかを記述する。従って、論理レベルでは物理レベルよりも単純化された構造でデータベース全体が記述される。論理レベルの単純な構造の実装には物理レベルの複雑な構造が必要となるが、論理レベルのユーザーにはそのような複雑さは無関係である。データベース管理者はデータベースに格納すべき情報を選別する責任があり、論理レベルの抽象化を利用する。
最も高いレベルの抽象化はデータベースの一部だけを記述する。論理レベルの構造はある程度単純化されているとしても、巨大なデータベースに格納されるデータの多様性のためにある程度の複雑さが残存している。データベース利用者の多くはデータベース内の全情報を必ずしも必要としない。むしろ一部のデータだけにアクセスすることが多い。ビューレベルの抽象化により、利用者とシステム間のやりとりが単純化される。システムは1つのデータベースについて複数のビューを提供する。
異なったレベルの抽象化による設計は次のことを提供する。
これはシステム設計やビジネスプロセス設計に使われる。一部のモデリング言語は複数の抽象化レベルを含む設計を生成する。