oinume journal

Scratchpad of what I learned

2023年に読みたい本

最近読書量が減ってきたので、今年は読書を頑張ろう!と思ったのでまずは読みたい本をリストアップしてみる。

プロを目指す人のためのTypeScript入門

仕事でTypeScript書いてるんだけど、型システムが難しすぎて詰まっているので、体系的に理解したい。

データ指向アプリケーションデザイン

分厚いけど良書だと噂の本。

モダン・ソフトウェアエンジニアリング

これも良書だと噂なので。

A Philosophy of Software Design, 2nd Edition

いい加減読み終わりたい...

詳解 システム・パフォーマンス 第2版

めちゃくちゃ評判がいい本なので。

LIFESPAN(ライフスパン): 老いなき世界

最近老いが気になってきたので...

Chapter 5 - Information Hiding (and Leakage) / A Philosophy of Software Design

第5章はInformation Hiding (and Leakage)というタイトル。いいモジュールを作るには情報の隠蔽(カプセル化)を行うことが大事だよ、という話。

5.1 Information hiding

  • Deep Moduleを作る上で最も大事なのは情報の隠蔽
  • モジュールを実装する上で必要なメカニズムをinterfaceとして表には出さないこと
    • ex)
      • B-treeにどういう情報を保存するか
      • ファイルの中身が物理的なディスクのブロックのどこに存在するかを認識する方法
      • TCPプロトコルの実装の詳細
      • マルチコアCPUでのスレッドの実装
      • JSONをどうやってパースしているか
  • 情報の隠蔽は2つの方法で複雑性を減らす
    • 情報の隠蔽はモジュールのインターフェースをシンプルにする
      • 具体例) B-tree classを開発者が使うときに、理想的なノードの広がりやツリー内部の均衡化については、利用者は気にしなくて良い
    • 情報の隠蔽はシステムの発展を容易にする
      • 情報がモジュール外には隠蔽されている=(モジュール外からみて)その情報への依存性がなくなっているので、その情報に関する内部的な設計の変更の影響はモジュール内部のみである
        • 例えば、TCPでネットワーク混雑時の制御についてプロトコルの仕様が新しく追加されても、TCPのデータの送受信のコードには影響がないはずである

5.2 Information leakage

  • 直訳すると情報の漏洩。情報の隠蔽の反対の概念
  • これはモジュール間の依存を作り出してしまう
  • 情報の漏洩はインターフェースだけで起こるものではない
    • 例えば特定のファイルフォーマットを扱う2つのクラスがあり、片方がファイルを読み込み、もう片方が書き込みをするような場合。ファイルのフォーマットが変更された時は両方のクラスを修正する必要がある。
  • 情報の漏洩もまたred flag(危険信号)
  • 情報の漏洩をどうやって防ぐか?
    • 先ほどの2つのクラスの例であれば、クラスが小さくて密接に関連しているのであればマージして1つにしてしまうのが良い
    • 情報をpull(取得)してカプセル化するクラスを新しく作る、という方法もある。
      • Another possible approach is to pull the information out of all of the affected classes and create a new class that encapsulates just that information. However, this approach will be effective only if you can find a simple interface that abstracts away from the details; if the new class exposes most of the knowledge through its interface, then it won’t provide much value (you’ve simply replaced back-door leakage with leakage through an interface).

5.3 Temporal decomposition

  • 時間による分解
  • Temporal decompositionの考え方で「ファイルを読み込んで、その内容を変更して、新しいバージョンを書き出す」というアプリケーションを考えると、3つのクラスに分解される。
    • ファイルを読むクラスと新しいバージョンを書き出すクラスはそれぞれファイルのフォーマットを知っている必要があるので、ここで情報の漏洩が発生する
    • 情報の漏洩を防ぐには、ファイルを読むクラスと書き込むクラスは同じものにする必要がある
  • 時間による処理の順番でクラスを分解するのではなく、そのクラスが行う処理が知識として何を持っているべきか(先程の例だとファイルのフォーマット)に着目してクラスを分解するべき

5.4  Example: HTTP server

  • 生徒がHTTPプロトコルの実装をした際のデザインの意思決定について、実際の例を交えて考察
  • HTTPプロトコル説明の図

