# 抽象化(ソフトウェア設計) ## 定義 抽象(abstraction)とは、実体の単純化された見方であり、重要でない細部を省いたものである。抽象は、複雑なものについて考え操作することを容易にするために有用である。モジュール型プログラミングでは、各モジュールがインターフェースという形で抽象を提供する。インターフェースはモジュールの機能の単純化された見方を示し、実装の詳細はそのモジュールの抽象という観点からは重要でないため、インターフェースから省かれる。 この定義において重要なのは「重要でない(unimportant)」という語である。抽象から省ける重要でない細部が多いほど、その抽象は優れている。しかし、ある細部を抽象から省けるのは、それが本当に重要でない場合に限られる。抽象は 2 つの方向で失敗しうる。第一に、本当は重要でない細部を含めてしまう失敗であり、これは抽象を必要以上に複雑にし、その抽象を使う開発者の認知負荷を増やす。第二に、本当は重要な細部を省いてしまう失敗であり、これは**不明瞭さ(obscurity)**を生む。抽象だけを見ている開発者は、それを正しく使うために必要な情報を欠くことになる。重要な細部を省いた抽象は**偽の抽象(false abstraction)**であり、見かけ上は単純でも実際には単純ではない。抽象を設計する鍵は、何が重要かを理解し、重要な情報量を最小化する設計を探すことにある。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]] §4.3) 本書はファイルシステムを例に挙げる。ファイルシステムの抽象は、ストレージデバイス上のどのブロックにデータを配置するかといった多くの細部を省くが、これは利用者にとって(十分な性能が提供される限り)重要でないためである。一方、多くのファイルシステムは性能向上のためにストレージへの書き込みを遅延させてデータをメインメモリにキャッシュするが、データベースのようなアプリケーションはシステムクラッシュ後もデータが保存されることを保証するため、データがいつストレージへ書き込まれるかを正確に知る必要がある。したがって、ストレージへのフラッシュに関する規則はファイルシステムのインターフェースにおいて可視でなければならない。抽象への依存は、プログラミングに限らず日常生活にも遍在する(電子レンジや自動車の操作インターフェースなど)。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]] §4.3) ## 層ごとに異なる抽象 第 7 章は、抽象が単一のモジュール内で成立する概念であることを超え、システムを構成する層(layer)の間の関係にも適用されることを示す。良い設計では、ある操作がメソッド呼び出しを通じて層を上下するたびに抽象が変化する。ファイルシステムなら「可変長バイト列のファイル」→「固定サイズのディスクブロックのキャッシュ」→「デバイスドライバ」、TCPなら「信頼性のあるバイトストリーム」→「ベストエフォートのパケット転送」のように、隣接する層は異なる抽象を提供すべきである。**隣接する層が似た抽象を持つことは、クラス分解に問題があることを示すレッドフラグである**。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]] 冒頭) この問題が最も具体的に現れるのが**パススルーメソッド(pass-through method)**である。パススルーメソッドとは、似た、あるいは同一のシグネチャを持つ別メソッドに引数をそのまま渡すだけで、それ以外はほとんど何もしないメソッドを指す。パススルーメソッドはインターフェースの複雑性を増やす一方でシステム全体の機能量を増やさないため、モジュールを浅くする。さらに呼び出し先のシグネチャが変わればパススルーメソッド側も追随して変更しなければならず、クラス間に不要な依存を生む。パススルーメソッドが多数見つかったら、それは関係する2クラスの間で責務がきれいに分割されていないことを示すシグナルであり、「それぞれのクラスは正確にどの機能・抽象に責任を持つのか」を問い直すべきである。解消手段は (1) 下位クラスを上位クラスの呼び出し元に直接公開し上位クラスから当該機能の責務を取り除く、(2) クラス間で機能を再配分する、(3) 両クラスを解きほぐせないなら統合する、の3通りである。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]] §7.1) ただし**インターフェースの重複は必ずしも悪ではない**。重要なのは各メソッドが有意な機能を追加しているかどうかであり、パススルーメソッドが問題なのは新しい機能を何も提供しないからにすぎない。正当化される例が2つある。第一に**ディスパッチャ(dispatcher)**で、引数を使って複数の候補メソッドから1つを選び呼び出すという有用な機能を提供する(例: Webサーバがリクエストの URL を見てファイル配信か PHP/JavaScript の実行かを選ぶ)。第二に**同一インターフェースを持つ複数実装**(オペレーティングシステムの複数のディスクドライバ等)で、各実装は異なる抽象を提供しており、あるドライバの使い方を覚えれば他のドライバも同じインターフェースで扱えるため認知負荷が下がる。この場合、抽象の同一性はレッドフラグではなく、むしろ利用者の学習コストを下げる意図的な設計である。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]] §7.2) **デコレータ(decorator)パターン**は、既存オブジェクトを受け取り似た、あるいは同一のAPIを提供しながら機能を拡張するラッパーオブジェクトを作る。Java I/O の `BufferedInputStream` が代表例である。著者はこのパターンが層をまたぐ API 重複を助長しやすいと警戒し、デコレータクラスはしばしば浅くなり、少量の新機能のために大量の定型コード(パススルーメソッド)を伴いがちだと述べる。デコレータパターンは新しい小機能ごとに新しいクラスを作りやすく、浅いクラスの氾濫(Java I/O のクラス構成が実例)を招く。著者はこのパターンについて「使いすぎている(easy to overuse)」という評価を明確に示し、デコレータを作る前に (1) 新機能が汎用的・論理的に関連するなら既存クラスに直接統合する、(2) 特定用途向けなら利用側クラスに統合する、(3) 新規デコレータではなく既存デコレータに機能を統合する、(4) そもそもラップする必要があるか(スタンドアロンのクラスにできないか)を問い直す、という代替案の検討を推奨する。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]] §7.3) 同じ「層が違えば抽象も違う」原則は、クラス単体のインターフェースと実装の関係にも適用される。両者が似た抽象を持つなら、そのクラスは深くない可能性が高い。第6章のテキストエディタ事例で、テキストを内部的に行単位で管理しつつ `getLine` / `putLine` のような行指向APIをそのままインターフェースにしたチームは、呼び出し側に行の分割・結合の実装を強い、クラスを浅くしてしまった。代わりに任意位置への `insert` / 任意範囲の `delete` という文字指向インターフェースを提供し内部表現は行のまま維持したチームでは、複雑性がクラス内部にカプセル化され、インターフェースが内部の記憶機構と大きく異なること自体がそのクラスの提供価値になった。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]] §7.4) ## 抽象の定義を変えることで複雑性を消す(第10章) 第4章・第7章がそれぞれ「抽象の質」「抽象の層間差異」を論じるのに対し、第10章はまったく異なる角度から抽象を扱う。すなわち、**抽象の意味論そのものを変えることで、複雑性の発生源(エラー)を丸ごと消せる**、という主張である。エラーを存在しないものとして定義するとは、実装を工夫してエラー処理コードを書きやすくすることではない。インターフェースが提供する抽象の意味論を再定義し、かつてエラーだった状況を正常な振る舞いの範囲に含めてしまうことである。これは抽象の設計自由度(「何を重要として見せ、何を見せないか」を決める権限)を使って問題そのものを消去する手法であり、第4章の定義でいえば「エラーという細部を、そもそも利用者に見せる必要のない非重要な細部に作り替える」操作にあたる。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] §10.3) この対比が最も鮮明に現れるのが Unix の `unlink` と Java の `substring` の比較である。Unix の `unlink` は、使用中のファイルを削除する際にディレクトリからエントリだけを外し、実データはオープン中の全プロセスが閉じるまで保持するという意味論を採る。これは「使用中のファイルは削除できない」という状況をエラーとして利用者に突きつけるのではなく、削除中のファイルの扱いを抽象の意味論そのものに組み込むことで、使用中ファイルの削除というケースを正常系に取り込んでいる。対照的に Java の `String.substring` は範囲外のインデックスを `IndexOutOfBoundsException` という例外的な細部として利用者に露出させる設計を選んでおり、これは第4章の分類でいえば「本当は重要でない細部(範囲外という状況)を、重要なもの(例外)として抽象に含めてしまった」失敗の一種と位置づけられる。両者は同じ「範囲外アクセス」という状況に対して異なる意味論設計を選んでおり、その違いが一方を深いインターフェース、他方をより浅いインターフェースにしている。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] §10.4, §10.5) ## 横断的知見 - 第4章はモジュール単体での抽象の質(重要でない細部を省けているか)を論じるのに対し、第7章はその抽象が層をまたいで反復されていないかという、モジュール間の関係に視点を広げる。第4章の基準(重要な細部を漏らさず、重要でない細部を最大限省く)を個々の層に当てはめても、隣接層が同じ基準の下で同じ抽象に収束してしまえば、それは抽象としての質の問題ではなくクラス分解の問題として第7章のレッドフラグに現れる。両章は抽象の評価軸が異なる次元(モジュール内在的な質 対 層間の差異化)であることを示している。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]]) - 第4章が提起した「抽象が省いた細部を『重要』と判断すべきかどうかの境界線」という問いに対し、第10章は具体的な判断軸を1つ与える。すなわち、ある細部(エラーになりうる状況)が「その情報がモジュール外で本当に必要とされるか」で重要性を判定し、不要ならインターフェースの意味論を変えて非重要な(=正常系に含まれる)細部に格下げできる、というものである。ただし第10章自身がこの操作の限界(§10.10「やりすぎ」)を認めており、境界線の判断基準は「エラーとして残すか消すかは常に安全にどちらかへ倒せる」という単純な話ではなく、両章を通じても文脈依存性が完全には解消されていない。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]]) ## 未解決の問い - 「重要でない細部」かどうかを判断する基準は文脈依存であり、第4章では原則の提示にとどまる。第7章は同一シグネチャの重複が正当化される2例(ディスパッチャ、複数実装)を示したが、それ以外のケースで「層ごとに抽象が異なるべきか、同一でよいか」を判断する一般的な基準は本書内でまだ与えられていない。 - 偽の抽象(false abstraction)を設計時に検出する具体的な手法は本章では示されていない。レビューや設計プロセス上でどう検出するかは未解決。 - デコレータパターンについて著者は「使いすぎ」と評価し4つの代替案を挙げるが、代替案同士のどれを選ぶべきかを決める基準(新機能の汎用性以外の判断材料)は §7.3 の範囲では明確化されていない。 - 第10章はエラーを存在しないものとして定義する手法の限界を「その情報がモジュール外で必要か」で線引きするが、この判定を設計時にどう下すか(レビュープロセスや設計基準)は本書内でまだ具体化されていない。抽象の意味論変更によってエラーを消す手法と、偽の抽象(重要な細部を誤って省く失敗)との境界はどこにあるのか、という問いは未解決のまま残る。 ## 関連 - ソース: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]] / [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 7 Different Layer, Different Abstraction]] / [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] - 概念: [[深いモジュール]] / [[情報隠蔽]] / [[例外処理の設計]] - 実体: [[John Ousterhout]] ## 出典 - John Ousterhout, *A Philosophy of Software Design*, Yaknyam Press, 2018, Chapter 4. - John Ousterhout, *A Philosophy of Software Design*, Yaknyam Press, 2018, Chapter 7. - John Ousterhout, *A Philosophy of Software Design*, Yaknyam Press, 2018, Chapter 10.