TL;DR
内容
モジュール設計の目標
システムの複雑さは、最も複雑なモジュールの複雑さで決まる。そのため、複雑さを抑えるにはモジュール設計が重要になる。理想は、各モジュールが他のモジュールから完全に独立していて、他のモジュールを知らなくても作業できる状態である。しかし実際には、モジュールは互いのクラスやメソッドを呼び出すので、あるモジュールの変更が他のモジュールに波及する依存関係が生まれる。
そこで、モジュール設計の目標はモジュール間の依存関係を最小限にすることになる。そのために、各モジュールをインタフェースと実装に分ける。インタフェースが実装よりはるかにシンプルなモジュールは、システムの他の部分に与える複雑さが小さい。また、インタフェースを変えずに実装を変更すれば、その変更は他のモジュールに影響しない。
インタフェースが持つ 2 種類の情報
インタフェースには、形式的な情報と非形式的な情報がある。形式的な情報は、引数の名前や型、戻り値の型、例外のように、プログラミング言語が正しさを検査できる部分である。非形式的な情報は、「このメソッドは別のメソッドより先に呼ぶ必要がある」といった使い方の制約のように、言語では表現も強制もできない部分で、コメントでしか書けない。
非形式的な情報のほうが大きく複雑だが、モジュールを使うために何を知る必要があるかを正確に示せる。そのため、未知の未知を減らすのに役立つ。
抽象化のよくある間違い
インタフェースは、モジュールの機能を単純化した抽象化である。抽象化から詳細を省いてよいのは、その詳細が本当に重要でないときだけである。
抽象化には 2 つのよくある間違いがある。1 つは、重要でない詳細を含めてしまうことである。抽象化が複雑になり、使う開発者の認知的負荷が増える。もう 1 つは、重要な詳細を省いてしまうことである。開発者は正しく使うための情報を得られなくなる。後者を偽りの抽象化(false abstraction)と呼ぶ。抽象化を設計するときは、何が重要かを見極め、重要な情報をできるだけ少なくする設計を探す。
深いモジュールと浅いモジュール
強力な機能を提供しながらシンプルなインタフェースを持つモジュールを、深いモジュールと呼ぶ。たとえば Unix のファイル I/O は、open、read、write、lseek、close の 5 つだけで多くの機能を提供している。Go や Java のガベージコレクションも深いモジュールの例である。
逆に、提供する機能に比べてインタフェースが複雑なモジュールが浅いモジュールである。次のメソッドは、インタフェースを覚える手間に見合う効果がない。
private void addNullValueForAttribute(String attribute) {
data.put(attribute, null);
}浅いモジュールは避けられない場合もあるが、複雑さを管理する上では役に立たない。レッドフラグの 1 つである。
Classitis
「クラスは小さくあるべき」「長いメソッドは分割すべき」という考え方が行き過ぎると、浅いクラスやメソッドが大量に生まれる。この状態をClassitisと呼ぶ。個々のクラスはシンプルになるが、機能を実現するのに大量のクラスが必要になり、それぞれのインタフェースのぶんだけシステム全体の複雑さが増す。
Java のファイル I/O はその例である。ほとんどの利用者はバッファリングを必要とするのに、デフォルトでは提供されず、3 つのクラスを組み合わせる必要がある。
FileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);インタフェースは、よくある使い方をできるだけシンプルにできるように設計するべきである。よく使う機能のために、めったに使わない機能まで学ばせる設計はOverexposureというレッドフラグであり、情報隠蔽の観点からも良くない。
キーワード
自分の理解
複雑さを軽減するには、モジュール設計が重要となる。深いモジュールはコスパ最高である。
Classitis の話は、一見 Martin Fowler のリファクタリングと真逆のことを言っているように見える。しかし、大きすぎるクラスもそれはそれで問題であり(単一責任の原則)、真逆というわけではない。