dev.nfurudono.com

A Philosophy of Software Designの読書メモ

各章のまとめをする。随時僕の思ったことを差し込む。なのでこのドキュメントを読んであの本を読んだ気になってはいけないし、あの本と同じくらいに信頼してはいけない。

Preface

ソフトウェアの設計とか、良いソフトウェアが何かとかの議論が未熟である

  • 以下のような事項については議論がされている
    • 開発プロセス
    • 開発ツール
    • プログラミングテクニック
      • OOP
      • functional
    • デザインパターンとかアルゴリズム
  • 設計については1971の "On the Criteria to be used in Decomposing Systems into Modules" から発展がない

設計技法を体系的に身につけて複雑さを減らせるようにすることを目指す

  • 分割統治が大きな関心ごとだが、そのための教育はされていない
    • どのように問題を分割するか
    • 問題の分割を決めることは設計のでかいタスクの一つ
  • プログラマの品質とか生産性にはそれなりにばらつきがある
    • 能力には才能ではなくて良い練習が効くのは一般的に言われているらしい
  • 設計のスキルがプログラマのすごい・普通を分けるだろうと仮定して、学部一年生に対して講義を始めた
    • 原理・原則を座学で伝える
    • ソフトウェア開発のプロジェクトもやって、それらを活かす練習をする
      • 学生はたくさんコードを書いて講師にレビューを受けて改善する
  • その講義で伝えた設計の原則を集めたのがこの本
  • この本を書いた人はスタンフォードの教授で色々ソフトウェアを作ってきた
  • 複雑さを減らすことがどの原則よりも大切

