oinume journal

Scratchpad of what I learned

Chapter 4 - Modules Should Be Deep / A Philosophy of Software Design

第4章はModules Should Be Deepというタイトル。

4.1 Modular Design

  • ソフトウェアの複雑性を管理するためのもっとも大事なテクニックの一つとして、全体の複雑性の一部分だけに直面するようにシステムを設計するということが挙げられる。このアプローチは modular design と呼ばれている。
  • この手法を用いると、ソフトウェアシステムは複数のモジュール(クラス, サブシステム, サービス)に分解される。
  • それぞれのモジュールは相互に依存せず独立しているため、開発者はあるモジュールの開発をしている時に他のモジュールのことを考えなくて良い
  • しかし、上記の独立性の話はあくまで理想で実際にはそんなことはない
    • 例えば、あるメソッドに引数を追加した場合はその呼び出し元のコードも変更しないといけない
  • 依存の認識と管理をするためには、モジュールをinterfaceとimplementationの2つに分けることが大事である
  • Typically, the interface describes what the module does but not how it does it.
    • interfaceはそのモジュールが「何をするか」を表明し、それを「どうやるか」については表明しない
  • あるモジュールの開発者は、そのモジュールのinterfaceと実装、およびそのモジュールが依存している他のモジュールのinterfaceを知っている必要があるが、他のモジュールの実装については知らなくても大丈夫である
  • interfaceが実装に比べてとてもシンプルに表現されているモジュールがベスト
  • 上記のようなモジュールは2つのメリットがある
    • シンプルなインタフェースを提供するモジュールは、他のモジュールにもたらす複雑性を少なくする
    • インターフェースが変更されない限り、他のモジュールには影響がない。

4.2 What's in an Interface?

  • interfaceにはformal, informalな情報がある
  • formal: メソッドのシグネチャ(引数およびその型、戻り値およびその型)
    • プログラミング言語の機能によってもたらされる
  • informal: そのメソッドを呼び出した時にもたらされる結果など
    • eg) 引数で与えられたファイル名のファイルを削除するなど
    • informalな情報はメソッドのコメントで説明されることが多い
    • Method Bを呼ぶ前にMethod Aを呼ぶ必要がある、みたいなものもinformalな情報である。

4.3 Abstraction

  • Abstractionとは、実体から重要ではない詳細を除外したもの
  • 間違った抽象化には以下の2つがある
    • 抽象化したが、重要ではないものを含んでしまう
      • 結果的に他の開発者の認知負荷をあげてしまう
    • 抽象化して重要なものを隠してしまう(false abstraction)
      • そのモジュールがシンプルなものだと誤解させてしまう
  • 良い抽象化の例として、ファイルシステムが挙げられる
    • ファイルにデータを書き込む時に、ユーザーはそのデータがストレージデバイスのどのブロックに書き込まれるかなどは意識しない
    • これはファイルシステムがうまく抽象化されていて、ユーザーにとっての不必要な情報を除外しているから
    • 一方で、書き込んだデータのフラッシュについての情報は除外されていない
      • 例えばデータベースのようなソフトウェアだと、「システムがクラッシュしてもファイルに必ず書き込まれているか」を保証するために、「実際のストレージにいつデータが書き込まれるか(=フラッシュされるか)」は知っておく必要がある。そのためこの情報は抽象化されても利用者からわかるようになっている。

4.4 Deep Modules

  • モジュールの深さはコストとベネフィットで考える
    • モジュールのコスト:システムの複雑性。interfaceによって表現される
    • 小さくてシンプルなインターフェースだと複雑性は少なくなる
  • 以下がコストとベネフィットの説明の図
  • UnixのFile I/O はdeep interfaceの良い例
    • 以下の図のようにinterfaceはシンプルだが、実装は以下のような複雑である
      • ディスク上での効率的なアクセスのためのファイルの実装
      • ディレクトリ構造とその階層構造
      • ファイルのパーミッション
      • interruption, background code、およびこれらのやりとり
      • 同時アクセスが発生した場合のスケジューリング
      • アクセスしたファイルのメモリ上のキャッシュ
      • セカンダリのストレージデバイス(ディスク、Flashドライブなど)を一つのファイルシステムに統合する
    • 上記のような複雑なものは利用者からは見えない一方で、何年もの時間を経て大幅に進化している
    • 別の例だと、ガベージコレクションもdeep interfaceである
      • 利用者がほとんど意識しなくていいものだが、実装はとても複雑という意味で。

4.5 Shallow modules

  • interfaceが(提供する機能と比較して)複雑であること

      private void addNullValueForAttribute(String attirubte) {
          data.put(attribute, null);
      }
    
  • 上のコードは何も抽象化していない

  • もしこのメソッドのドキュメントを適切に書いたとしたら、コードより長いドキュメントになる
  • Shallow moduleはRed Flag
    • A shallow module is one whose interface is complicated relative to the functionality it provides. Shallow modules don’t help much in the battle against complexity, because the benefit they provide (not having to learn about how they work internally) is negated by the cost of learning and using their interfaces. Small modules tend to be shallow.
      • Shallow moduleは提供するベネフィットをラーニングコストで打ち消してしまっている
        • 小さいモジュールがshallow moduleになりやすい
  • (個人的に思ったこと)Clean Architectureって割とshallow moduleになりやすいのでは?と思った。例えばinfrastructure層とか、クライアントライブラリのメソッド呼ぶだけみたいなのが多い
    • クライアントライブラリ自体をinfrastructure層に見立てるというやり方もあるっぽい

4.6 Classitis

  • 昨今だとクラスは小さくする方が好まれている。メソッドも同様で「N行超えたら分割するべき」みたいな風潮がある
  • しかし、Deep Classを目指すのであれば、クラスは大きくなる傾向にある
  • Classitis
    • The extreme of the “classes should be small” approach is a syndrome
  • Classitisはたくさんのクラスを生み出し、それぞれのクラスはシンプルになるが全体のシステムとして見ると複雑性が高まる
    • 小さいクラスはそれ単体では十分な機能を提供できない

4.7 Examples: Java and Unix I/O

  • Classitisの典型的な例としてJavaのIOまわりのクラスの話がある。例えばJavaでオブジェクトをファイルから読むときのコードは以下のようになっている。

      FileInputStream fileStream = new FileInputStream(fileName);
      BufferedInputStream bufferedStream = new BuffferedInputStream(fileStream);
      ObjectInputStream objectStream = new ObjetInputStream(bufferedStream);
      ....
    
  • バッファリングは基本的にみんな使うので、上記のように専用のクラスを使わないと有効にならないデザインよりかは、よくあるケースに対してシンプルなデザインになっていた方が良い

    • この例だとデフォルトでバッファリングを有効にして、オプションで無効にできるとか。
  • 対照的な例として、Unix Filesystemはシンプルになっている。
    • シーケンシャルIOが最も一般的に使われるので、それをデフォルトの挙動にしている
      • readはシーケンシャルアクセスで、 lseek でランダムアクセスもできるようになっている

4.8 Conclusion

  • interfaceを抽象化してシンプルなものにして、複雑な実装はinterfaceから除外することが大事