#book #r2019 ![image](https://gyazo.com/c1fad82e1bce74b701c78655c515f4ed/thumb/1000) ## TL;DR - ソフトウェアには様々な複雑性がある - それぞれに対して戦略的なアプローチをする必要がある - そのためには以下が重要 - shallow moduleではなくdeep moduleが良いmoduleである - いいコメントやいい名前づけは複雑度を減らす - この本の中では、複雑度にまつわるものに名前をつけて複雑性への理解とその戦略的なアプローチへの理解を学ぶ # Introduction to Complexity > Complexity is anything related to the structure of a sowtfare system that makes it hard to understand and modify the system ## [[Symptoms of complexity]] - [[Change Amplification]] - あるタスクを実装しようとするときに生じる変更箇所の数 - [[Cognitive Load]] - あるタスクを実装しようとしたときに生じる技術調査や実装するために変更が必要な箇所の学習量 - これが大きいとそれだけ多くのことを学習する必要がある - またこれが高いとそれだけ考慮漏れによるバグや不具合が生じる可能性が高い - > Sometimes an approach that requires more lines of code is actually simpler, because it reduces cognitive load. - アノテーションなどで魔法をしてしまう実装などは、行数は少ないが [[Cognitive Load]] が高い - [[Unknown unknowns]] - あるタスクを実装しようとしたときに何をすべきかがわからない状態 - この現象が最悪で、実装するにあたって知りたいことがなんであるかも知らない状態なので学習するのも苦労する ## Cause of complexitiy - [[Dependencies]] (依存) - 依存が多いとそれだけ影響範囲が大きくなる - `import` や 見えてるinterfaceの数 や そのクラスが使用されている箇所など - [[Change Amplification]] と [[Cognitive Load]] に影響を与える - [[Obscurity]] (曖昧さ) - ある情報が自明でないときに曖昧さがある - 例えば変数名やメソッド名からコメント不足やドキュメント不足など - 誰にでも伝わるような設計があればドキュメントは少なくて済むのでこれも設計によって改善の余地がある - [[Cognitive Load]] と [[Unknown unknowns]] に影響を与える ## Strategic vs Tactical Programming > Working code isn't enough - [[Tactical Programming]] - タスクを終わらせることに特化した方法 - 複雑度は一気にあがることはなく、こういった [[Tactical Programming]] によって少しづつ積み上がっていく - [[Strategic Programming]] - コードは機能するだけでは十分ではないという考えに基づく方法 - 複雑度の低いコードを書くことに投資することで、短いスパンではスピードが落ちるが、長い目でみると費用対効果が高くなると考えられる - 10 ~ 20 % の全体の時間を複雑度の低いコードを書くための調査や実装などにかけるべきである - よいコードベースは技術的な優位性やマーケットでの成長速度を産むのでスタートアップだからといってそこに投資をしないというのはあまりおすすめしない # Modules > In an ideal world, each moduke would be copletely independent of the others. > Unfortunately, this ideal is not achievable. > The goal of modular design is to minimize the dependencies between modules. モジュールと一概にいってもクラスだけではなく、interface と 実装があればそれは全てモジュールとして扱うことが可能。例えばHTTPリクエストなどもmoduleといえる - 最高のモジュールは、実装よりもシンプルなinterfaceがあるモジュールのこと - 他のmoduleに課す複雑度が下がる - interfaceを変更しないような変更のときには、他のmoduleに一切の影響を与えない ## formal vs informal information - [[formal information]] - メソッドのシグネチャや引数の数、型など字面からわかる明示されている情報 - [[informal information]] - 副作用やメソッドの呼び出し順序やメソッドの制約などのドキュメンテーションなどのみを通じて知ることができる情報 ## Abstractions > An abstraction is a simplified view of an entity, which omits unimportant details > In modular programming, each module provides an abstraction in form of its interface. - Interfaceは、moduleの機能をシンプルな形式で提供している - Abstraction、Interfaceから `unimportant details` が排除されることは重要で `unimportant` というのに注意しないと失敗することがある - `unimportant` な情報をabstractionに含めてしまう - abstractionが必要以上に複雑になり、[[Cognitive Load]] が増加してしまう - `unimportant` では、ない重要な情報を排除してしまう - obscurityが大きくなり、開発者はabstractionから欲しい情報が引き出せなくなってしまう ## Deep Modules - Interfaceが少なく、シンプルで機能が多く含んでるモジュールを [[deep module]] と呼ぶ - 逆にInterfaceがたくさんあるけど、機能が少ないモジュールを [[shallow module]] と呼ぶ - shallow moduleは複雑度が増してる赤信号なので気をつけるとよい ### Classitis - 「クラスは小さくあるべき」という風潮のこと - 意味なく小さいと単純に [[shallow module]] を大量に生んでしまうだけ java ``` FileInputStream fileStream = new FileInputStream(fileName); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileStream); ObjectInputStream objectInputStream = new ObjectInputStream(bufferedStream); ``` - ↑のように無駄に小さく責務がクラスで分かれているのは赤信号、Bufferがいるかどうかユーザーが選べる方が柔軟かもしれないが、シンプルなユースケースは必ず1つデフォルトとして提供すべきである。上記の例でいえば `new ObjectInputStream();` でFileからBufferつきで読めてもおかしくない。 ## Information Hiding (and Leakage) ### Information Hiding - 再三話に出てるように、moduleは機能を持つべきである。つまりは、知識を持つべき。 - このように知識をmoduleの中に詰め込むことを [[information hiding]] と呼ぶ - これによって複雑度が下がる - 機能が中に隠れるのでInterfaceはよりシンプルになる - 機能が中に隠れることでmodule間の依存は少なくなり、システム全体の柔軟度が高くなる - Privateなフィールドなどは、知識を隠す手助けはしてくれるが知識を実際に隠してるわけではない ### Information Leakage - hidingとは逆でmoduleの知識が外に表出してしまってる場合 - 染み出し方はいくつかあり - interfaceに染み出してしまう - 暗黙的な依存や制約を持ってしまってる場合 - インスタンスが初期化されてる前提とか - メソッドの呼び出し順とか - [[Temporal decomposition]] はInformation Leakageのパターン - fileの読み書きなどで同じパーザーのロジックを使ってる場合には、複数の場所で前提が暗黙的に存在しておりleakしてると言える ## General Purpose Modules - moduleの設計をするときによく対峙するシチュエーションは、`general-purpose` にするか `special-purpose` にするか - おすすめは、 `somewhat general-purpose` - 機能は現在のニーズを満たすような具体的な機能でよくて、ただしinterfaceは一般性を持って設計するというもの - 例えばテキストエディタを設計する場合には、 Java ``` // special-purpose void backspace(Cursor cursor); void delete(Cursor cursor); // somewhat general-purpose void insert(Position position, String newText); void delete(Position start, Position end); ``` - 機能は、テキスト削除などに特化してるがinterfaceは一般的になっている。 - [[special-puropose]] では、ほとんど似たような機能を2つにわけて提供してしまってる。 ## Different Layer, Different Abstraction - Layerが違うということは、責任も違うということ - しかしLayerだけを作るというバッドデザインがたくさんある - Interfaceから実装が容易に想像できるようなものは、うまくabstractionしてるとはいえず多くの場合にはshallow moduleになる ### Pass-through methods Java ``` public class TextDocument ... { private TextArea textArea; private TextCodumentListener listener; ... public Character getLastTypedCharacter(){ return textArea.getLastTypedCharacter(); } public int getCursorOffset() { return textArea.getCursorOffset(); } public void insertString(String textToInsert, int offset) { textArea.insertString(textToInsert, offset); } public void willInsertString(String stringToInsert, int offset) { if(listener != null){ listener.willInsertString(this, stringToInsert, offset); } } } ``` - Layerを通ってるはずなのに何もせずに次のLayerにそのまま渡してしまうという実装パターン - メソッドが増えても複雑さが増すだけなので意味がない ### Decorators - あるクラスに対して機能を拡張するためのクラスのことを指す - 例えばwrapperなんかも [[Decorator]] - これらは容易に [[Pass-through methods]] を生んでしまうのであまりよくない - 以下の方法で回避できないかを考える - 元のクラスに実装を追加する - クラスではなく、処理をしているところにロジックを追加する - すでにある [[Decorator]] をうまく利用する - 完全に独立した新しいクラスを作る方がよくないかを考える Pass-through variables Java ``` main(argc, argv); // argcからcertを取ってm1に渡す m1(..., cert, ...); // m1では特にcertは使わない m2(..., cert, ...); // m2では特にcertは使わない m3(..., cert, ...) { ... openSocket(cert, ...); ... } ``` - [[Pass-through methods]] の変数版 - [[Pass-through variables]] を改善するのは難しい - 共有してるオブジェクトに詰めるようにする - グローバル変数にする - Contextオブジェクトにする - どの方法もその変数への依存を増やすのでシステム全体の複雑度をあげてしまう - 筆者的には、Contextオブジェクトをimmutableにする方法がもっとも有効と考えてる ## Better Together Or Better Apart - ある2つのコードを同じ場所におくか、離れたところに置くかという問題 - 基本的には、コンポーネントの数とコンポーネントの大きさはトレードオフになっている - いくつか指針が挙げられる - Bring together if information is shared - 知識をシェアしているもの同士は同じ場所におくのがよい - 例えばあるフォーマットのデータをパースする処理などが一箇所になるようにする - Bring together if it will simiplify the interface - Bring together to eliminate duplication - Separate general-purpose and special-purpose code - 一般性のあるモジュールの中に具体的なユースケースでしか使わない処理を混ぜてはいけない - メソッドを分割したり、統合したりする際にはある機能の単位で独立して分けることが重要 - > Each method should do one thing and do it completely - 他のメソッドの処理を理解しないといけないなどがあってはいけない # Exception - 例外はソフトウェアを複雑にする大きな要因のひとつ - エラーの種類が多すぎる、またそれらをハンドリングするのが難しい - プログラミング言語的に綺麗にかける言語がない - 状態が中途半端になったりする - 例外のハンドリングの方法はおもに3つ - 例外が起こっても続ける - 例外を投げて呼び出しもとに通知する - これは中途半端なstateを生み出すので大体の場合においてはうまく行かないし複雑 - さらに悪いことにあまり起こらない例外を書くと滅多に実行されないコードを書くことになるので将来的な複雑度に寄与する - > code that hasn't been executed dosen't work - 必要な例外を必要なだけ書くことが重要 - 例外を減らすことは複雑度を下げる点で重要だが、必要な例外まで減らすと柔軟性を下げるので注意が必要 ### Define errors out of existence - 例外にならないようにAPIを定義する - 例えば、flushのような機能で - `データがあるときにデータを空にする` : データがない場合には例外 - `空になることを保証する` : データがなくても結果は保証できてるので例外を投げない ### Mask exceptions - エラーを検知したあとに内部でそれをハンドリングして再送などを行うことで外側には知られないようにする ### Exception aggregation - メソッドを呼び出すところですぐにハンドリングするのではなく、さらにトップレベルで吸収するようにする Java ``` try { if(...) { handleUrl1(...); } else if(...) { handleUrl2(...); } else { handleUrl3(...); } } catch (NoSuchParameter e) { // do something } ``` - ↑ だと `handleUrl1` `handleUrl2` `handleUrl3` でそれぞれ例外を処理することも可能だけどトップレベルでやる ### Just Crash - 以下のようなときには、諦めてアプリをクラッシュさせる - ほとんどその例外が起こらない - 例外が起こった結果ハンドリングすることがない (out of memory がいい例) # Code Comments - コメントは、コード単体では表現できないコードを書いた人の思想を反映させたり、補足するものである - よく言われる `why not` を書くという話に繋がる考え - コメントは複雑度のうち特に [[Cognitive Load]] と [[Unknow unknowns]] に役立つ - 何を変更しないといけないかの情報を付与することで [[Cognitive Load]] を減らすことができる - システム全体やコードの構造をドキュメンテーションして暗黙知などを減らし、[[Unknown unknowns]] を減らすことができる ### コメントの書き方 - コメントはコードから明らかではないことを書く - convention (何を書くコメントかフォーマットはどうかなどの規約) を決める - Interface - Data Structure member - Implementation Comment - Cross-module comment - コードに書かれていることをコメントで繰り返さない - 練習として例えば、異なる単語を使って説明してみる - コードと同じ抽象レベルでコメントを書かない - コードよりも lower level か higher level でコメントを書く - lower level : コードに具体性を追加する (implementation comment によく使う) - higher level : コードに理由を追加する (interface commentによく使う) ### [[Interface Comment]] - Higher Levelでどうやってそのメソッドを使うかを書く - 引数が何を表すか、引数やメソッドの動作の制限など ### [[Implementation Comment]] - Lower Levelで何がしたいかの思想のヒントを書く - コードを読む人は、何をしているかの直感的理解があるとコードは簡単に読めるので直感的に理解するための情報をコメントによって与える ## Choosing Names for variables, methods ... - いい名前は、動作が想像できる助けになるため [[Implementation comment]] になる - いい名前は2つの要素を持つ - precision - 具体的かつ正確でないといけない - 一般性のある名前は勘違いや複数人間での意味のぶれを招く - consistency - 曖昧さを防ぐという意味でも同じ名前を同じ目的のために一貫性を持つということは重要である - 以下を守るとよい名前づけができる - 同じ目的のためには同じ言葉を使う - 同じ目的ではないものには使わない - 挙動がにぶれが出ないように目的を具体化させる - あくまでも指針であり、直感的かどうか読みやすいかどうかは読み手 (チームの自分以外のn-1人) が決める ## Using Comments as a part of design process - コメントを書くという工程をコードを書くプロセスに組み込むことで設計をする助けにする - 筆者は以下のプロセスでコードを書く 1. クラスのinterface commentを書く 2. 重要なpublicメソッドのinterfaceコメントとシグネチャを書く 3. 自分の設計やデザインがしっくり来るかを考えて方針を決める 4. クラスの重要な変数などに対してもコメントを書く 5. メソッドの処理の中身を書いて、implementation commentを必要に応じて書く 6. 5の過程で追加で必要になるメソッドなどもあるのでその度にinterface comment を書き必要に応じてimplementation commentを書く # 感想 - ソフトウェアの複雑性について考察している本 - 内容自体は誰でも一度は実際にもしくは何かの本で触れたことのある内容ばかりだがそれに対して良い名前をつけたり分解、考察をしている点がとても素晴らしい本 - 共通言語にしてチームの中での思想を合わせるにはとてもいい本