# 深いモジュール ## 定義 モジュールとは、インターフェース(interface)と実装(implementation)を持つコードの単位であり、クラス・メソッド/関数・サブシステム・サービスなど形態を問わない。インターフェースは他のモジュールで作業する開発者が当該モジュールを使うために知る必要のある情報全体、実装はインターフェースが約束した機能を果たすコードである。モジュールはこの 2 面から捉えられる。インターフェースはモジュールが系全体に課す**コスト**、実装が提供する機能は**利益**であり、良いモジュールとは利益が大きくコストが小さいものである。 **深いモジュール(deep module)**とは、強力な機能を持ちながらインターフェースが単純なモジュールを指す。モジュールを長方形に見立てたとき、面積が機能量、上辺の長さがインターフェースの複雑性を表すとすれば、深いモジュールは面積が大きく上辺が短い長方形になる。単純なインターフェースの背後に大きな内部複雑性を隠しているため、良い抽象を提供する。対する**浅いモジュール(shallow module)**は、提供する機能に見合わないほどインターフェースが複雑なモジュールであり、複雑性との戦いにほとんど寄与しない(小さいモジュールは浅くなりがちである)。 Unix のファイル I/O は深いモジュールの好例として挙げられる。`open` / `read` / `write` / `lseek` / `close` という単純なシグネチャの 5 個のシステムコールのみが公開インターフェースであるのに対し、実装はディスク上のファイル表現・ディレクトリ探索・権限管理・キャッシュ・スケジューリングなどを含む数十万行規模で、これらは呼び出し側から一切見えない。対照的に、Java でシリアライズされたオブジェクトをファイルから読むには `FileInputStream` / `BufferedInputStream` / `ObjectInputStream` という 3 個のオブジェクトを個別に生成しなければならず、バッファリングの付け忘れという罠まである。この対比は、「クラス病(classitis)」——「クラスは良いものだから、より多くのクラスがより良い」という誤った通念——が浅いモジュールの乱立を招く典型例として提示される。インターフェース設計の指針としては、最も一般的な使い方が最も単純になるようにすべきである(Unix システムコールはシーケンシャル I/O をデフォルトにし、ランダムアクセスが必要なときだけ `lseek` を使わせる)。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]]) ## 汎用性と深さの関係 新しいモジュールを汎用(general-purpose)で作るか特定用途向け(special-purpose)で作るかは設計上の頻出の岐路である。本書がとる立場は「やや汎用寄り(somewhat general-purpose)」で、完全な汎用でも純粋な特定用途向けでもなく、モジュールの機能(functionality)は現在の必要を反映してよいが、インターフェースだけは複数の用途を支えられる程度に汎用化する、というものである。「somewhat」が重要であり、現在の必要に対して使いにくくなるほど汎用に走ってはならない。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 6 General-Purpose Modules are Deeper]] §6.1) 汎用化が深いモジュールを生む機構は次のとおりである。インターフェースを汎用にすると、個々の特定用途向けメソッドを1つの汎用メソッドに統合できるためインターフェース自体は単純になる一方、汎用メソッドは複数の用途を1つの実装でまかなう必要があるぶん実装は複雑になりうる。しかしこのトレードオフは総体として有利であり、上辺(インターフェースの複雑性)が短くなる度合いのほうが実装の複雑性の増分より大きいため、結果としてモジュールはより深くなる。学生プロジェクトのテキストエディタ事例が具体的にこれを示す。UI操作に一対一対応する特定用途向けメソッド群(`backspace`/`delete`/`deleteSelection`)を、`insert`/`delete`という2個の汎用メソッド(`Position` 型の任意区間を対象にする)へ置き換えたところ、個々のUI機能を呼び出し側で組み立てるコードはわずかに長くなったものの、特定用途向けメソッドの乱立が解消されてシステム全体のコード量はむしろ減り、しかもUI側の認知負荷も下がった。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 6 General-Purpose Modules are Deeper]] §6.2–§6.3) 汎用と特定用途向けを分離すべき理由は、汎用アプローチが将来の再利用可能性で得をするからではなく(それは副次的な利点にすぎない)、たとえモジュールが結局元の目的にしか使われなくても、汎用アプローチのほうが単純さの点で優れているからである。特定用途向けのAPIはUI側の設計判断(選択・カーソル・特定のキー操作)をテキストクラスに漏出させ、UIとテキストの両クラスを開発する側に高い認知負荷を課していた。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 6 General-Purpose Modules are Deeper]] §6.1–§6.2) 著者は汎用/特定用途向けのバランスを見極めるため、次の3つの自問を提示する。(1) 現在のすべての必要をカバーする最も単純なインターフェースは何か(メソッド数を減らせているなら汎用化が進んでいる可能性が高いが、引数を増やして数を減らしただけなら単純化になっていない)。(2) このメソッドは何回(いくつの状況で)使われるか(1つの用途にしか使われないメソッドは特定用途向けすぎる兆候)。(3) 現在の必要に対してこのAPIは使いやすいか(単一文字操作だけのAPIのように、汎用にしすぎて現在の用途で使いにくくなっていないか)。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 6 General-Purpose Modules are Deeper]] §6.5) ## 複雑性を下方に押し込む 深いモジュールを作るためのもう一つの視点が、**複雑性を下方に押し込む(pull complexity downwards)**という判断原則である。新しいモジュールの開発中に回避不能な複雑性を見つけたとき、それを利用者(上位)に負わせるか、モジュール開発者自身が内部で引き受けるかという選択に直面する。複雑性がそのモジュールの機能に関係するものであれば、後者を選ぶべきである。モジュールの利用者数は開発者数より多いのが通例であり、開発者が苦労するほうが利用者が苦労するより総コストが小さいからである。この原則は、設計原則6「モジュールにとっては実装が単純であることよりインターフェースが単純であることのほうが重要である」の直接の由来になっている。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]] ch.8 冒頭) 著者はこの原則を、開発者が陥りがちな逆方向の振る舞い——確信の持てない条件に対して安易に例外を投げて呼び出し側に処理を委ねる、方針が定まらないときに**設定パラメータ(configuration parameters)**を用意してシステム管理者に決定を委ねる——への批判として展開する。設定パラメータを外部に公開することは、複雑性を下方ではなく上方に押し上げる典型例だと位置づけられる。擁護論には一定の正当性があり、利用者が自分のドメインの事情(例: どのリクエストが時間的に重要か)を基盤コードより詳しく知っている状況では、パラメータ化によってより広い領域で高い性能が得られうる。しかし著者は、多くの場合、利用者や管理者にはパラメータの適切な値を決定することが困難または不可能であり、実装側にひと手間かければ値を自動的に決定できる場合も多いと批判する。具体例として、ネットワークプロトコルの再送タイムアウト値が挙げられる。応答が一定時間内に届かなければ再送する設計において、再送間隔を設定パラメータとして公開する代わりに、成功したリクエストの応答時間を計測しその倍数を再送間隔とすれば、トランスポートプロトコル自身が妥当な値を計算でき、動作条件の変化にも動的に追従できる(設定パラメータは時間が経つと実情に合わなくなりやすい)。したがって設定パラメータを公開する前には「利用者(または上位モジュール)は、ここで決定するよりも良い値を決定できるか」を自問すべきであり、公開する場合も妥当な既定値を自動計算できないかを先に検討すべきである。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]] ch.8 §8.2) 一方でこの原則には**やりすぎの危険**がある。押し込む複雑性が本当にそのモジュールの機能と密接に関係しているときにしか正当化されない。極端な例として、アプリケーション全体の機能を単一クラスへ押し込むのは明らかに理にかなわない。複雑性を下方に押し込むことが妥当なのは、(a) 押し込む複雑性がそのクラスの既存機能と密接に関係し、(b) 押し込むことで他の箇所に多くの単純化がもたらされ、(c) 押し込むことでそのクラスのインターフェースが単純化される、という3条件がそろう場合に限られる。無関係な複雑性まで引き受けると、モジュールは深くならずむしろ浅くなる。バックスペースキーの動作を実装するメソッドをテキストクラスに直接持たせる例(第6章)は、一見複雑性を下方に押し込んでいるようで、実際には上位コードをさほど単純化せず、しかもUI知識はテキストクラスの中核機能と無関係であるため、単純化ではなく情報漏出になっている。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]] ch.8 §8.3) ## 結合と分離の判断基準 深いモジュールを作る・保つための第3の視点が、「2つのコードを結合すべきか分離すべきか」という判断である。第4章の静的な2面構造(インターフェース/実装)、第6章の動的な機構(汎用化)、第8章の動的な判断規則(複雑性をどちらが引き受けるか)はいずれも1つのモジュールの内部設計を扱うのに対し、この判断基準は複数のモジュール(または関数・クラス・サービス)の間で機能をどう配分するかを扱う点で異なる階層の問いである。細分化(モジュールを増やすこと)は自動的に複雑性を減らすのではなく、コンポーネント数の増加・管理コードの追加・分離・重複という新たな複雑性を生むため、結合すべきか分離すべきかは常に検討を要する。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]] ch.9 冒頭) **結合したほうがよい兆候**は次の4つである。(1) 情報を共有している——両方のコードが同じデータ形式やプロトコルの詳細を知っている場合(HTTPリクエストの読み取りと解析の例)。(2) 結合するとインターフェースが単純になる——複数モジュールの機能を1つにまとめることで、モジュール間で受け渡していたインターフェース自体が消える場合(同じHTTPサーバ例、Java I/Oのバッファリング自動化の例)。(3) 重複を除去できる——同じコードパターンが繰り返し現れている場合、1箇所に集約したほうが単純になる。(4) 分離すると片方を理解するのにもう片方が必要になる——2つのコードが相互に強く依存しており、独立に理解できない場合。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]] ch.9 §9.1–§9.3) **分離したほうがよい事例**の核となる原則は、汎用機構(general-purpose mechanism)と特定用途向けコード(special-purpose code)の分離である。あるモジュールが汎用機構を提供するなら、そのモジュールは特定用途向けコードや別の汎用機構を内包すべきでなく、特定用途向けコードは通常、上位層の別モジュールに置くべきである。これは、モジュールが「複数の独立した機能の混在」を抱えている場合の分離規則でもある。著者はこれを2つの実例で対比する。エラーログ出力を専用クラスに切り出した例は**分離すべきでなかった失敗**であり、ロギングメソッドが浅く、呼び出し元と強く依存し合っていたために複雑性だけが増した。逆にエディタのundo機構で、汎用的な `History` クラス(アクションのリストを管理する核)と、特定用途向けの `History.Action` 実装(`UndoableInsert` 等)を分離した例は**分離すべきだった成功**であり、汎用機構・アクションの詳細・グループ化の方針という3つの独立に理解できるカテゴリへの分割が実現された。両者を分ける基準は、分離後の各部分が呼び出し元やドキュメントを見ずに独立して理解できるかどうかである。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]] ch.9 §9.4, §9.6–§9.7) この判断基準はメソッドの分割・統合にも及ぶ。長さそのものはメソッドを分割する理由にならず(「20行を超えたら分割せよ」という通念に著者は明確に反対する)、数百行のメソッドでもシグネチャが単純で読みやすい深いメソッドなら問題ない。分割が正当化されるのは、(a) 部分課題を汎用的に切り出せる場合(子は親を知らずに理解でき、親は子の実装を知らずに理解できる)か、(b) 元のインターフェースが無関係な複数の役割を持ちすぎていた場合、のいずれかに限られる。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]] ch.9 §9.8) ## 横断的知見 - 第4章は深いモジュールを「インターフェース(コスト)と実装(利益)」という静的な2面構造として提示するのに対し、第6章はそこに「汎用化によってインターフェースが単純化し、実装がやや複雑化しても総体として深くなる」という動的な機構を与える。Unix I/Oの深さ(第4章)は既に確立された汎用インターフェースの結果だが、テキストエディタ事例(第6章)はその深さを設計判断として作り出す過程を示している点で相補的である。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 6 General-Purpose Modules are Deeper]]) - 第4章の「深さ」は長方形の比喩によるインターフェース/実装の静的な大小関係にとどまるが、第8章の「複雑性を下方に押し込む」はその大小関係を生み出す動的な判断規則(どちらが苦労を引き受けるか)を与える点で、両者は同じ現象を異なる角度から補完している。第4章が示す「Unix I/O は深い」という評価結果は、第8章の原則(開発者が苦労を引き受ける)を実装レベルで体現した帰結として読める。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]]) - 第8章の「複雑性を下方に押し込む」と第9章の「結合か分離か」は、一見どちらも複雑性の配置に関する原則で重なって見えるが、扱う階層が異なる。第8章は**1つのモジュールの内部**で複雑性をどちらが引き受けるか(開発者 対 利用者)を問うのに対し、第9章は**複数のモジュールの間**で機能をどう配分するか(結合 対 分離)を問う。両者は独立の軸として補完し合う——たとえば第9章のundo機構の例(§9.7)で汎用の `History` クラスに複雑性を集約する判断は、第8章の「開発者が苦労を引き受ける」原則と同じ方向を向いているが、第9章はさらに「その複雑性をテキストクラスに同居させず独立クラスへ分離すべきか」という、第8章にはない軸を追加している。したがって第4・6・8章が積み上げた「単一モジュールをどう深くするか」の知見に、第9章は「深いモジュールの境界をどこに引くか」という一段上の判断を接続する。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]]) - 第8章の「複雑性を下方に押し込む」正当化3条件((a)機能との密接性、(b)上位の単純化、(c)インターフェースの単純化)と、第9章の「結合したほうがよい兆候」4つは、(a)↔情報共有・理解の相互依存、(c)↔インターフェースの単純化、という形でほぼ対応関係にある。両章は独立した文脈(1つのモジュールへの複雑性の集約/2つのモジュールの結合)でほぼ同じ判断材料に到達しており、この一致は著者の判断基準が「機能的な密接性」と「インターフェースの単純化」という2つの軸に収束することを示唆する。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]]) ## 未解決の問い - クラス病を避けつつ、モジュールを不必要に巨大にしない(責務過多にしない)境界をどう引くかは、第4・6・8・9章では明示的に論じられていない。第8章の§8.3が示す3条件と第9章の結合/分離の4兆候は判断材料になるが、これらの条件をどう測定・検証するかまでは述べられていない。 - 第6章は「インターフェースの単純化 > 実装の複雑化」という不等号が成り立つ理由を事例(テキストエディタ)で示すのみで、この不等号が一般に成り立つ条件・成り立たない場合の見分け方は述べていない。第8章§8.3の3条件がこの不等号の成立条件を部分的に明確化しているが、定量的な基準ではない。 - 第9章のエラーログ出力の例(§9.6、分離失敗)とundo機構の例(§9.7、分離成功)はいずれも「分離後に各部分が独立して理解できるか」を事後的にしか判定していない。設計段階でこれを事前に見分ける方法(結合してみる/分離してみる以外の判断手段)は述べられていない。 ## 関連 - ソース: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]] / [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 6 General-Purpose Modules are Deeper]] / [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]] / [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 9 Better Together Or Better Apart?]] - 概念: [[抽象化(ソフトウェア設計)]] / [[情報隠蔽]] - 実体: [[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 6. - John Ousterhout, *A Philosophy of Software Design*, Yaknyam Press, 2018, Chapter 8. - John Ousterhout, *A Philosophy of Software Design*, Yaknyam Press, 2018, Chapter 9.