5.5 Example: too many classes

  • 生徒がよくやってしまう間違いとしては、たくさんのShallow Classに分割してそのクラス間での情報漏洩をもたらしてしまうこと
    • 例) ネットワークコネクションからHTTPリクエストを読み込んでstringに変換するクラスを作り、別のクラスでそのstringをパースする
      • Temporal Decompositionのダメな例
      • 例えばContent-Length headerでbodyの長さが指定されているので、Bodyの長さを計算するにはまずContent-Lengthをパースしないといけない
        • stringになったリクエストをパースする時にBodyの長さがわからないとパースできない
        • コードの具体例がなかったのでちょっとここはよくわからなかった
      • 結果として、2つのクラスに分割してもそれぞれがHTTPリクエストの構造を知っていないとダメな実装になっている
      • さらに呼び出し側も2つのクラスの使い方を知っている必要が出てくるので、呼び出し側に余計な認知負荷がかかる
    • よって、上の例ではリクエストを読み込むクラスとパースするクラスは一つにマージした方が良い
      • HTTPリクエストをパースという処理を1クラスに集約することで情報の漏洩が発生しない
      • クラスの数を減らすことで利用者から見たインターフェースはシンプルになる

5.6 Example: HTTP parameter handling

  • HTTP requestがサーバーに来ると、サーバーはリクエストの情報を参照する必要がある
  • 例えば、以前のFigure 5.1で言うとphoto_idというリクエストパラメーター
    • パラメーターはURLのQuery Stringとして送られてくることもあれば、ボディで送られてくることもある
    • Query Stringの場合はURLEncodeされているが、処理する時にはこれはデコードされている必要がある
  • 著者の生徒の大半は以下の良い情報の隠蔽を行なっていた
    • パラメータはQuery StringまたはBody経由で送信されてくるが、それを利用する人にはどちらから来ているかは重要ではないのでマージしている
    • 2つ目の良いところは、パラメーターのデコードについて利用者が意識しなくて済むようにしていること
  • ただし、生徒が作るモジュールは以下の点でShallow Moduleだった
    • HTTPRequestというクラスを作ってその中に Map<String, String> getParams() というメソッドを定義していた
    public Map<String, String> getParams() {
      return this.params;
    }
  • Map<String, String> という内部のデータ型を利用者に公開するのではなく、String getParam(String name) という形にした方が、仮に内部のデータ構造を変えたいときに利用者に影響を与えずに済む
  • パフォーマンスチューニングのために内部のデータ構造を変更することもあるので、なるべく内部のデータ構造は利用者からは見えないようにした方が良い
  • あとはこのMapを利用者側で変更できないようにImmutableにする必要があるなどの問題もある

5.7  Example: defaults in HTTP responses

  • HTTP Responseに関する、生徒たちがよくやる不適切なデフォルト値について説明
  • HTTP ResponseのHTTPのバージョンに関して
    • (生徒の)あるチームはメソッドの呼び出し元にHTTPのバージョンを指定させるようにしていた
    • しかし、HTTPのバージョンはリクエストで指定されているものと一致しているべき
    • なので、リクエストのHTTPバージョンをそのままレスポンスのバージョンとすることが望ましい
    • 呼び出し元がHTTPのバージョンを指定すると、このHTTPのライブラリと呼び出し元の間で情報の漏洩が発生してしまう
  • HTTPレスポンスに必要なものとしてDateヘッダーもある
    • これもHTTPライブラリが気の利いたデフォルト値を提供すると良い
    • もし呼び出し元が何かしらの理由でDateヘッダーをオーバーライドしたい場合は、専用のメソッドを用意する
    • クラス or モジュールはできる限りいい感じのデフォルト値を用意する
    • P.26のJavaのIOバッファリングは、この点でよくない例となっている
      • IOライブラリのほとんどのユーザーはバッファリングするのでそれがデフォルトになっているべきだが、Javaの標準ライブラリではバッファリングのために別のクラスを使ってラップする必要がある
    FileInputStream fileStream = new FileInputStream(fileName);
    BufferedInputStream bufferedStream = new BuffferedInputStream(fileStream);
  • Red Flag: Overexposure(過度の露出)
    • よく使われるAPIが、たまにしか使われない機能もユーザーに覚えさせるようにしてしまうと、認知負荷を増大させてしまう(意味がよくわからなかった)

5.10 Conclusion

  • 情報の隠蔽とDeep Modulesは密接に関連している
  • モジュールがたくさんの情報を隠蔽することで、インタフェースをシンプルにしつつ多くの機能が提供される
  • システムをモジュールに分解するときに、実行時に行われる処理の順番を意識しないようにすること
    • 以前に説明したTemporal decompositionが発生し情報の漏洩やShallow modulesの問題を引き起こす
    • モジュールが知っているべき知識を考慮してそれを各モジュールがカプセル化することで、Deep modulesを作ることができる

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を変更しても全てのページの背景色が変わらないので、どの部分を変更すればいいのかが分かりにくい