Chapter 1. Introduction (It's all about complexity)

良いソフトウェアをいい感じに作るためには複雑さに対処することが必要

  • ソフトウェアはそれがどんなものであるかを表現できれば作れたも同然である。例えば筋肉がなくても作れる
  • そのためソフトウェアを書くのを律速する能力は、作っているシステムがどんなものであるかを理解する能力である
  • 残念なことにプログラムを成長させていくに連れて複雑さは増していく。
  • 複雑さを増すとそのソフトウェアを理解することは難しくなり、成長させる速度やコストは増す。バグも入る
  • 開発ツールで複雑さに対処することはできるし、そうしてきた
    • コード生成とかバージョン管理とかプログラミング言語とかはその例
  • ツールの力だけでなんとかしきることは、とはいえ無理
  • ソフトウェアを簡単に作れるようになって、もっとすごいシステムを安価に作れるようになるためにはソフトウェアをシンプルに保つことが必要
  • まあ複雑さはどう頑張っても増えるものではあるのだけど、設計をシンプルにしておけばソフトウェアがでかく強力になってもまだ耐えられるようにできる

複雑さを減らすためには二つのアプローチがある

  • コードをシンプルで明らかにすること: こっちはまあそうでしょう、と思える
    • 名前の付け方に一貫性を持たせる
    • 特別扱いを減らす
  • 複雑性をカプセル化する。(moduler designと呼ぶらしい)
    • 分割統治をするということ
    • 丁寧に説明すると、でかい問題を解決するために部分問題の証明を忘れられるようにすること

開発者はソフトウェアの複雑さに対処し続ける必要がある

橋とかのデザインと違って、ソフトウェアはずっと設計を変え続けるもの。この考え方に足していないのがウォーターフォールな開発で、一度に全体を設計したらそれを開発の過程で変更することはなく、もし微妙なところが出ても書く開発対象の範囲内で対処する。

アジャイル開発は設計を変え続けるアプローチに即している。将来的に開発したいでかいものは見据えつつ全体の設計は後回しにして欲しいソフトウェアのサブセットをまずは設計して開発する。開発の段階でまずそうとわかった設計は、次の開発の前に変更してまずさを潰す。こうすることで設計の問題が小さいうちに課題を発見し解決できる見込みが増える。

こういうイテレーティブな開発では設計が終わることはない。開発者は常に設計に気をかける必要があるし再設計する必要がある。改善するために小さく刻んで開発しているのだからそれはそう。設計を改善できないのならば、アジャイルに開発する意味がない。

今日ではアジャイルな開発をしているわけで、開発者常に設計の課題について考える必要がある。設計のことに常に気をかけないということは、常に複雑さに対処しなければならないということ。

この本の目的

二つある

  1. ソフトウェアの複雑さを理解すること
  • 複雑さとは何か
  • 複雑さは何が問題なのか
  • 不必要な複雑さを持っていることにどのようにして気がつくか
  1. ソフトウェア開発にあたって、複雑さを最小化するための個別なテクニックを習得すること

Chapter 2. The Nature of Complexity

この章では複雑さとは何かや、どのようにシステムが不必要に複雑であるかを見分けるかを議論する。設計がシンプルかを見分けるだけでは設計をシンプルに作ることはできないが、その判断をもとにシンプルに作るためのアプローチをできる。

設計方針を色々試してみれば良くて、その結果いい感じになるやつを使えば良い。そのいい感じになるかの評価をできるようにすることがこの章の目的である。

複雑さの定義

この本で議論を進めるために以下のように複雑さを定義する: 複雑さはソフトウェアシステムの構造に関するもののうち、システムの理解や修正を難しくするものである。

例えば以下のような例がある

  • コード片がどのように動作しているか理解できない
  • システムのどこを修正して良いかわからない
  • 他に影響を与えずにバグ修正をするのがむずい

要するに、理解と修正が難しければそのソフトウェアシステムは複雑であるということ。それが簡単だったらシンプル。

システムの複雑さは、コンポーネントの複雑さをその変更頻度で重み付けした上での総和であると思える。

また、読み手にとっての複雑さと書き手にとっての複雑さは異なる。書き手にとってシンプルであっても読み手にとって複雑であればそれは複雑である。自分がそういうコードの書き手であったら、その分断が起きた理由を探ると良いだろう。そのギャップを埋める必要がこの仕事にはあるのだから。

複雑さの症状

一般的に三つの兆しがあって、開発を辛くする

  • 変更範囲の拡大: 簡単にできそうな変更が思ったより大変なパターン。メンタルモデルとその実現方法があっていないときに生じる。メンタルモデルを表現する力が足りていない
  • 認知負荷: うまく使うためにやらないといけないことが多いことが原因
  • unknow unknown: 変更する必要性に気がつけなくなっていること

unknown unknownが特に辛い。変更範囲が広いのはめんどいのは間違い無いのだけど、全部対処すれば自信を持って変更を完遂できる。一方でunknown unknownは自信を持つために全てのコードを読む必要があって辛い。システムがでかいとそもそも無理。

設計の目標の一つにシステムを明らかにすることがある。認知負荷とunknown unknownを減らすことにつながる。理解もコーディングもシュッとできるのがいい。何かを考えてそれが実際に通じるかの判断も簡単になる。

複雑さの原因

複雑さの症状をざっと見て、なんでソフトウェア開発が辛くなるのかを議論した。次に複雑さの原因を議論してシステムに問題が入らないように設計できるようになりたい。

複雑さは依存と不明瞭さによって生じる。ここではそれらをざっくり語って、後の章では細々とした設計での意思決定がどのようにそれらと関係するかをみる。

依存

扱う対象がそれ単体では理解できない、変更できないときに一緒に変更するやつに依存するという。頻繁に変更するコンポーネントが他のコンポーネントに依存していると、依存されているコンポーネントまで頻繁に変更するハメになる。そこで以下のシステムの複雑さの定義の言い換えを思い出す。

システムの複雑さは、コンポーネントの複雑さをその変更頻度で重み付けした上での総和であると思える。

依存が多いシステムではたくさんのコンポーネントの変更頻度が上がるため、システムの複雑さも大きい。

特に複雑なコンポーネントを他の変更が多いコンポーネントから依存させないことがシステム全体の複雑さを抑えるために効く。

不明瞭さ

大事な要素が明らかになっていないことを指す。例えば数値の単位がわからないとか、名前がなんの意味も表してないとか。あるいは依存が存在することが明らかじゃ無いのもそう。一貫性のなさもこの要因で、同じ名前が異なる用途に使われているとやばい。

ドキュメントがやばいことが多くの原因で、コメントをちゃんと書けばよかったりする。デザインが良ければそもそも明らかであってドキュメントを不要にすることもできる。

めっちゃコメントがいるような場合はデザインがまずいことの兆しだし、明瞭さを増すためにはシステムの設計を改善するのが正攻法。

依存は変更範囲の拡大と認知負荷に作用して、不明瞭さはunknown unknownと認知負荷に作用する。依存と不明瞭さを下げる設計技法が手に入ればソフトフェアの複雑さを下げられるはずだ。

複雑さはシステム全体のもの

一個一個の細かい要因が全体を壊すほどの複雑さ単独で生むわけではなくて、複数が重なり合って首が回らなくなるもの。対処するためには "zero tolerance" philosophy に従うべきらしい。

複雑さは既存のコードベースを修正することを難しく、またリスキーにする。

Chapter 3. Working Code Isn't Enough (Strategic vs. Tactical Programming)

プログラミングタスクに際してのマインドセットは良いソフトウェア設計のために大事な要素である。

良いデザインを得るためには、すぐにコードが動くことを至上とするするtacticalなマインドセットではなくて、綺麗なデザインのために時間をかけた上で問題点を修正する方針が必要だ。

この章ではなんで戦略的なアプローチが良い設計を生むのか、そして戦略的なアプローチの方が結果的には安いことを主張する。

Tactical Programmingとはどんなか

多くの開発ではtacticalな手法が取られる。例えばバグを治すためにその場しのぎの対症療法をするようなこと。確かにそのときは早いのだけど、良いシステムの設計が得られることはないだろう。そういうときに、少しずつ不必要な複雑さがシステムに入り込む。

そのうちしんどさに気がついてリファクタリングとかをしたくなるのだけれど、仕事には期限があって新しい機能を追加しないといけないからやはり複雑さは残ったままになる。今見えている問題にはすぐに効くパッチだけ当てて、全体をよくすることはない。

そのうちマジでヤバくなるのだけど、その頃には全体を治すのが大変になっていて、当然そんな時間を取ることはできないので諦めてずっとその場しのぎの変更を入れ続けることになる。

一回tacticalな道に足を踏み入れるとそこから抜け出すことは容易ではないのだ。

Storategic Programmingとはどんなか

まともなソフトウェア設計者になるための第一歩は動くだけのコードでは不十分であることに気が付くことだ。タスクを早く終わらせるために不必要な複雑さをコードに入れることは許されない。コードは既存のものに追加されていくものなので、今書かれているコードを将来誰かが編集することにもなる。なので今書くコードは動くだけでなく素晴らしい設計を体現することを目標にしないといけない(もちろん動く必要はあるけど)。

Strategic programmingには投資の心構えが必要で、例えばよく考えることに時間を投資するとか、いくつか設計してみて一番綺麗なやつを選ぶとかする。変更の可能性をいくつか想定してみてまあいけそうだと思えることを検証する。良いドキュメントを書くこともその一環である。colliraryとして、良い設計をするためにはソフトウェアを変更する能力が効く、が主張できる。ソフトウェアを変更する能力がないひとは変更に強いことを主張できないが、変更する能力がある人はその設計がどこまでの変更をどのくらいの大変さで実施できるかを評価できる。他にも、設計がまずいことに気がついたときに目を瞑るのではなくちょっと時間をかけてよくする必要がある。

どれくらい投資するべきか

最初に全部設計するのは効果的ではなさそう。理想的な設計は作りながらわかっていくものなので、少しずつのたくさんの投資を基礎の上で行うこと。1-2割の時間を設計にかけることをここでは提案している。スケジュールを破壊するほどは長くはないし、利益を産むために十分な時間でもあるはず。

スタートアップだから、みたいな言い訳は通じなさそう

Facebook, Google, VMWareを引き合いに出してstorategicにやった方が良さそうですよ、と主張している。意味的に新しいことはここでは特に言ってなさそう。よくある(本当によくある)tactical programmingの正当化への反論をここではしている。

教訓

  • storategic programmingをしろ、効果は思ったよりもすぐに現れる
  • 明日ではなく今日やるもの
  • 全てのエンジニアが良い設計への投資をすることが効果的

Chapter 4. Modules Should Be Deep

開発者がシステム全体ではなく一部の複雑さだけに対処すれば良い方にすることを目的とするソフトウェア設計着本がある。Modular designと呼ばれていて、ソフトウェアの複雑さをなんとかするためにすごく大切。

この章ではこの基本的な原則を解説する。

Modular designとは何か

ソフトウェアシステムを複数の(それなりに独立した)モジュールに分解する。ここでいうモジュールはクラスかもしれないし、サブシステムかもしれないし、サービスかもしれない。理想的にはそれぞれのモジュールは他には全く依存しないで欲しい、つまり開発者はそれぞのれモジュールを他のモジュールのことを完全に忘れて開発できるようになっていて欲しい。そういう理想的な世界では、ソフトウェアシステムの複雑さはそれを構成するモジュールたちの複雑さの中で一番複雑なやつの複雑さである(あれ、そういう定義だったか。まあ気持ちはわかる)。

現実世界ではそうは問屋が卸さなくて、あるモジュールは他のモジュールの関数とかを呼ぶ必要があって、多かれ少なかれ他のモジュールを知っておく必要がある。モジュールの間いに依存が生じることもあるはず(つまりあるモジュールを変更したら、他のモジュールを変更する必要が出てくるかもしれないということ)。Modular designの目的はモジュール間の依存を最小化すること。

依存を管理するためにはモジュールを二つの要素、インターフェースと実装に分けて捉えると良いだろう。モジュールを外から使うために知っておくべきものがインターフェースで、そのモジュールが何をするかを表す。それをどのように実現するかは表さないのが典型的。実装はインターフェースを満たすためのもの。あるモジュールで仕事をする開発者は、他のモジュールのインターフェース、そのモジュールのインタフェース、そのモジュールの実装を頭に入れて働くことになる。他のモジュールの実装を頭に入れる必要はないし、そうしないべきである。

ここでいうインターフェースはプログラミング言語の意味論で強制されるものに限らなくて、自然言語でドキュメントとして記述されることもある。そのモジュールを使うために開発者が知るべき情報は全てインターフェースの一部である。形式的でない部分はコメントとして書くしかなくて、それが正しいことをプログラミング言語が保証することはできない。そして残念なことにそういう形式的でない部分は複雑でより多い傾向にある。

明瞭に書かれたインターフェースは開発者がそのモジュールを使うために知る必要のあることを占めることであって、unknown unknownを減らす。

抽象化

Modular designと関係する概念。抽象化は対象を単純化した見方のことで、重要でない詳細を捨てたもの。複雑なことをしなくてもそれを扱えるようになるので抽象化は便利。

モジュラなプログラミングではモジュールがインターフェースによってその抽象化を提供することになる。インターフェースはモジュールの単純化された機能を説明することになるということ。モジュールを使うためには重要でない詳細を捨ててそのモジュールを理解すれば済むので嬉しい。

捨てているのが重要でない部分であるのが大切。抽象化のミスり方には二つある。

  1. 重要でない詳細を抽象化に含めること: 抽象化を複雑にする点でよくない
  2. 重要な詳細を抽象化に含めないこと: 不明瞭なシステムになるのが良くない。抽象化だけをみている開発者はその抽象化を正しく使うために必要な情報を見落とすことになる

後者のことをfalse abstractionと呼ぶ。一見シンプルだけど実はそうでもないやつ。抽象化を設計するためにはどんな情報が大切かと、どのように大切な情報を減らすかが鍵。

Deep/shallow modules, classiti

機能的でシンプルなインターフェースを持つモジュールが最高のモジュールということになりそう。抽象化がうまくいったやつがいい。そういうモジュールのことをdeepなモジュールと呼んでいる。

反対にインターフェースが複雑な割に大した機能を提供してないものはshallowなモジュール。

我々はモジュールをdeepにすることを目指すべきであって、そのインターフェースの絶対的なサイズは問題ではない。

まとめ

  • モジュールをインターフェースと実装に分けて捉えることで、その複雑さをシステムの他の部分から隠せる
  • そのモジュールを使う側はそのインターフェースだけに気をかければ良い
  • 大事なことはモジュールが深くなるように設計すること。そうすれば隠蔽できる複雑さを最大化できてシステム全体の複雑さを抑えることに貢献できる