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から除外することが大事

Chapter 3 - Working Code Isn’t Enough / A Philosophy of Software Design

第3章はWorking Code Isn’t Enough (Strategic vs. Tactical Programming)というタイトル。

Tactical Programming

  • 近視眼的に目の前のタスクを終わらせるためにコードを書く
  • 目の前のタスクを終わらせることが最優先になるので、これだといい設計はもたらされない
  • これが積もり積もってシステムに複雑性をもたらす
  • 開発メンバー全員がこのアプローチで開発すると一気に複雑性が高まる
  • tactical tornado
    • Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado.

Strategic Programming

  • The first step towards becoming a good software designer is to realize that working code isn’t enough.
    • working code = 動くコードの意味?
  • Your primary goal must be to produce a great design, which also happens to work. This is strategic programming.
    • 第一のゴールを素晴らしいデザインでかつきちんと動作するものにするべき
  • 良いデザインに対して投資する、という考え方を持つべき
    • 最初に思い浮かんだ設計のアイデアをそのまま実装するのではなく、少し時間をかけてよりシンプルな設計がないかを模索する時間をかけるべき
  • どのぐらい良い設計のために時間をさくべきか?
    • 作業の10-20%とこの本ではいっている
    • 良いデザインにそのぐらいコストをかけると、数ヶ月後にはそのベネフィットを感じることができる
  • スタートアップと良い設計への投資
    • スタートアップで10-20%コストをかけることは現実的ではないと考えられているため、大抵のスタートアップではTacticalなアプローチが採用される
    • Facebook is an example of a startup that encouraged tactical programming
    • the company’s motto was “Move fast and break things.”
    • Over time the company realized that its culture was unsustainable. Eventually, Facebook changed its motto to “Move fast with solid infrastructure”

Chapter 2 - The Nature of Complexity / A Philosophy of Software Design

第2章は"The Nature of Complexity"というタイトルで、ソフトウェアのComplexityつまり複雑性についてじっくり説明されている。

Complexityの定義

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.

Complexityの3つの症状

1. Change amplification

- とある機能追加・変更をしたいだけなのに、いろんなところを変更しないといけない。以下の図で言うと、背景色を変更したいだけなのに`bg = "red"`の定義が複数箇所あるため、これらを全て修正しないといけない、みたいな。

2. Cognitive load

  • 認知負荷。開発者が機能追加・変更のタスクを完了させるためにどのぐらいシステムの内部を知っている必要があるか、と言うこと。多ければ多いほど把握できなくなり、修正漏れなどのバグが発生することになる。
  • システム設計者は時々コードの行数で複雑性を表そうとするが、1行に処理が集約されてすぎていて逆に認知負荷を高めるケースもある。1行より複数行のコードの方がシンプルになっている場合もある

3. Unknown unknowns

  • 開発者が機能追加・変更のタスクを完了させるために、どの部分を変更すればいいのかが明らかではないこと。もしくはどの部分を変更すればいいのかの情報が明らかではないこと
  • 上の図だと、「背景色を変更したい」と言うタスクがある時に、bannerBgを変更しても全てのページの背景色が変わらないので、どの部分を変更すればいいのかが分かりにくい

Chapter 1 - Intruduction / A Philosophy of Software Design

A Philosophy of Software Design の第1章を読んだのでそのまとめ。

Intro

  • プログラムは機能が増えるごとに複雑さが増していく。複雑さが増えると、開発スピードが遅くなりバグが増える
  • 開発ツールは複雑性に対処するに役立つが、これには限界がある。一方、シンプルなソフトウェアのデザインはより大きくてパワフルなプログラムを導いてくれる。
  • 複雑性に対処するには2つのアプローチがある。
    • 1つ目はコードをシンプルかつ明らか(わかりやすく)すること。例えば複雑性は特殊ケースを削除することで減らすことができる。
    • 2つ目のアプローチはカプセル化でmodular design とよばれている。
      • Modular designではソフトウェアをモジュールに分割して管理して(OOPだとクラスとか)それぞれのモジュールは他のものに依存しない。
      • そのため、プログラマーはあるモジュールの開発をするときに、他のモジュールの詳細を知らなくて済む。

ウォーターフォールモデルの紹介

  • ウォーターフォールだと基本的には設計フェーズではすべてを設計し、開発のフェーズでは設計をしない。ソフトウェアは物理的なシステムより複雑で目に見えないので、特に大きなシステムであるほど全体を細部まで理解することは難しい。
  • 開発フェーズで初めて問題がわかるケースもよくあり、場合によっては設計のやり直しが発生する。ウォーターフォールモデルだとこれは大きな手戻りになってしまう。
  • この問題があるため、最近のソフトウェア開発ではアジャイルのようなインクリメンタルなアプローチが使われている。

How to use this book

ソフトウェアの設計スキルを向上させるための良い手法の一つは、"red flags"という複雑なソースコードの断片のサインを認識することである。この本ではその"red flags"を、メジャーな設計の問題を通じて説明する。