Closed118

A Philosophy of Software Design 輪読会

ゲントクゲントク

Preface

要約

  • ソフトウェア設計の技術水準は45年前からたいして進歩していない
    • 45年前の論文「On Criteria to be used in Decomposing Systems into Modules」(システムをモジュールに分解する基準について)
    • ソフトウェア開発プロセスや開発ツール、プログラミング技術などたくさん議論されてきたが、著者的には「ソフトウェア設計の革新的な問題」ではないらしい
    • "state of the art"で「最先端の(技術)」っていう意味らしい
  • 「問題を分解すること」がいちばん大事な課題(だと著者は考えている)が、それをテーマとした授業をやっている大学は1つもないらしい(ほんとか?)
    • いわゆる「分割統治法」?
    • 「forループとかOOPは教えるけどソフトウェア設計は教えない」の主語が"We"なので、著者は大学の先生?
      • John Kenneth Ousterhoutは、スタンフォード大学のコンピューターサイエンスの教授です。彼はジョングラハムカミングと共にエレクトリッククラウドを設立しました。 ウィキペディア(英語)

  • プログラマーによって品質と生産性に大きな違いがあるが、それらは「生まれ持った才能」として片付けられてきた
  • 著者は「設計スキルこそが、優れたプログラマーと平均的なプログラマーとを分かつものである」と仮定して、大学でソフトウェア設計に関する授業を行った
    • 反復的なプロセスでソフトウェア設計を学ばせる
      • ソフトウェア設計の原則を教える → 学生にソフトウェアを開発させる → コードレビューを行う → コードを修正させる
  • この本は、↑の授業で出てきたソフトウェア原則に基づいている
  • 著者はソフトウェア設計のすべてを理解しているわけではないが、かなり開発経験豊富
    • OSをスクラッチで3つ作った
    • ファイルシステムとかストレージシステムとかいくつか作った
    • デバッガー、ビルドシステム、GUIツールキット、スクリプト言語、エディター、描画ソフト、presentation(なんだろう?)、集積回路、など
  • この本は「ソフトウェア設計の最終決定版!」ではなく、あくまで1つの意見書なので、鵜呑みにしないでほしい
  • 最終的なゴールは「ソフトウェアの複雑さを軽減すること」
sushidesusushidesu

スクリプト言語、エディター、描画ソフト、presentation(なんだろう?)、集積回路、など

インタラクティブエディター (テキスト・描画・プレゼンテーション・集積回路のための) かも

Kyosuke AwataKyosuke Awata

ソフトウェア設計の技術水準は45年前からたいして進歩していない

最近は設計という単語に含まれる範囲が増えてきている気がする
進歩しているかは自分にはまだ分からないが、広まってきたなーとは感じる

反復的なプロセスでソフトウェア設計を学ばせる

プラハチャレンジも似たようなところあるなと思った

ゲントクゲントク

オブジェクト指向や関数型プログラミングはテクニック
もっと本質的なところを議論したい、本、っぽい

プラハの社長プラハの社長

Preface

要約

複雑な問題を簡単な問題に分解するのが一番難しいけど大事だよね

翻訳

  • ソフトウェア設計に関する議論はここ80年ほとんど交わされていない。唯一の論文は'on the criteria to be used in decomposing systems into modules'
    • OOPとかFPはこの人の「ソフトウェア設計」の定義には含まれていないみたいだ
      • 「答えではなく問いを模倣する」という名言に照らし合わせると良いかも。問いを知らずにOOPだけ真似してもgetter/setterだけ定義したdata object作って終わるイメージ。問いを意識していれば全部同じところに帰結しそう
  • プログラマの生産性や質には大きな開きがあるが、これは才能ではなく質の高い習慣によってもたらされるものだと信じている
    • talent is overratedを読んでみようかな
    • ケントベックの「私は偉大なプログラマではなく、偉大な習慣を身につけたプログラマだ」に似てる
  • なのでそれを立証するためにスタンフォードでソフトウェア設計の授業を教えてみた。そのエッセンスをこの本に詰め込んでいる
  • 自分のソフトウェア設計に関する知識が絶対に正しいとは思っていないので半信半疑で聞いてほしい
  • 何より大切なのはソフトウェアの複雑性を取り除くことで、この本に書いてあることをなぞることではない
プラハの社長プラハの社長

chapter1: Introduction

とにかく複雑だぜ

  • ソフトウェア開発はとにかく複雑な作業
    • 人間が行い得る最もクリエイティブな作業
    • 物理的な制約を受けない
      • すごく共感する。プログラミングの難しさは制約がなさすぎることに起因していると思う
    • 特別な才能はいらない
    • 必要なのは自分が作りたいシステムを把握すること。把握さえできれば作れる
    • 故に「システムを把握可能にすること」が何より大切
      • これを阻害するのが複雑性
      • ソフトウェアは開発が進むほどに依存関係が生じたり、必ず複雑になっていく
      • 複雑性が増すと理解するのに時間がかかったりバグが増える
      • バグが増えるとさらに時間がかかる...というネガティブスパイラル
      • 「開発が進むと必ず複雑になっていく」ことを認識しているチームは強いよなぁと思った。作業時間の20%はデザインの見直しに充てたり
  • 確かに役立つツールは増えてきたが、それだけで立ち向かうには限界がある

複雑性に立ち向かう

  • ソフトウェアで表現する対象を把握することが何より大切で、複雑性がそれを難しくする
  • であれば複雑性を抑えることがエンジニアにとっては何より大切

じゃあどうやって抑えるのか

  1. コードをシンプルに明白に保つ
  • 特別なケースを排除したり、一定の記法に則ったり
    • 前者は区別すべきものを区別すること(userとproductを同じthingクラスとして表現したら当然条件分岐や特別なケースが増えるよね、的な)、後者はレイヤーアーキテクチャとかマイクロサービスとかマクロな観点と、lint使うとか命名規則とかミクロな観点と両方含まれてそう
  1. 隠蔽する
  • modular design
    • OOPにおけるクラスもmodular designの一つ
  • 何かを変更するのにシステム全体を理解しなければいけないとすぐ限界が訪れる
  • 変更するときにごく一部のモジュールの事だけ意識すればよくて他は無視できる状態を目指す
  • モジュール同士はお互いについて知らないよう疎結合に保つ

デザインは継続的なプロセスである

従来のソフトウェア開発観

  • 建設と似たようなものだと捉えられていた
  • 物理的な建造物の建設から着想を得ているウォーターフォール型の開発プロセスは、建設の進め方に近い
  • 前半でデザインを固めて後半は開発するだけ
  • 橋を作っている最中に「この柱は抜いてみるか」とはならない。デザイン通りに作る必要がある

実際のソフトウェア開発

  • しかしソフトウェアは非常に柔らかい(malleable)
  • かつ物理的に表現することが困難なので全体を把握しながら作業を進めることが難しい
  • だからあっという間に当初のデザインが崩れる
  • デザインはincremental(直訳:増分)に変更していく必要がある
    • しかしwfだと初期デザインに問題があると判明するのは開発の後半で、その頃には既にデザインを担える人材がいないことも多い
    • だから出来るだけ既存デザインを崩さないように機能追加していく
    • これが却って複雑性を生み出す
  • アジャイル開発はデザインのプロセスが開発のライフサイクル全体に引き伸ばされている
    • リファクタリングをしないアジャイルプロセスに対する自分の違和感はこれかもしれない。初期デザインが不適切になりやすいソフトウェア開発の特性に対処するためにデザインのプロセスを開発ライフサイクル全体に引き伸ばしているわけで、デザインの見直しをしないって大事な目的が一つ失われているよね、的な

本書の目的

複雑性を理解する

複雑性を排除するテクニックを身につける

  • これさえ知っておけばok的なテクは無いので少し抽象的な内容が多め
  • サンプルとしての簡潔さと実世界の表現度は両立が難しいので、本書のサンプルを読むだけで理解するのは難しい
  • ベストは本書で学んだ知識を使って実世界で自分が立ち向かっている問題に当てはめてみること
  • red flag(注意が必要な要素)に気づくこと
    • red flagを見たら、それを解決する代替手段をたくさん考えること
    • 代替手段をたくさん考えるほどコードのred flagは減っていき、どんどん綺麗になる
      • 最近プラハチャレンジで全く同じ話をした。強いプログラマほど代替案をたくさん考えている
  • 原理主義者にならないこと
    • 全てのアプローチには限界と例外がある
    • 本の中でoverdoing a good thingって書かれていたのが印象深い。直訳で「良いことをしすぎる」。良い=いくらでもやって良いではない
プラハの社長プラハの社長

「良いデザインとは相反する力のバランスを取っている」みたいな表現も素敵

  • 会社の文化とかチームの文化としてパクりたい
プラハの社長プラハの社長
  • ソフトウェアの柔軟性はメリットだから、それを活かすならアジャイルにしましょう
  • そもそもソフトウェアの柔軟性を活かす必要がないならWFでも良くね?
    • でもそういうケースほぼないよね
    • posレジとか銀行ですら軽減税率とかインボイス制度とか出てくるし
  • 一方でソフトウェアの場合は「変えられない」がないから、実は無理に近い仕事もやってしまっている可能性がある
  • 実は建築とか物理的なものも本当はアジャイルで作った方が良いのではないか
    • 3回立ててようやく理想の家が立つ
    • でも中国の違法建築は時々潰れてるよね(3階建てを後から6階建てにしたり)
  • 落ち着いたらリファクタリング、だと一生できない
    • 強制的にその時間を作る必要がある
ゲントクゲントク

「感想」は強調して書くの、いいですね

「感想」は*強調*して書く
ゲントクゲントク

要約も、

文章の順序と情報の構造が必ずしも一致しているわけではない

ことを意識するとこんなふうにわかりやすくまとめられるんですね、真似します

sushidesusushidesu

Chapter 2: The Nature of Complexity

2: 複雑さの性質

  • この章では、「複雑さ」とは何か?どう判断するのか?原因は何か?について取り上げる
    • その他、基本的な仮定も行う
  • 複雑さを認識する能力は重要な設計スキルである
    • それができると、、、
    • 問題を特定できる
    • 良い選択ができる

2-1: この本における複雑さの定義

  • 複雑さとは、ソフトウェアの理解と変更を困難にするソフトウェアの構造(に関連するもの) である
    • もっと良い訳がありそう
    • 理解と変更が困難な場合、複雑
    • 逆に理解と変更が簡単な場合、単純
    • 例えば、、、
      • コードの一部がどのように動くか分かりづらい
      • 小さな改善を実装するのにたくさんの手間がかかる
      • 改善のためにシステムのどこを修正すべきかが明白でない
      • バグ修正時に別のバグを生みかねない
    • コストと利益の観点:
      • 変更により多くの労力がかかる場合、システムは複雑
      • 単純なシステムではより大きな改善を小さな労力で実装できる
  • システムの複雑さは、最も一般的な活動によって決まる
    • たくさん触る部分が複雑な場合→システムは複雑
    • 複雑な部分はあるが、ほぼ触れない場合→システムは複雑ではない
    • 必ずしもシステムの規模や機能に関係しているわけではない
  • システムの全体的な複雑さは、「作業に費やす時間で重み付けされた各部分の複雑さ」によって決まる
    • C = 複雑さ
    • p = システムの各部分
    • cp = 各部分の複雑さ
    • tp = その部分の作業に費やす時間の割合
    • JSで書くとこう?
      • const overallComplexity = partsOfSystem.reduce((acc, { complexity, time }) => acc + complexity * time, 0)
    • → 複雑さを隠すことは、複雑さを排除することと同じくらい良い
  • 開発者としてのあなたの仕事は、他の人も簡単に作業できるコードを作ることである
    • 書き手が単純だと思っても、読み手が複雑だと感じたならば、それは複雑である
    • 書き手と読み手の意見の違いから学ぼう

Your job as a developer is not just to create code that you can work with easily, but to create code that others can also work with easily.

印刷して飾りたい

プラハの社長プラハの社長

触らなければ複雑ではない(取り除けなくても隠蔽できれば良い)、読み手と書き手の意見が衝突したら読み手を優先する、あたりが自分に響いた

ゲントクゲントク

読み手と書き手の意見が衝突したら読み手を優先する

優先する、っていうより、「それは複雑である」ということは事実であることを受け止めよう、っていうふうに私は解釈しました
意見が衝突した → その後の意見交換が大事そうですね

プラハの社長プラハの社長

優先する、っていうより、「それは複雑である」ということは事実であることを受け止めよう、っていうふうに私は解釈しました

確かに!

プラハの社長プラハの社長

知識差故に複雑性の定義が異なる読み手とどう折り合いつけると良いんだろう?
自分もエンジニアなりたての頃はクリーンアーキテクチャとか無駄に複雑だと思ってた

sushidesusushidesu

複雑さはライブラリが隠蔽してくれている場面も多そう (ReactとかSWRとか)

Kyosuke AwataKyosuke Awata

隠蔽した複雑さはどうやって解決するのか?
それともその複雑さは受け入れることになる?

ゲントクゲントク

複雑さはライブラリが隠蔽してくれている場面も多そう (ReactとかSWRとか)

隠蔽した複雑さはどうやって解決するのか?
それともその複雑さは受け入れることになる?

ライブラリにして提供する、っていうのが解決策になっているような気がする

Kyosuke AwataKyosuke Awata

ライブラリにして提供する、っていうのが解決策になっているような気がする

ライブラリ使用者にとっては単純でも、ライブラリ開発者は複雑さを受け入れることになる?と思った
なのでどこかで複雑さを受け入れるしかない場面が来るのかなと
これを解決するための実践的なやり方をどうやって身につけるか、とかが書いてあると嬉しいなと

Kyosuke AwataKyosuke Awata

Chapter2.2: Symptons of complexity

複雑さがもたらす影響について

一般的に以下の3つ

  1. Change amplification
    • ある変更をする際に、単純な変更のはずなのに、多くの箇所でコードの修正が必要になってしまう状態。
      • ただし、変更箇所さえ明確にできていれば、その全てを漏れなく変更することで、最悪なんとかできる。
      • 早すぎる共通化?みたいな考え方もあるから、ここはバランス取らないとダメだなと思った。特にフロントエンドって共通化のタイミング難しくない?
  2. Cognitive load
    • ある変更をする際に、開発者が知っておくべき知識が多すぎる(認知負荷が高い)状態。
      • 人間の脳には処理能力の限界があるので、見落としや忘れが発生するリスクが高くなる。
      • ただし、知っておくべき知識を明確にできていれば、メモを取ったりとか何らかの工夫をすることで、最悪なんとかできる。
    • コードの行数が少ないからといって、単純もしくは認知負荷が低いとは限らない。
      • 数行のコードでアプリケーションを書けるFWがあったとしても、それを理解するのは難しい。
      • 時にはより多くのコード行を必要とするアプローチの方が認知負荷を下げることができる。
        • 最近Scala書いててこれはよく思う
  3. Unkown unknowns
    • ある変更をする際に、「何をするべきなのか?」「どういう情報が必要なのか?」というそもそものところが分からない状態。
    • これが3つの中で最も最悪な状態。
      • 「何か問題があるんだろう」とは分かる。
      • ただし実際に何が問題なのか、それによって何が起きるのかは、やってみないと分からない。
      • 全てのコードを読んで理解すれば、解決することはできるかもしれないが、どの規模であってもそれは現実的ではない。
    • こうなる前にどこかで止められなかったのか?日々の積み重ねしかない?

その影響を受けないようにするために

システムを常にシンプルに保てるように設計することが大事。
シンプルに設計できていれば、以下のような状態になる。

  1. ある変更において、修正が必要となるコードが少ない。
  2. 既存のコードがどのように動作し、変更を加えるために何が必要かをすぐに理解できる。
  3. 何をすべきが素早く推測でき、その推測が正しいと自信を持てる。

18章に続く...!


疑問

P.18の下から4行目は何が言いたいんだろう?
コードを全部読んでもダメ!なぜなら...という文脈でこれを言っているような気がするが、文書化とかどう関係してくるのか?

Even this may not be sufficient, because a change may depend on a subtle design decision that was never documented.

なぜなら、変更は文書化されていない微妙な設計上の判断に依存する場合があるからです。

プラハの社長プラハの社長

自分の要約も貼っておこう

複雑さの兆候(症状)

変更の増幅

  • 1つの変更を加えるために多数のコードを修正しなければいけない
    • a code should have only one reason to change原則と同じかな

認知負荷

  • 1つの変更を加えるために多数のコードを読み解いて把握しなければいけない
    • コード行数が少なければ認知負荷が低いとは限らない
    • メモリを確保したコードとは別の場所でメモリを解放しなければいけないと、プログラマは常にメモリ状態を認識しておかなければいけなくなる。メモリの確保と解放を同じコードで実行すれば、これを気にしなくて済む
      • reactのhooks以前のライフサイクルメソッドに通ずるものを感じた。あれも前処理と後処理が同じeffect hookにまとまっているからこそ認知負荷が軽減している

未知の未知

  • 1つの変更を加えるためにどこを修正しなければいけないのか分からない
  • これが一番たちが悪い
    • 正しく修正するためには変更が必要な箇所が別にあったのに、そこに気づけず対応漏れしてしまう
      • しかも対応漏れがあったことに気づくのは実際リリースしてバグが発現した時
      • 事前に潰そうと思えば全てのコードに目を通さなければいけなくなる
    • 最初の2つは時間さえかければ正解に辿り着ける
    • 未知の未知は時間をかけても正解に辿り着けない可能性がある
ゲントクゲントク

コードを全部読んでもダメ!なぜなら...という文脈でこれを言っているような気がするが、文書化とかどう関係してくるのか?

「ここは運用でカバーしてる」みたいなものはコードには現れてこない。なのでコード全部読んでもそういうの見落とす可能性はある、みたいな?

sushidesusushidesu

いつ共通化すべきか?

  • 3アウト制 (3回目で共通化する)
  • 初めから共通化しておく (後から共通化するのは大変なので)
    • これは設計に精通している&ドメイン知識に詳しいからこそできることなのかも
sushidesusushidesu
  • SidebarLayoutとSidebarContentを分けるのは早すぎた共通化だったのか?
    • 責務の分離なので、問題ないと思った (配置と中身)
Kyosuke AwataKyosuke Awata

早すぎる共通化

詳しい人なら早めにやっても良いかもしれないが、あんまり詳しくない人が早めにやると、不要な共通化をしてしまったりするかも。

詳しいとは?
ドメインに詳しい -> 他で使われる可能性があるかを判断しやすい
設計に詳しい -> コードが複雑になる予兆を判断しやすい

プラハの社長プラハの社長

自分なりの要約を書いてみる

2.3 複雑性を生み出す要因

プラハの社長プラハの社長

複雑性を生み出す要因

  • dependency(依存性)
    • 定義:特定のコードをそれ単体では理解・編集できないこと
      • 依存関係の例
        • bgcolorが統一されていなければいけない(暗黙的な依存性?)
        • ネットワーク上の送受信側(プロトコルを介した依存性)
        • メソッドの呼び出し(シグネチャを介した依存性)
    • 依存関係をゼロにすることはできないけど、数を減らしたり、シンプルに保つことはできる
      • 例だとバナーのbgcolorは統一されていなければいけない、という依存関係が発生しているが、一つの変数に切り出すことで相互に依存する関係から変数に向かって依存関係の方向が統一された
      • かつ変数を参照するAPIを通すことでコンパイラに依存関係を守ってもらえる
        • 依存関係の数を減らしつつ種類を変えることで複雑性を軽減できる例として良かった。バナーの色は統一されていなければいけないというのは暗黙的な依存で、共通の変数を参照しているのは明示的な依存
        • コンパイラに指摘してもらえない類の依存性こそ一番注意が必要そうだなぁと気が引き締まったe
        • この辺は型をちゃんと学ぶことで少しは予防できそう
  • obscurity(はっきりしない,曖昧)
    • timeみたいに、変数名の情報が少なすぎるとか
    • 不規則
      • 2つの変数名が同じものを指している
    • 見えない依存性
      • 例えばエラーコードを増やした時、コードに対応した文字列を含む辞書の方にも改修が必要だったとする。エラーコードを管理しているコードを触っている人にとって辞書の存在は見えない依存性なので曖昧さが増す
    • 曖昧さはドキュメント不足から生じることもあるが、デザイン上の問題からも生じる。ベストはドキュメントが要らないほどシンプルなシステムとして設計することだから
sushidesusushidesu

2.3 複雑さの原因

  • 複雑さは、依存関係と曖昧さの2つによって引き起こされる
  • 依存関係
    • 特定のコードを単独で理解or変更できない場合、そこには依存関係が存在する
    • 依存関係が有る場合、特定のコードが変更されたとき、他のコードを変更or検討する必要がある
    • 依存関係を完全に排除することはできない
      • 依存関係の数を減らして、可能な限り単純で明白にしよう
      • 依存の向きを統一するとか、層を意識するとか?
  • あいまいさ
    • 重要な情報が明確ではない場合、曖昧さが発生する
    • 例: 一般的な変数名 (timeなど)
      • 有用な情報を伝達しない
    • あいまいさは、ドキュメントの不十分さからくる
      • システムがクリーンで明白な設計の場合、ドキュメントは少なくなる
      • ドキュメントを書かなくていいくらい、システム設計を単純化しよう

2.4 複雑さはincrementalである

  • 複雑さは、たくさんの小さな依存関係とあいまいさの蓄積によって引き起こされる
    • 漸進的なので、制御が困難
    • 蓄積されると、単一の依存関係や曖昧さを修正するだけでは解消にならない
    • 「ゼロトレランスの哲学」を採用する必要がある (3章)

2.5 結論

  • 複雑さは、依存関係と曖昧さの蓄積から生じる
  • →複雑さが増すにつれて、変化の増幅、認知負荷、未知の未知につながる
  • →新しい実装のためにより多くのコードの変更が必要になる
    • 時間もかかる、実装に必要な情報が見つからない (=正しく実装できない)
  • →困難でリスクが高い
Kyosuke AwataKyosuke Awata

依存の向きを統一するとか、層を意識するとか?

具象ではなく抽象に依存させるとかもあるかも?
後はDIとかも、依存関係を実際の処理を書く時は気にしなくて良くしてくれてる?

Kyosuke AwataKyosuke Awata

あいまいさは、ドキュメントの不十分さからくる

コードの動作や仕様を説明するのはテストコード、なぜそうなったのかという背景とかは別のドキュメントとして残すとか良いのかな〜と思ったり

Kyosuke AwataKyosuke Awata

暗黙的な依存関係をなくすためには、それが同じものかどうかを知ることが難しい

(例)
あるページのバナーの色を変えた時に、他のページのバナーも必ず変える必要がある=>依存関係がある
あるページのバナーの色を変えた時に、他のページのバナーも必ず変える必要があるかもしれない=>依存関係があるとは言えない

的な?

sushidesusushidesu

本の章ごとの依存関係が明白で、各章の責務が明白だったのでとても読みやすかった

sushidesusushidesu

リンタでチェックできるでも良さそう
機械的にチェックできる = 明白である

プラハの社長プラハの社長

3.1 戦術的プログラミング

戦術的プログラミングと戦略的プログラミングに分けられる

戦術的プログラミング

  • とにかく早く機能を完成して届けることを優先する
  • 後からリファクタリングすれば良いや、と思って、多少の複雑性は妥協して機能開発を優先する

問題

  • 複雑性はincrementalに増えていくので、戦術的プログラマの作業により少しずつ複雑性が増してくる
  • 複雑性が増してくると他のプログラマも戦術的プログラミングに傾倒し始める
  • いずれ気づいた頃には複雑性を取り除くためには何ヶ月もかかるので事業的に選択が不可能になる
    • 1日30分だけ時間が増えるのはステークホルダーにも気付かれないが、丸1日リファクタリングに費やす〜とか、認識しやすい時間になってくるほど調整コストも積み重なってくるから、小さめに実施してくのが大切だなと思った
  • 戦術->戦略に切り替えるのは非常に難しい

タクティカルトルネード

  • とにかく実装は早いけど雑、それがタクティカルトルネード
  • 組織によってはこういう人がヒーローのように評価されるが、この人の書いたコードをメンテナンスしなければいけないエンジニアに高評価されることはまずない
  • タチの悪いことに、トルネードの後始末をしているエンジニア(実際はヒーロー)のパフォーマンスが低いように見えてしまう
    • 自分の前職も四半期評価だったから短期的に成果を出せる人が評価される傾向にあって、まさにこういうトルネードの人ばかり評価されて、すぐ負債化してた
sushidesusushidesu
  • トルネードの後始末をしているエンジニアを評価するのって難しそう
  • 良い設計であることを判定できるは良い設計ができるエンジニアだけではないか
Kyosuke AwataKyosuke Awata

なぜ戦術的と戦略的という言葉にしたのか?

ミクロとマクロという意味で捉えて良さそう

人類みな麺類的にいうと、
micro ... しっかり醤油味
macro ... 魚介系
である

Kyosuke AwataKyosuke Awata

Chapter3.2 Strategic Programming

  • 戦略的プログラミングとは何か?
    • 優れたソフトウェア設計者になるための第一歩は、コードを書くだけでは不十分であることを認識すること
    • 最も重要なのはシステムの長期的な構造である
    • 動く「だけ」のコードではなく、将来の拡張を容易にするようなコードを書く
  • 戦略的プログラミングを行うためには?
    • 投資マインドが必要
    • 最速の道を進むのではなく、設計を改善するための時間を投資する
      • 質の良いコードを書くことが最速で作ることに繋がる、みたいなスライドがあったはず。t_wadaさんの質とスピードだったような
    • 具体的に何をすべきか?
      • 最初に思いついたアイデアを実行するのではなく、いくつかの代替案を試して、最も良いものを選ぶ
      • 将来的に変更する必要がある場合を想定して、それが容易になるような設計を心がける
      • 良いドキュメントを書くこと
        • 良いドキュメントとはなんだろう
    • ただしこれらの先行投資が必ずしもあっているとは限らない
      • 時間が経てばその間違いは現れてくる
      • そのときに無視したり、その場しのぎの対応をせず、時間をかけて修正する
Kyosuke AwataKyosuke Awata

良いドキュメントとはなんだ?

  • ちゃんと運用されることが少ない
  • ドキュメントを作ること自体がコストになる、運用していくのが大変
Kyosuke AwataKyosuke Awata

Chapter3.3 How much to invest?

  • では具体的にどれくらい先行投資をするべきか?
    • システム全体を設計するような莫大な先行投資は効果が低い
      • ウォーターフォールを指している
    • 理想的な設計は、そのシステムで経験を積んでいく内に見えてくる
      • そこで継続的にたくさん小さな投資をすることが大事である
      • ここはアジャイル開発まわりと言っていることが似てそう
    • 筆者のおすすめは、開発時間の10%~20%
      • スケジュールに大きな影響を与えない程度に小さい
      • 長期的に大きな利益を生むには十分な大きさ
      • 初期段階でこの10%~20%を払えば、後で回収できる
  • 戦術的プログラミングをやったらどうなる?
    • 戦略的プログラミングと逆のことが起きる
      • 最初は早いがすぐに遅くなっていく
        • 割と懐疑的ではある。質の良いコード書ける人はそもそも速いジャンって感じ
      • 未来から時間を借りているようなものと例えている
        • 金融債務と同様に返済額は借りた額を上回る
        • そして金融債務と違い、技術負債は完全に返済されることはほとんどない
        • ここのたとえ話面白いな〜とか思った
プラハの社長プラハの社長

tactical/strategicの違いはマインドセットから生まれる
って文章で「周りにそのマインドがない場合はどうすれば...」と絶望した

ゲントクゲントク

負債を負債と認識せず負うのはよくないが、きちんと返済計画を立てて負債を負うのは、いいんじゃないかなと思った

あと、負債を負うことになった理由にもよるきがした
直せる人ならすぐ直せるようなクソコードは残すべきではないが、ビジネス的な理由(市場からのプレッシャーなど)から生まれる負債は負ってもよさそう

ゲントクゲントク

3.4 Startups and investment

スタートアップ企業も戦略的プログラミングのアプローチをとるべき、という話

スタートアップ企業では、プロダクトをとにかく早く市場に出したいというプレッシャーがある。
戦術的アプローチを取るべきか?戦略的アプローチを取るべきか?

  • 戦術的アプローチを取る企業の考えは、とにかく早くリリースして、そのプロダクトが成功してからいいエンジニアを雇ってきれいにすればいい、というもの
    • 実際にはスパゲティコードをきれいにするのは相当難しい
    • 適当な設計の見返りはすぐ返ってくるので、最初のリリースに行き着けない可能性すらある
    • 戦術的アプローチの結果コードベースがボロボロになると、その噂は界隈にすぐ広まり、いいエンジニアは寄り付かなくなる(「あとからきれいにすればいい」ができなくなる)
  • 優秀なエンジニアを雇うことは、企業の成功にとって最も重要な要素の一つ
    • 平均的なエンジニアを雇うよりもめちゃくちゃ金がかかる、というわけではないのに、優秀なエンジニアの生産性は平均的なエンジニアのそれよりはるかに高いため、費用対効果が高い
    • ここでいう「優秀なエンジニア」は、良い設計に深くこだわるエンジニアのこと

スタートアップ企業の実例

戦術的プログラミング、戦略的プログラミング、それぞれのアプローチで成功した企業がある。

  • 戦術的アプローチをとった企業の例: Facebook
    • 創業当初のモットーは「Move fast and break things」
    • 新人エンジニアは入社1週間以内に本番環境にコードをコミットすることが奨励されていた
    • 「Facebookはエンジニアが力を持つ会社だ」「規制や制限が殆どない」と評判になった
    • しかし、戦術的なアプローチによる負債が蓄積し、コードベースは不安定・理解しにくい・作業しにくいものになっており、Facebookはこのやりかたが持続可能なやり方ではないということに気づいた
    • Facebookはモットーを「Move fast with solid infrastructure」に改めた
    • 負債を解消できているのかは、わからない
    • ※ 戦術的アプローチは世界のスタートアップ企業で当たり前のように行われていて、Facebookが最悪企業というわけではない(たまたま目についた企業の一例として上げただけ)
  • 戦略的アプローチをとった企業の例: Google, VMWare
    • Facebookと同時期に成長した企業
    • コードの品質と優れた設計を重視した
    • 両社の技術文化はシリコンバレーでよく知られるようになり、優秀なエンジニアを採用するという点においては両者に対抗できる企業はほとんどなかった

3.5 Conclusion

  • 良い設計を手に入れることは、大変
    • 日々の小さな問題は積み重なって大きな問題になるので、継続的に負債解消に時間を投資すべき
  • 良い設計を手に入れられれば、大きな対価をすぐに得ることができる
  • 良い設計を手に入れるためには、戦略的プログラミングを一貫して行うことが重要
    • 明日投資する、じゃなく、今日投資する
  • 開発がピンチに陥ると投資を後回しにしたくなるが、ピンチは常にやってくるものなので、投資は無限に先延ばしにされることになる
    • 投資を先延ばしにすればするほど、問題の解消は難しくなる負のループ
    • 投資の先延ばしを続けていると、戦術的プログラミングのアプローチが会社の文化として根付いてしまう危険性がある
  • 一人ひとりのエンジニアが、優れた設計のために小さな投資を継続的に行うことが重要

感想

  • 戦術的アプローチでブイブイ言わせてたMeta(Facebook)がReactを作って公開してるの、すごいなと思った
  • 少数の優秀なエンジニアが頑張って尻拭いしてたんじゃ追いつかないので、全員で頑張ろうとすることが大事、それはそう、いい話だと思った
    • 現時点で全員が優れた視点を持ったエンジニアじゃなかったとしても、コードレビューなどで教える/学ぶ姿勢が大事そう
sushidesusushidesu

戦略的アプローチをとった企業の例: Google, VMWare

本当の最初期から戦略的アプローチだったのか?は気になった
大なり小なり戦術的アプローチを取る場面もあったのではないか 🤔

sushidesusushidesu

Chapter 4: Modules Should Be Deep

4 モジュールは深くする必要がある(?)

開発者が全体的な複雑さのごく一部に直面するだけで済むような設計 = モジュラー設計

4-1: Modular design

  • モジュラー設計
    • 独立したモジュールの集まり
    • クラス、サブシステム、サービスなど様々な形式
  • (理想的には) モジュールは他のモジュールから完全に独立
    • → 他のモジュールを知らずに作業できる
  • しかし、この理想は達成できない
    • なぜなら、、モジュールは互いの関数/メソッドを呼び出すことによって連携する必要があるから。
    • => モジュールはお互いについて何かを知っている必要がある
    • => 依存関係がある
      • = 一つのモジュールが変更された場合、他のモジュールを変更する必要がある
    • 例: メソッドの引数
      • メソッド・メソッドを呼び出すコードとの間に依存関係を作成する
  • モジュール設計の目標 = モジュール間の依存関係を最小限に抑えること
  • 各モジュールをインターフェースと実装の2つからなるものとして考える
    • インターフェース
      • 別のモジュールで作業している開発者が使用するために知っておく必要のある全てのもので構成されている
      • 通常、モジュールの機能を記述するが、動作は記述しない
    • 実装
      • インターフェースによって行われた約束を実行するコードで構成されている
    • 開発者が特定のモジュールで作業するとき以下を理解する必要がある
      • そのモジュールのインターフェースと実装
      • そのモジュールによって呼び出される他のモジュールのインターフェース
    • 自分が作業しているモジュール以外の実装を理解する必要はない!
  • モジュールとは、インターフェースと実装を持つコードの任意の単位である
    • OOPの各クラスはモジュール
    • これも
      • クラス内のメソッド
      • オブジェクト指向ではない言語の関数
    • ↑インターフェースと実装があるので、モジュール設計手法を適用できる
    • これも
      • 上位レベルのサブシステムとサービス
      • これらのインターフェースは、カーネル呼び出しやHTTPリクエストなど様々な形式を取る
    • クラス設計に焦点をあてているけど、ほかの種類のモジュールにも適用できる
  • 最適なモジュールはインターフェースが実装よりもはるかに単純なモジュール
    • メリット
      • シンプルなので、システムの残りの部分に課す複雑さが最小限になる
      • インターフェースを変更せずに実装を変更しやすい

4.2 インターフェースに含まれるもの

  • インターフェースには2種類の情報が含まれている
    • 公式
      • コードで明示的に指定されている
      • プログラミング言語によって正しいかどうかを確認できる
      • 使用者が知っておく必要があることを正確に示す事ができる
        • → 未知の未知を排除するのに役立つ
    • 非公式
      • プログラミング言語で理解または強制できるようになっていない
      • コメントのみで説明できる
      • プログラミング言語で説明が正しいことを保証できない
      • 大きく複雑
        • 関数が引数で指定されたファイルを削除する
        • クラスの使用に制約がある
プラハの社長プラハの社長

面白かったところ

  • インターフェースと実装があるものは全てモジュール
  • プログラム上明文されていない暗黙的なインターフェース(実行順序とか、ファイルを削除することとか)
  • そのプログラムを使うためにエンジニアが知っていなければいけないことは全てインターフェース、とう定義が面白い
ゲントクゲントク

いわゆる関数型プログラミングでは、暗黙的なインターフェイスを「副作用」として、なるべく局所化しよう!っていうことなんですかね

sushidesusushidesu
  • プログラミング言語の表現力が増えると正式なインターフェースに含めることができる情報が増えるので、表現力の高いプログラミング言語は複雑さを排除しやすいと言えそう
    • 学習コストが高いと言われがちだが、、
    • 習熟している人にとっては、コードで担保される情報が増えるため理解しやすいはず
  • 細かい型とかつけてもただ複雑になるだけ!凝った型は不要論
    • ここで言われている 複雑さ はコードの見た目の複雑さ
    • 細かい凝った型を付けない場合、たしかに見た目はシンプルになる
    • しかし、その分コードで担保される情報が減る
    • 実は見えない複雑さが増えている!! のでは
プラハの社長プラハの社長

4.3: 抽象化

  • 抽象化とは単純化されたエンティティのビューである
  • 大切ではない情報を省き、大切な情報だけ見せること
  • 悪い抽象化
    • 大切ではない情報が見せられている(認知的負荷の増加)
    • 大切な情報が見せられていない(曖昧さ,obscurityの増加)
      • 待機児童数を調べるために区役所のサイト見てて、1つのサービスでパターン1と2の悪い抽象化を両方網羅してるからわかりづらいか、と思って少し腑におちた
  • 抽象化の例
    • プログラミング:
      • ファイルシステム
        • 使い手にとっては「どのブロックに保存されているか」などの情報は見せられないので意識しなくて済む。
        • 一方でdbmsにとってはファイルシステムがストレージに書き込んだタイミングを厳密に知る必要があるので、抽象化したとしてもdbmsに対してはこの情報は見せなければいけない
    • 実用生活:電子レンジ、車

4.4: 深いモジュール

  • 提供される機能とインターフェースを2つの軸で考えた時、より多くの機能を小さいインターフェースで提供しているモジュールを「深い」と定義している
  • インターフェースが小さいほどシステムの複雑度の増え方は穏やかになる
  • コストとリターンの観点で考えても良いかもしれない(コストがインターフェース、リターンは機能数)
  • 例としてunixのI/Oモジュール
    • 5つのメソッド(open,read,write,close,lseek)しか提供されていない。かつメソッドのシグネチャもシンプルで小さい。つまりインタフェースが小さい。
    • しかしとてつもなく多くのことが裏では起きている。つまり機能数は多い。i/oを実装する上では、こんなことを考えなければいけない:
      • どんな形式でデータを格納すれば効率的に読み書きできるのか
      • どうやってパスから効率的にファイルを見つけるのか
      • 競合するアクセスが起きたとき、どうやって整合性を保つのか
      • 割り込みハンドラとバックグラウンドプロセスの整合性をどう保つのか
        • ここの理解に自信がない。例えばdbがクラッシュして割り込みハンドラ起因でファイルにログを書き込む時と、バックグラウンドで動作しているスクリプトとは優先順位が区別されるべき、ということなんだろうか?割り込みハンドラ起因の時は追加の割り込みを許さない、みたいな
      • ディスクアクセスを減らすためにどうやってキャッシュするのか
      • セカンダリストレージにどうやって書き込むのか
  • これだけ多くのことをやっていて、unix自体は発展を続けているにも関わらず、ioに関する5つのメソッドは変わってない
  • スゴスンギ
  • goやjavaのgcも深いモジュールの良い例。インターフェースがゼロ(勝手に実行される)のでコストがゼロだし、gcによりメモリ解放のコードがシステムから排除されるので複雑性が取り除かれる

4.5: 浅いモジュール

  • インターフェースの割に機能が少ないもの
  • 例えば連結リスト。要素を頭かお尻にくっつけるのにさほどコードは必要ない(提供されている機能は少ない)が、使う側は新しいインターフェースを学ぶ必要がある
  • nullをリストのお尻に追加するメソッド(addNullValueForAttribute)、みたいなのは浅いモジュールの典型例
    • 新しいインターフェースを学習する手間の割に得られる機能が少ない
    • 結局内部的にはdataと宣言された変数のattributeを追加していることを読み解かなければいけない
    • このメソッド名をタイプする方がコードを書くより多くのキーストロークを必要とする

思ったこと

  • フラグで機能を切り替えるのとメソッドを分けるのとでは、この人の定義では複雑度は変わるんだろうか
    • open(int: flag)とopen,openCreateIfNotExistsみたいな
    • どちらも使う前に中身を読まなければいけないことに変わりはないしインターフェースの複雑さにも提供する機能にも大差はない気がするので、どっちでも良いと言うのだろうか
  • 浅いモジュールとはいえ自前で毎回連結リストを実装するの面倒だよなと思った
    • 浅いモジュールが言語として用意され尽くしている言語の方が扱いやすいところにつながるのかもしれない
    • それこそ自前で作られたモナドとか、言語に組み込まれているものよりは学ぶインターフェースが増えそう(実装が自分の認識と異なる可能性もあるし)
  • 前の議論で「深いモジュールが良いとはいうけどactive recordとか闇深くなりがちだよね」という話をしたけど、それは大切な情報が見せられていない悪い抽象化だからという話なのかもしれない?
    • 車は免許がいるけど電子レンジは免許がいらない。提供するモジュールが重要な副作用をもたらしかねない時は深いモジュールであるほど使用者に学習を求めても良いのかもしれない?
  • OSSの時代に生まれてよかったと思った
    • 深いモジュールを存分に使えている
プラハの社長プラハの社長

この人の定義ではレイヤーアーキテクチャは複雑性が増すのかな?あとはutilとか切り出したら機能は変わらないけどインターフェースだけ増えちゃうことになるのかな?

  • 深いモジュールの中身を縦長に切り取るか横長に切り取るかの話で、utilに切り出すとしてもsplice2みたいな無意味なメソッドを切り出すのではなく深いメソッドを切り出そうぜ、的な。そんだけ浅いモジュールなら都度定義すれば良くね?的な
  • repoのメソッドを1つ呼び出すだけのusecase層なら省略しても構わないのでは、的な
Kyosuke AwataKyosuke Awata

Chapter 4.6 Classitis

まとめ

クラスを小さくすることに意義を唱えている章?

  • 一般的な教え
    • クラスは小さくあるべきで、深くあるべきではない
    • 関数やメソッドについても同じ
    • これを突き詰めていくアプローチを、クラス炎と呼んでいる
    • クラスは良いものであり、多い方が良いと脳死で考えてしまう
  • 何が起こるのか
    • 個々のクラスは単純になるが、システム全体で見ると複雑さは増大する
    • たくさんのクラスを組み合わせて使わなければならなくなる
    • それぞれのクラスは独自のインターフェースを持つので、それらを理解しなければならない
    • 定型分が増え、冗長になる

感想

  • 意見の割れそうな章だなと思った
  • 個人的には、クラスは小さくあるべきとは思わない。
  • ただしクラスの中で細かくメソッドに切り出したりするのはありだと思う。
  • 肥大化したクラスがあったとしても、内部的に別クラスに切り出すような別の責務や機能がない限り、コード量で分けることはする必要はないと思う

Chapter 4.7 Example Java and Unix I/O

まとめ

Javaを良くない例、Unixを良い例として、前章の具体例を出してきた。

  • Java
    • クラス炎が顕著に見られる
      • 例えば、ファイルを開いてシリアライズされたオブジェクトを読むために、3つのオブジェクトが必要
        • FileStream ... 初歩的なI/Oのみを提供する
        • BufferInputStream ... FileInputStreamにバッファリングを提供する
        • ObjectInputStream ... シリアライズされたオブジェクトの読み書きを提供する
    • Java開発者はきっと、常にバッファリングが必要とは限らないのだから、これで良いと主張する
    • しかし、インターフェースは一般的なケースを単純化するよう設計するべき
      • バッファリングを必要とするI/Oの方が一般的
      • バッファリングが不要なケースは、それを指定できるような仕組みを用意しておく
        • コンストラクタを分けたり、メソッドを介して設定できるようにしたり
  • Unixは一般的なケースを単純化できている
    • シーケンシャルI/Oが最も一般的であることを認識して、それをデフォルトの動作としている
    • ランダムアクセスしたい場合は、seekシステムコール(?)を使ってくれ
    • インターフェースに多くの機能があっても、ほとんどの開発者がそのうちのいくつかを意識すればよいだけなら、そのインターフェースの複雑さは、よく使われるものだけで考えても良いと主張している

感想

  • 確かにJavaのI/Oはイケてない。
    • それぞれのクラスが利用者にとってあまり価値のあるものになっていない。
    • 単独で使えないのがあんまり良くない気がする。
    • 内部的に分かれていても良いが、使う側にその内部事情を察してもらっているようなもの。
  • インターフェースに多くの機能があっても、よく使われるものだけを複雑さの基準にしても良いみたいなことを言っている
    • これってJSのArrayとかも該当するのだろうか
    • 機能は多いけど、map, filterとか人気どころだけなら簡単に使える

Chapter 4.8 Conclusion

まとめ

  • モジュールのインターフェースを実装から切り離すことで、実装の複雑さを隠蔽できる
  • モジュールを使う人は、インターフェースだけを理解すれば良い
  • モジュール設計をする際の重要な点は、一般的なユースケースに対して簡単なインターフェースを持ちつつも、重要な機能を提供できるようにすること(深いモジュール)

感想

  • 深いモジュール、浅いモジュールの話がインパクトデカいが、ここで筆者が伝えたかったのは、どちらかというと良いインターフェースを提供しなさいってことだったのかなと思った
  • 良いインターフェースを提供するためには、誰が何のために使うモジュールなのかをよく考える必要があるなと思った
  • 何でも汎用的なインターフェースを考えるのではなく、パッケージプライベートとかで使える人を限定するとか、可視性も重要なファクターっぽいな〜とか思ったりした
ゲントクゲントク
  • モジュールの提供者側から見て、モジュールの利用者側をどのように想定するか、なのかなと思った
  • モジュールの利用者を広く想定する(汎用的なモジュールにする)と、一部の利用者からは「冗長だ!」とされてしまう
  • UnixとJavaを比べるのが適切なのか?っていうのはちょっと思った(プログラミング言語のほうがより汎用的なモジュールとして設計されるべき?)
  • 想定する利用者の幅を狭めにして設計すれば、深いモジュールが作りやすい?
  • 例えばElmは、利用者をフロントエンド開発者に絞っている
ゲントクゲントク

Information Hiding (and Leakage)

  • 情報隠蔽と情報漏洩
  • この章と後のいくつかの章では、深いモジュールを作るためのテクについて解説する

5.1 Information hiding

  • 情報隠蔽
  • いわゆる「カプセル化」
    • モジュールの実装に知識を埋め込み、インターフェイスには必要最低限の情報だけを表す(他のモジュールに対しては知識を隠している)
  • 隠したい知識は、主にデータ構造やアルゴリズム
    • B-treeに情報を格納する方法、格納した情報を効率的に取り出す方法
    • ファイル内の論理ブロックに対応する物理ブロックを特定する方法
    • TCPの実装方法
    • マルチコアプロセッサでスレッドをスケジューリングする方法
    • JSONをパースする方法
  • なぜ情報を隠蔽すると複雑さが軽減されるのか
    • インターフェイスが簡略化され、開発者の認知不可を軽減するため
    • 情報がモジュール内に隠蔽されていれば、モジュール外に影響を及ぼさずに設計を変更することができるようになり、システムを拡張しやすくなるため
  • private修飾子を使ったからといって、そのプロパティやメソッドの情報を隠蔽したことにはならない
    • getter/setterなどによって外部に公開することが可能なため
  • 情報を部分的に隠蔽することにも価値がある
    • あるメソッドのもっとも一般的な使用例と、それ以外の使用例を別のメソッドとして設計すれば、メソッド単位では情報を隠蔽していることになる

5.2 Information leakage

  • 情報漏えい
  • 設計上の決定が複数のモジュールに反映されている状態のこと
  • モジュール間に依存関係が生じるため、設計変更の時、関係するすべてのモジュールに影響を及ぼす
  • 設計上の決定がインターフェイスに現れていない場合でも、情報の漏洩は起こる
    • 2つのクラスが特定のファイルフォーマットに関する知識を持っているとする(おそらく読み込みのクラスと書き込みのクラス)
    • どちらのクラスも、ファイルフォーマットについてインターフェイスに公開していなかったとしても、ファイルフォーマットには依存していることになる
    • ファイルフォーマットの変更が必要になれば、両方のクラスを修正する必要がある
  • バックドアによる漏洩は、インターフェイスを介した漏洩よりも悪質
  • 情報漏えいは、もっとも重要なレッドフラッグの1つ
  • 情報漏えいに対する高い感受性は、設計者として身につけるべき最高のスキルの1つ
  • 特定の知識が単一のクラスにしか影響しないようにするにはどうすればよいか、よく考える
  • 特定の共通した知識を持つクラスを統合して1つのクラスにしてしまうのもありだが、大抵の場合インターフェイスがでかくなってしまい、浅いモジュールになってしまう
Kyosuke AwataKyosuke Awata

部分的に隠蔽するって具体的にどんな感じ?

  • クラスがどういうメソッドを公開するのか?かなと思ったり
    • ドメインモデルを例に挙げると、createとreconstructを分けるみたいな感じ?
sushidesusushidesu

5.3 Temporal decomposition

時間的分解

  • 情報漏洩の原因の一つ: 時間的分解
  • 時間的分解
    • システムの構造が操作が発生する時間順序に依存する
    • 例: ファイル読み取りと書き込みでクラスが分かれているファイルを読み取って、編集して、再度書き込むという一連の流れを実装する際に、読み取りと書き込みを別のクラスにわける (=ファイルの上書き保存)
      • ファイル形式に関する知識が2つのクラスに表れている
  • モジュールを設計するときは、タスクが発生する順序ではなく、各タスクを実行するために必要な知識に焦点を合わせる
    • 順序は、情報隠蔽(の構造)と一致していない限りモジュールの構造に反映されるべきではない

感想

  • 時間的凝集ってあったかも
  • 特に慣れていないことをする時に、やりがちかも
    • excelの処理するやつでreaderとwriterに分けてしまっていたかも、、、
    • (輪読会を受けて訂正)↑一連の流れではなく、ファイルimport機能とファイルexportの機能という別の機能のための実装だったので、問題なかったのかも
  • 改めて意識したい: タスク実行に必要な知識に焦点を合わせる

5.4 Example: HTTP server

例: HTTPサーバー

  • ソフトウェア設計コースの学生が行った設計の例を見る
  • webサーバーがHTTPリクエストを受け取り、レスポンスを返すのを簡単にするためのクラスを実装しよう!というお題

感想

  • ほとんどはHTTPとは何かの説明だった
  • この次の章から具体的な設計の例が紹介されていくっぽい 次回に期待!
Kyosuke AwataKyosuke Awata

単一責任の原則には異議を唱えている?

-> 読んで変更して書いて、という一連の処理が確実に必要なら1つのクラスにする。それがそのクラスの持つべき単一の責任だから?

sushidesusushidesu
  • (Zennのはなし)「パーマリンクをコピー」したときにコピーされる専用のURLが用意されているのはなぜだろう?
    • 元のURLにはユーザー名が含まれており、ユーザー名が変わるとリンクが機能しなくなる可能性があるから?
    • もともとは 「パーマリンクをコピー」したときにコピーされる専用のURL が正しいURLで、仕様が変わった?
    • 仕様変更に対応できるようにするため?
Kyosuke AwataKyosuke Awata

Chapter 5.5 too many classes

紹介事例

多数の浅いクラスに分割してしまい、クラス間において情報漏洩が発生した

具体的にどんな感じか

HTTPリクエストを受けとるために、2つのクラスを使っていた

  • 1つ目のクラス ... ネットワーク接続からリクエストを文字列に読み取る
  • 2つ目のクラス ... その文字列の解析

何が問題なのか

モジュール間において時間的な依存関係が生まれている。

1つ目のクラスを呼んでからじゃないと2つ目のクラスは呼べない。

モジュール間に重複したコードが生まれている。

Content-Lengthヘッダーはリクエストボディの長さを指定するので、リクエストの合計の長さを計算するためには、ヘッダーを解析する必要がある。
ネットワークからリクエストを文字列に変換する場合にも、文字列からボディだけを取得する場合にも、この文字数が必要となる。

どうするべきだったのか

このケースなら、1つのクラスに統一するのが良かった。

特定の機能に関連するコードを1つにまとめることができる。
より良い情報隠蔽ができ、シンプルなインターフェースを提供できた。

補足

ただし、大きすぎるクラスが良いという訳ではない。
9章では、どういう時にクラスを分けることが理にかなっているのかを説明するとのこと。


Chapter 5.6 HTTP Parameter Handling

紹介事例

リクエストのパラメータを取得する際に、情報隠蔽ができていなかった。

前提

  • サーバーはリクエスト情報からパラメーターを取得する必要がある
    • クエリパラメーターから取得するかもしれないし、ボディから取得するかもしれない
  • 各パラメーターには、名前と値がある
  • 値は、空白や記号をエンコードしていることが多いが、サーバーはエンコードされていない形式を必要とする

ほとんどの学生がやっていた、良かった例

  • サーバーは、パラメータがクエリパラメーターとボディのどちらで指定されたか気にしないことにして、それらのパラメータを1つにマージするようにした
  • エンコードしていることを内部に隠蔽した

ほとんどの学生がやっていた、悪かった例

  • HTTPRequest型のクラスを作って、キーと値のマップを返すだけのメソッドを作っていた

何が問題なのか

  • 内部情報が完全に漏洩していて、もし内部実装が変わったらすべての使用箇所も修正が必要になる
  • Mapの参照を受け取るので、そのMapを変更したら...???というのも呼び出し側の考慮する点となってしまう

どうするべきだったのか

  • getParameterメソッドとgetIntParameterメソッドをHTTPRequestクラスに作る
  • 引数で受けたキーの値を返すだけで、内部情報は隠蔽できる
  • 存在しないキーだったら例外を投げるようになっている

Chapter 5.7 Defaults in HTTP Response

紹介事例

HTTPレスポンスオブジェクトを例に、デフォルト値の重要さを説明

具体的にどんな感じか

  • レスポンスオブジェクトを作成する際に、HTTPプロトコルのバージョンを明示的に指定するようなインターフェースにしていた
  • レスポンスの送信時刻を表すDateヘッダも(多分)明示的に指定するようになっていた

何が問題なのか

  • レスポンスとリクエストのバージョンは必ず一致すべきで、それを呼び出し側が知っていること自体が情報漏洩している
  • 一般的なユースケースにおいて、Dateヘッダを呼び出し元が指定する必要はない

どうするべきだったのか

  • どちらもデフォルト値をHTTPクラス側が設定するべき
  • 仮にオーバーライドしたい時があるのなら、そういう特殊なメソッドを作っておけば良い
    • その場合に呼び出し側がバージョンや時刻を知っていることは情報漏洩ではない

補足

可能な限り、クラスは明示的に要求されなくても正しいことをするべき。
最高の機能とは、その存在を意識することなく手に入れられるもの。

  • 26PのJava I/Oは、これを満たしていない
    • ファイルI/Oのバッファリングは普遍的に望ましく、明示的な要求は不要で、存在を意識する必要すらなかった
プラハの社長プラハの社長

5.8 クラス内の情報隠蔽

  • 情報隠蔽の原則はシステム内の様々な箇所、例えばクラス内の設計にも適用できる
    • インスタンス変数を参照するメソッドを減らせばクラス内の依存関係を減らせる
      • LCOMと近い考え方だなと思った

5.9 やりすぎ注意

  • 「外部に必要のない」情報を隠蔽するのが大切なのであって、必要な情報を隠蔽するのはダメ
  • 例えば特定のクラスのパフォーマンスを維持するためには設定パラメータが重要だとしたら、パラメータは外部に開かれていなければいけない
    • 理想はパラメータが自動的に最適化されることだけど
      • インターフェースが0になっている状態

5.10 結論

  • 各モジュールは可能な限り公開する情報を最小限にとどめることでシステム内の依存関係を軽減できる
  • 情報隠蔽を考えるほどモジュールは深くなっていく傾向にある。情報(や機能)が隠蔽されていないとモジュールはどんどん浅くなっていく

モジュールを分解する際の注意点

  • 時系列に分割すると暗黙的な依存関係が生じた浅いモジュールが出来上がりやすい
  • 時系列ではなく「この機能を実現するためにどのような知識が必要なのか」を考えて、いくつかの知識を隠蔽する形でモジュールを分割していくと深いモジュールになりやすい
    • DDDにおけるドメイン層がビジネスルール以外について何も知らない状態も、良いモジュール化の例と言って良さそう

6.10 汎用モジュールは深い

  • モジュールを汎用的に作るか特定の用途に特化して作るか、悩みどころ
    • 汎用的に作る
      • 将来のために今少しだけ時間をかけて設計を考える、という原則に合致していそう
      • 当初想定していなかった用途で使われることになった時、時間を節約できる可能性がある
      • 一方で未来を予測するのは難しいので、作ったものの使われないコードが生まれる可能性も高い
      • 汎用的に作りすぎると、今目の前にある問題をうまく解決できないかもしれない
    • 特定の用途に特化して作ること
      • 漸進的に開発していく、という原則に合致していそう
      • いざ汎用的にする必要が生じた時に初めてリファクタリングすれば良いのではないか
    • どちらにもメリットがありそうだが、どんな判断基準を使って使い分ければ良いのか?
      • unixに則れば「make each program do one thing well」の原則で特化の方が推奨されそうだしアジャイル開発の文脈でも推奨されそうだけど、この著者はどうなんだろうな
Kyosuke AwataKyosuke Awata

インスタンス変数を参照するメソッドを減らせばクラス内の依存関係を減らせる
LCOMと近い考え方だなと思った

  • これまでの考え方と少し違うのかな?と思った
  • あくまで深いモジュールを使った上で、最終的な複雑さを低下させる手法として紹介しただけ?
ゲントクゲントク

6.1 Make classes somewhat general-purpose

新しいクラスを作るときの設計の指針

はじめから汎用的なものを目指して設計する

  • 将来的な変更に対して柔軟に対応しやすい
  • 時間はかかるし大変だが、再利用性が上がり、後々の時間を節約することができる
  • とはいえ、ソフトウェアシステムの将来のニーズを予測することは難しい

はじめは必要な機能だけを持つように設計する場合

  • 汎用的な設計にしたところで、結果的に必要とされない機能も実装してしまうかもしれない
  • 汎用的にしようとしすぎたがために、特定の問題の解決が難しくなってしまうこともある
  • いま必要なものだけを構築し、今後必要になったものをインクリメンタルに実装していくほうがいいという意見もある

学生にやらせてみてわかったこと

  • はじめから汎用的なものを目指したほうが、よりシンプルで深いクラスができた
  • 「ある程度」汎用的に実装するというところにスイートスポットがある
  • モジュールの機能は現在のニーズを反映するべきだが、インターフェイスは将来的に複数のニーズをサポートできそうなものにするべき

6.2 Example: storing text for an editor

  • 学生にGUIテキストエディターを作らせてみた

GUIテキストエディター

  • 機能
    • ファイルの表示、編集
    • 同じファイルを異なるウィンドウで同時に見ることができる
    • undo, redoのサポート
  • ファイル操作の基礎となる、テキストを管理するクラス
    • ファイルをメモリにロードする
    • ファイルのテキストを読み込んで変更する
    • 変更したテキストをファイルに書き戻す

学生にやらせてみてわかったこと

  • 多くのチームは、テキストクラスに対して、特定の用途に特化したAPIを実装した
  • 生徒たちがテキストクラスに実装した、特定の用途に特化したAPI
    • backspaceとdeleteの実装
    • コピーや削除が可能な選択範囲機能の実装
  • これらのAPIを用意すれば、UIの実装が容易になると考えた
  • 実際には、UIの実装にはほとんど利益をもたらさなかった
  • deleteなどのほとんどのメソッドは、UIのコードの一箇所でしか呼び出されない
  • UIの実装者は、テキストクラスの大量のメソッドについて学ばなければならなかった
  • テキストクラスにUIの情報が漏洩していた
  • UIの操作を追加するたびにテキストクラスには新しいメソッドを定義しなければいけなかった
  • UIの開発者は、テキストクラスの開発にも追われることになった
  • クラス設計の目的の1つとして、それぞれのクラスが独立して開発できるようにすることがあったが、特定の用途に特化したAPIを実装してしまったことで、それができなくなってしまっていた
sushidesusushidesu
  • 深いモジュールのインターフェースは複雑になるか
    • 必ずしもならない気がする
    • ユースケース視点で考えると、たしかに無限のユースケースを合体したような複雑なインターフェースになってしまう
    • そうではなく、そのモジュールはどう有るべきか?という視点で考えるとシンプルなインターフェースにできる (気がする)
    • 例: reactのhooks (useState, useEffect...etc)
      • 引数も返り値もシンプルだけどいろんな事ができる
  • 賢くて深いモジュールを作るべきと言えるかも
sushidesusushidesu

6.3: より汎用的なAPI

  • テキストクラスをより汎用的にする
    • テキストクラスのAPIは基本的なテキスト機能に関してのみ定義する必要がある
    • テキストの変更に必要なメソッドは insertdelete だけ (position=(テキスト内での位置)を渡す)
    • positionを操作するメソッドとdeleteを組み合わせることでbackspaceキーもdeleteキーも実現できる
  • UI実装時: 特化APIを使用して書くコードよりも少し長くなるが、コードがより明白になる
  • クラス自体(?): コードが少なくなる
  • クラスに新しい機能を追加するときも、必要な機能はほぼあるため簡単

6.4: 汎用性はよりよい情報隠蔽につながる

  • 汎用アプローチは明確な分離を提供する
    • →より良い情報隠蔽をもたらす
      • テキストクラスは、UIの詳細を認識する必要がない
    • → 認知負荷も軽減できる
      • 再利用できるいくつかのメソッドについてだけ知れば良いので
  • 「誰が何をいつ知る必要があるかを判断する」ことが重要
    • 詳細を知る必要がある場合は、できるだけ明確にする
    • 知る必要がある情報をインターフェースの背後に隠すと、曖昧さが生じる
      • 例: 元のbackspaceメソッド
        • UI実装者はどのように削除されるのか?を知りたい
        • APIを見るだけではわからないので、backspaceメソッドのコードを読みに行く必要が生じる

感想

  • 作るときは逆から考えると良いかなと思った
    • 明確な分離(情報隠蔽)を意識する → 良い汎用的なAPIを作ることができる
    • 汎用的にしよう!だけだと抽象的なので
Kyosuke AwataKyosuke Awata

Chapter 6.5 Question to ask yourself

  • 汎用的なクラスの設計を認識することは、作成するよりも簡単
    • 「インターフェースをどこまで汎用的にするべきか?どこまで専用的にするべきか?」のバランスを見つけるために役立つ、自分自身に問いかけるいくつかの質問を紹介
  • 現在のニーズを全てカバーする、最もシンプルなインターフェースは何か?
    • API全体の機能を減らさずに、メソッドの数を減らすには、より汎用的なメソッドを作ることになる
    • メソッドの数を減らすことは、個々のメソッドのインターフェースがシンプルである場合のみ有効な手段
    • メソッドの数を減らすために、追加で引数が必要な場合は、単純化できていないかもしれない
  • このメソッドはどのような場面で使われるのか?
    • あるメソッドが特定の用途のために設計されている場合、専用的すぎるという赤信号になる
    • いくつかの専用的なメソッドを1つの汎用的なメソッドに置き換えることができるかどうか検討する
  • 現在のニーズにとって使いやすいインターフェースになっているか
    • この質問は、汎用化しすぎたかどうかの判断に役立つ
    • あるクラスを現在の用途で使うために、多くのコードを追加する必要がある場合、適切なインターフェースではない可能性がある

Chapter 6.6 Push specialization upwards(and downwords)

  • ほとんどのソフトウェアでは、専用的なコードを持たざるを得ない
    • アプリケーションはユーザーに特定の機能を提供するが、ここから専用的なコードを排除することはできない
  • ただし、専用的なコードと汎用的なコードは分離しておくべき
    • そのためには、専用的なコードをソフトウェアスタックの上または下に押し出す
      • 上に押し上げる(いわゆるユースケース層を挟むのはこれに該当しそう
        • 特定の機能を提供するアプリケーションの最上位クラスは、必然的にその機能に特化する
        • しかし、機能を実装するための低レベルなクラスにその特化性は必要ない
        • エディタの例では、UI層が特化されたコード、テキストクラスが汎用的なコードと言える
      • 下に押しやる(インフラ層に対して依存性を逆転させる話と似ている?
        • デバイスドライバはその一例
        • OSは、様々な種類でストレージ等、大量のデバイスをサポートしなければならない
        • それぞれのデバイスは、特化したコマンドセットを持っている
        • OSは「ブロックを読む」「ブロックを書く」という汎用的な操作のみインターフェースに定義している
        • デバイスの開発者が、そのデバイスに特化したドライバをOSが提供しているインターフェースを使って開発する
プラハの社長プラハの社長

6.7 エディタの取り消し(undo)機能の実装

  • 例えば文字列を選択して削除して別の箇所にスクロールした後、直前の編集作業を取り消す場合、テキストをもとに戻しつつ再選択して、かつスクロールの位置も戻さなければいけない
  • 1連の流れを取り消す機能が必要とされた

生徒の実装(悪い例)

  • textクラスに取り消し可能アクションのリストを格納し、取り消しに関する特化したメソッドを多数用意した。
    • 例:文字列の挿入などを行う際には、 自動的に編集内容がリストに追加された
    • 例:文字の選択を行う際はユーザインターフェースから取り消し用のリストに操作を追加するメソッドを追加で呼び出していた
  • 実際に処理を取り消したいときはユーザインターフェースから取り消しメソッドを呼び出して、取り消すアクションに応じて条件分岐していた。 テキストの編集に関することならテキストクラスの中で操作を取り消せるが、文字列の選択等に関する場合はユーザインターフェースから渡されたコールバックを実行する必要があった

この実装の問題点

  • 汎用的な用途特化した用途が1つのクラスに混在していた(取り消し可能なアクションの履歴を格納しておく汎用的な機能と、テキストの編集やカーソル選択などの特化したメソッド)
  • これにより テキストクラスとユーザーインターフェースの間に結合が生まれた
    • 新しい取り消し手段を追加するためには毎回テキストクラスを編集しなければいけないし、選択取り消しの場合はユーザーインターフェースからコールバックを渡さなければいけないし
      • OCPに違反してる感じがする

解決方法

  • 取り消し可能な操作履歴を管理しておく汎用的なモジュールを切り出すことで解決できる
  • こんな感じだろうか
    • selectの取り消し処理はどう実装するんだろう?textクラスでは実装せずUI側で実装するノリなのかな

History

  • HistoryはActionの配列を持つ
    • Historyの責務はactionsを1つずつ取り出してActionのundo/redoを実行することのみ。
    • 複数のアクションを1つの取り消し可能の処理としてまとめるためにフェンスを設置するメソッドも用意されている(この章の冒頭に出てきた複数処理の取り消しがこれで実現できる)

Action

  • Actionの責務はundo/redoメソッドの細部を実装して、各処理の具体的な取り消し手順に関する知識を持つこと
  • こちらは特定の操作に特化したクラスになっている。テキストクラスはUndoableInsert みたいなクラスを実装するかもしれないし、ユーザーインターフェースのクラスはUndoableSelect みたいなクラスを実装するかもしれない

この実装のキモ

  • 機能全体を3つの異なるカテゴリに分割したこと
    • 取り消し可能のアクションをまとめて管理してundo/redoを 呼び出す汎用的な仕組み
    • アクションの具体的な取り消し処理を格納しておく特化的な仕組み(複数のクラスが実装するが、 それぞれのクラスはアクションに関するごくごく小さな知識を持つだけで済む)
    • 複数のアクションを1つにまとめる仕組み
  • それぞれのカテゴリはお互いに関して何も知らなくても実装できる。Historyは 自分が取り消すアクションが何なのか知る必要はないし、アクションは 自分自身が制御する取り消し処理についてのみ知っていれば良いし、どちらもアクションのグループ機能について知る必要は無い
  • 特に重要だったのは機能を汎用的な部分と特化的な部分に分けて、特化的な部分はアクションにまとめたこと。そこさえ分けてしまえば、残りのデザインは自然に仕上がった
    • わかりやすい文章の 書き方にも通じるものを感じた。脈略のない段落とか章の分け方をされると理解しづらくなるけど、テーマごとに区切られているとわかりやすくなるイメージかも
  • 補足: 汎用的な機能と特化的な機能を分けるのは双方のコードが近しい機能に関する知識を共有している時だ
    • 例えば今回の例だと汎用的な取り消し機能と直感的なテキスト編集の取り消し機能はコードを分割した方が良い
    • 一方で異なる機能を実現している汎用機能と特化機能は、くっつけた方が良い時もある。例えば今回の例ではテキスト編集に関する汎用的な機能を提供するテキストクラスに、特化的な取り消し機能が実装されている(undoTextEdit的な?)
    • 汎用的な取り消しクラスに実装するよりもテキストクラスの方が実装場所としてはしっくりくる。なぜならテキスト編集の取り消し機能はテキスト編集に関する文脈でしか登場しないから

6.8 特別なケース(特例)をコードから排除する

  • ここまではクラスやメソッドから 特化的な機能を排除することについて考えてきたが、メソッドボディーの中にも特化的な機能を排除する余地がある
  • if文にまみれたコードは不具合を起こしやすくなるので、できれば特例を作らずに自動的に処理される仕組みを考えたい

  • 悪い例
    • 生徒が作ったテキストエディタの例ではカーソルが選択状態にあるかどうかを判定するフラグが用意されていた
    • コードの様々な部分で「もしカーソルが選択状態でなかった場合はhoge」 このような特例に関する高度がそこら中に誕生した
  • 良い例
    • カーソル未選択状態は、開始位置と終了位置が一致しているカーソル状態として扱う
    • 未選択の状態でコピーペーストをしたとしても0バイトをコピーペーストするだけなので実際は何も起きないし、不具合も起きないようにする

6.9 結論

  • 特例を完全にコードから削除することはできないが、不必要な特化機能や特例はシステムの複雑さを増大させる
  • 適度に汎用的なコードを混ぜた方がモジュールは深くなるし、知識は隠蔽されるし、シンプルでわかりやすいことになりやすい
ゲントクゲントク

Different Layer, Different Abstraction

  • ソフトウェアシステムはそうで構成され、上位の層は下位の層が提供する機能を利用する
  • 各層はそれぞれ抽象を提供する
    • ファイルシステム
      • 最上位層はファイルの抽象化を実装している
      • 次の下位層は固定サイズのディスクブロックのメモリ内キャッシュを実装している
      • 最下層はデバイスディバイダーで構成されており、二次記憶装置とメモリ感でブロックを移動させる
    • TCPのようなネットワーク伝送プロトコル
      • 最上位層はあるマシンから別のマシンへ確実に配信されるバイトのストリームを抽象化している
      • 次の下位層はマシン間で、ベストエフォート方式でサイズに制限のあるバケットを送信する
  • 似たような抽象度を持つレイヤーが隣接している場合、クラスの分解に問題があることを示唆するレッドフラッグである
  • この章ではそのような問題が発生する状況と、その結果生じる問題、その問題を解決するためのリファクタリングの方法について説明する

Pass-through methods

  • 隣接するレイヤーの抽象度が似ている場合、パススルーメソッドという形で現れることが多い
  • 引数を受け取ってそのまま別のメソッドに渡すだけのメソッドのこと
  • パススルーメソッドはクラスを浅くする
  • クラスのインターフェイスが複雑になるが、システムの機能は増えないため
  • TextAreaのinsertStringメソッドのシグネチャが変更された場合、TextDocumentのシグネチャも変えなければいけない
  • パススルーメソッドは、クラスの責任分担に混乱がある
    • TextDocument クラスは insertString メソッドを提供しているが、テキストを挿入する機能はTextAreaに実装されている
  • パススルーメソッドを見つけたら、2つのクラスについてよく考え、それぞれのクラスがどの機能と抽象化を担っているのかを自問する
  • 解決策は、クラスをリファクタリングして、各クラスが明確で首尾一貫した責任の集合を持つようにすること
sushidesusushidesu

7.2 インターフェースの重複

明確な責務がある場合は、シグネチャ(インターフェース)が同じでも問題ない

  • それぞれの新しいメソッドが重要な機能に貢献することが重要
    • 同じシグネチャを持つメソッドがあることは必ずしも悪いことではない
      • OSのディスクドライバ: 異なる種類のディスクをサーポートするために同じインターフェースの実装がある
    • パススルーメソッド → 新しい機能を提供しないので✗
    • 例: dispatcher
      • このシグネチャはdispatcherが呼び出すメソッドのものと同じ (ことが多い)
      • しかし、いくつかのメソッドの中から選択するという便利な機能がある
        • WEBサーバー: リクエストに対して処理を選択する

7.3 Decorators

多くの場合、デコレータを作るよりもいい方法があるはず

  • デコレータデザインパターンは、レイヤ間でのAPIの複製を促進する
  • デコレータクラスは
    • 浅い傾向がある
    • 少量の新機能のために大量の定型文を導入する必要がある
  • JavaのI/Oのように浅いクラスが爆発的に増加する
  • 代替案
    • 新しい機能を基となるクラスに直接追加できないか?
    • ユースケースにマージできないか?
    • 既存のデコレータとマージできないか?
    • 既存機能をラップせずに、既存機能に依存するスタンドアロンクラスとして実装できないか?
      • 継承より移譲と似ているかも
sushidesusushidesu
  • dispatcher、呼び出す先のメソッドの粒度を揃えるべきだと思います
  • デコレータクラスが悪いわけではなく、浅いデコレータクラスが問題
Kyosuke AwataKyosuke Awata

Chapter 7.4 Interface versus implementation

  • クラスのインターフェースと、内部実装は異なるべきである
    • これらがほぼ同じなら、うまく抽象化できておらず、浅いクラスになっている
  • 6章の例
    • テキストクラスの内部実装が行単位で文字列を扱うようになっていた
    • インターフェースも行単位を意識したものになっていた
    • ユーザーが欲しいのは、insert/deleteのようなインターフェース
    • 内部実装をうまくカプセル化して、そのクラスをより深くしていく
    • 前の章で出てきた特殊すぎるユースケースにならないように、バランスを取る必要はありそうだなと思った

Chapter 7.5 Pass-through variables

  • パススルー変数とは ... 長い一連のメソッドを通して渡される変数のこと
    • 中間メソッドは、その変数をただ受け渡すだけなのに、存在を認識せざるを得ない
    • 新しい変数が出現した場合に、変数を関連する全てのメソッドの渡していくため、変更量が増える
  • しかしこれを排除するのは難しい
  • 紹介されている解決案①
    • 共有オブジェクトを定義して、最上位メソッドがそのオブジェクトを作成、最低位オブジェクトはそのオブジェクトがあれば使う
    • この共有オブジェクトどこで作られてるの?となりそう。コンストラクタでDIっぽくやるならアリだと思う。そうすれば使う側の存在チェックいらないし。
  • 紹介されている解決案②
    • グローバル変数を使う
    • 当然ながらあまりおすすめはされていない
  • 紹介されている解決案③
    • コンテキストオブジェクトの導入
    • 筆者はこれが多いらしい
    • ReactのuseContextと同じっぽい感じがする(あれはシステムのインスタンスごとに1個ではないと思うが)
    • コンテキストはおそらく多くの場所で必要とされるので、パススルー変数になりやすい
    • コンテキストの参照をシステムの主要なオブジェクトのほとんどに保存しておくことでこれを避ける
    • コンテキストのデメリット
      • その変数何?どこで使われてるの?みたいなものが生まれてしまう可能性がある
        • ルールを作りましょうという割とフワッとした対応が書かれていた
        • 複数箇所で使われる場合限定+例外的に必ずコメントを書くとかどうだろう
      • マルチスレッド環境で問題が起こるかもしれない
        • イミュータブルにすれば問題ない
    • それでも筆者はコンテキストより良い解決策をまだ見つけていないとのこと

Chapter 7.6 Conclusion

  • インターフェース、引数、関数、クラス等を追加するたびに開発者は学習が必要で、複雑さは増す
  • これらを追加したことで増えた複雑さ以上の利益をもたらすためには、これらを追加していない時に存在していた複雑さを取り除くことが必要
    • そうでないなら無駄な複雑化になっている
    • 例えば機能を追加してもカプセル化することで複雑さを軽減できている
  • パススルー変数は、機能は追加されないのに複雑さだけを増してしまっている
プラハの社長プラハの社長

8章全部!

複雑さを引き下げる(pulling down)

  • 避けられない複雑性はモジュールの奥深くに隠蔽すること
    • 多くのモジュールは開発者の数より利用者の数の方が多いので、開発者が複雑性を担保した方が良い
      • 名言
    • シンプルな実装よりシンプルなインターフェースを目指すべき
      • また名言
  • 複雑性がモジュールの表層部に出てきてしまうと、こういうモジュールになりがち:
    • 例外を頻繁に投げるので、利用者が対処の術を考えなければいけない
    • 設定パラメータをexportするので、利用者が設定を学ばなければいけない
      • zero configurationって書いてるだけでfwってそれなりに流行るイメージある

テキストエディタの例

  • 大半の生徒は行ごとの操作をインターフェースとして公開していた。これはファイルの実態が行ごとにテキストを管理していたから、そのまま公開する方が開発者にとっては楽だった
  • しかし大半の操作は行ごとではなく文字ごとに行われるので、文字ごとに変更するインターフェースが外部に公開されてる方が利用者にとっては都合が良い
  • その分モジュールの複雑さは増すけど、それは開発者が頑張れば良い

configパラメータ

  • configパラメータは複雑性を上(利用者側)に押し付ける例
  • 一部の人は「configがあれば利用者が最適化できるから良いのでは」と思うかもしれない
    • 例えばリクエストの再トライを例に考える。特定のリクエストは優先度が高いから再トライの間隔を狭めたいとか
  • しかしconfigは、本来モジュールが対処すべきことから逃げて他の人に押し付けている可能性がある
    • 例えばリクエストの再トライなら、今まで成功したリクエストのレスポンスタイムに係数をかけて動的に再トライの時間を決められるはず
  • configを作る際は「利用者は自分たち以上にこの設定を最適化する余地はあるのか?」と考えるのが大切。可能な限り内部で自動化したい
  • configを作って渡すのは問題を完全に解決せず、不完全なソリューションを提供することに等しい。不完全なソリューションはシステムの複雑性を増す
    • 認知的負荷が高まるからかな

やりすぎ注意

  • 複雑性を引き下げることをやりすぎる1番の例はアプリケーションのコードを全て一つのクラスにまとめること。これは当然意味がない

  • 複雑をモジュールの深いところに引き下げるべき条件がある

    • 引き下げようとしている複雑性がモジュールの機能に深く関連している
    • 複雑性を引き下げることでアプリケーション全体でシンプルになる箇所が増える
    • 複雑性を引き下げることでモジュールのインターフェースがシンプルになる
      • これってOR条件なのかAND条件なのか気になった。つまり複雑性を引き下げるのは積極的に行われるべきなのか、ここまで厳しい条件を満たした時のみやるべきなのか。ここまでの筆者の書き方的に前者(OR条件)な気はするけど
  • 例えばテキストクラスの例ではUIとテキスト管理を一つのクラスにまとめることで複雑性は引き下げられた(backspace keyを押した時のメソッドを呼び出せば文字が一つ削除される)が、UIとテキスト管理は深く関連している機能ではないし、利用者にとってのインターフェース単純化にも貢献していない

    • この例では情報の漏洩につながっていた
  • make each program do one thing very wellというunixの思想と深いモジュールの考え方はもしかしたら相反するのではないか(unix的な思想だと小さなクラスが出来上がる、という理解をしていたため)、という話を以前したが、読み進めた感じは近い結果に至りそう

    • 「モジュールが深くなる=one thingをしていない(many thingsになる)」のではなく、「モジュールが深くなる=one thingをより上手くやる」、という感覚なのかもしれない
  • 最近めっちゃmysqlの設定値を調べてたから納得感の強い話だった。設定値を知るためだけの研修とかやってるし...でもdbはドメイン最適なチューニングが一番求められる領域だからconfigを潤沢に用意しておかないとしんどいんだろうか

ゲントクゲントク

Chapter9 Better Together Or Better Apart?

  • ソフトウェア設計におけるもっとも基本的な疑問のひとつ「2つの機能があったとして、それらを同じ場所に実装すべきか、分離して実装すべきか」
  • その決定をするときに考慮すべき要因について
  • 統合するか分離するべきかを決めるときは、「システム全体の複雑さを軽減し、モジュール性を向上させること」を目標にする

細分化による複雑性の増加

システムを小さなモジュールに分割することによって、分割する前には存在しなかった複雑性が生まれることもある

  • コンポーネントの数が増えたことによる複雑性の増加
    • すべてのコンポーネントを把握するのが難しくなる
    • 膨大な数の中から目的のコンポーネントを探し出すのが難しくなる
    • インターフェイスの数が増え、インターフェイスが増えるごとに複雑さが増していく
  • 細分化によって、コンポーネントを管理するためだけのコードが追加されがち
    • EntityとかValueObjectみたいなクラスは、それに該当するだろうか
  • 細分化によって、情報が分離される
    • コードが分離されると、開発者はコンポーネントを同時に見ることが難しくなり、その存在を意識することさえ難しくなる
      • エディタ上でペインを並べて表示するとか、editor.action.showHoverする(vscode)(なんて呼べばいいんだろう?)するとか、現代ではエディタ等の工夫でなんとかなりそう
    • それぞれが完全に独立していて、まったく気にする必要がないようなものであれば、分離しても良い
    • コンポーネント間に依存関係がある場合、分離は良くない
  • 細分化によって、コードの重複が生まれがち

コードを1つにまとめたほうがいいケース

密接に関係しているコードは一緒にしたほうがいい

  • 情報を共有している場合
    • たとえば、両方のコードが特定の文書の構文に依存している場合など
  • いっしょに使われる場合
    • どちらか片方を使うときは、もう片方もかならず使うような場合
    • 一方向の場合は、その限りではない
    • たとえば、ディスクのブロックキャッシュではほぼ必ずハッシュテーブルが使われるが、ハッシュテーブルはブロックキャッシュでのみ使われるというわけではない
  • 概念的に重複している場合
    • 両方のコード片を含む単純な上位カテゴリーが存在する
    • 例えがよくわからなかった
  • 片方のコードだけでは理解できない場合
sushidesusushidesu

9.1 情報を共有している場合はまとめる

  • HTTPサーバーの例 (5.4)
    • メソッド1: リクエストを読み取って文字列に変換する
    • メソッド2: 文字列を解析してコンポーネントを抽出する
    • 両方のメソッドがHTTPリクエストのフォーマットについての知識を持つ必要があった
    • 同じ場所で行うと、コードはより短く単純になる

9.2 インターフェースがシンプルになる場合はまとめる

  • まとめると元のインターフェースよりも単純/使いやすいインターフェースにできる場合がある
    • モジュールがそれぞれ問題に対する解決策の一部を実装している場合によく発生する
    • 例: HTTPサーバー
      • メソッド1には、メソッド2に文字列を渡すためのインターフェースが必要だった
      • メソッドが結合されると不要になった
  • まとめると機能を自動的に実行できる場合がある
    • → ユーザーが個別の機能を意識する必要がなくなる
    • 例: JavaのI/Oライブラリ
    • FileInputStream クラスと BufferedInputStream クラスがまとめられ、デフォルトでバッファリングが提供される場合、大多数のユーザーはバッファリングの存在を意識する必要がない

9.3 まとめて重複をなくす

  • 同じパターンのコードが何度も繰り返されている場合、重複をなくすことができる場合がある
  • 方法1: 繰り返されるコードスニペットを別のメソッドに切り出す
    • 繰り返されるコードスニペットが長く、置換メソッドのシグネチャが単純な場合に最も効果的
    • スニペットが1~2行しか無い場合は、あまりメリットがない可能性がある
    • スニペットが環境と複雑な方法で対話する場合は、複雑なシグネチャが必要になり、あまりメリットがない
  • 方法2: 問題のスニペットを1か所で実行するだけで済むようにリファクタリングする
    • 例: いくつかの場所でエラーを返す必要があるメソッドで、エラーを返す前にクリーンアップ処理が必要 (図9.1)
    • gotoを使用して、クリーンアップ+エラーリターンを1箇所にまとめることが出来る (図9.2)
    • メソッド切り出しとの違いはリターン処理までまとめらるかどうか?
  • gotoは一般的には悪とみなされているが、ネストされたコードからエスケープするために使用される状況では便利
    • ジャンプ先を読まないとreturnしているかわからないのでやっぱり微妙かも?
プラハの社長プラハの社長

特化したコードと汎用的なコードは分ける

  • 複数カ所から参照されるような共通の機能は、汎用的な機能を提供することに集中して、特化的な用途に関するコードを別のモジュールに分けた方が良い
    • テキストエディタの例
      • テキスト操作に関する汎用的な部分と、UIに関する特化的な部分は分けて作るのが最良
      • UIに関するコードが汎用的な部分に紛れ込むと、情報漏洩やインターフェースの増加につながる

どうやって分けることが多いのか

  • 一般的にアプリケーションは特化的なコードが上のほうに、汎用的なコードが下のほうに集まることが多い
    • db操作が下の方にあって、ドメインロジックがそれを扱うイメージに近いのかな
  • 同じ抽象化のレイヤーで 汎用的なコードと特化的なコードが同じモジュールに集まっている場合、片方を汎用的に、もう片方をその上に位置する特化的な用途に分割できないか考える

例1: 範囲選択と挿入カーソル

  • セレクターとカーソルを1つのクラスにまとめた生徒がいた
    • boolで 表現される状態がクラスに生まれた。カーソルが選択状態にあるのかどうか、選択領域のどちらの端にカーソルがあるのか、など
  • しかしこれは不自然なクラスだった
    • 問題1:このクラスを利用する側は選択とカーソルを異なる要素として認識しつつ、それらを別々に操作する必要があった
      • 例えば選択領域にテキストを挿入する際は、まず選択領域を削除するメソッドを呼び出して、その後にカーソルの位置を取得するメソッドを呼び出して、テキストを挿入する必要が生じた
    • 問題2: オブジェクトを分けるよりもさらに複雑な実装になった。カーソルの位置を取得するためには、そもそも選択状態になっているかをいちど判定してから、選択箇所のどちらの端にカーソルが当たっているのか判定する必要があった

どう分ければよかったのか

  • 範囲選択とかそれは1つのモジュールにまとめるほど近しい存在ではなかった。分割したほうがシンプルになった。カーソルポジションは選択範囲とブーリアンの計算から導かれるものではなく直接的に表現されるものになった。
  • さらに改善したバージョンではpositionクラスと言う概念が導入された。行数と、行の中の文字数を保管しておくクラス。選択領域は複数のpositionクラスをまとめることで表現され、カーソルは一つのpositionクラスで表現できた。
  • これは、より汎用的で低レイヤーなモジュールを導入することで複雑性が取り除かれた例の一つ
    • 本来異なるものであるカーソルと選択領域を一つのモジュールにまとめた、という意味では生徒が作成したクラスと同じことをやっているように見える。何が違うのか?おそらく「選択領域とカーソル」クラスを作るのか「position」クラスを作るのか、というクラスを捉える際の抽象度の違いなのではなかろうか。AとBという特化した機能を単純にまとめると不自然な状態になるから、Cという汎用的な概念を見つけてまとめるとしっくりくる、みたいな
  • こんな感じだろうか

例2:ログのために用意された別クラス

  • エラーが発生した際にメッセージをログに出力するクラスを別途用意していた生徒もいた。しかしこれだと非常に浅いメソッドになるし、そこそこのドキュメントが求められるし、1カ所でしか呼び出されないメソッドなのでクラスを分ける必要もなかった。
  • むしろ分けることによって確認の手間が増える
    • 上の階層(利用する側のコード)に手を加える人はクラスの定義を参照するためにジャンプする必要が生じる
    • 下の階層(ログ出力クラス)に手を加える人は用途を確認するために利用する側のコードにジャンプする必要が生じる
  • 自分は複雑なアプリケーションを読む時は、そのタイミングで興味のない部分は読み飛ばしたいと思うから、 1行ずつすべてのコードに目を通さなければいけない長いメソッドよりも、抽象的な名前の細かいメソッドに分けられていた方が「今はログに関する挙動は興味ないから飛ばそう」と思えるし、分けた方がやっぱり好きだなと思った。けど「そのメソッドの中で何をしているかわからない以上(未知の未知があるかもしれない)、一度は目を通さなければいけないのだから、コードリーディング中に読み飛ばすという選択肢がそもそもない」と言われるかな?
Kyosuke AwataKyosuke Awata

Chapter 9.7 メソッドを分けるか結合するか

  • 長いメソッドは短いメソッドより理解しにくい傾向があるので、多くの人はメソッドを分割する正当な理由として長さだけを主張する
    • 長さそのものがメソッドを分割する良い理由になることはほとんどない
    • メソッドを分割するとインターフェースが増え複雑さが増す
  • 長いメソッドが常に悪いわけではない
    • 20行のコードブロックが5個あるメソッドを例に挙げる
    • ブロックが独立している場合、ブロックごとに理解ができるので、メソッドを分けてもあまりメリットはない
    • ブロックの相互作用が複雑な場合、一度にすべてのコードを見ることができるようにブロックをまとめるべき
    • 長いメソッドであっても、シンプルなシグネチャで読みやすければ問題ない
    • それは深いメソッドなのだから
  • メソッド設計で最も重要なことは、きれいな抽象化。
    • メソッドは1つのことを完全に行うべき
    • シンプルなインターフェースながらも深い機能を提供するべき
    • この性質を備えているメソッドならば、長さは問題ではない
  • メソッドを分割することが意味を持つのは、全体で見たときにより綺麗な抽象化をもたらす場合のみ
    • 長いメソッド(親メソッド)をサブタスク(子メソッド)に切り出す
      • 親メソッドは子メソッドの実装は読まなくてよいし、子メソッドは親メソッドのことを意識する必要がない
      • 子メソッドが比較的汎用的な場合に有効
      • ただし親メソッドと子メソッドのどちらも理解しなければならないような場合は、よい分割にはなっていない
    • メソッドを2つに分割する
      • 密接に関連しない複数のことを行おうとしたために、過度に複雑なインターフェースになっている場合に意味がある
      • 分解したメソッドそれぞれは、元のメソッドの一部の機能を持つ
      • 必ず分割後のインターフェースのほうがシンプルになるような分割をする
      • ほとんどの呼び出し元が、分割後のうちどちらかだけを呼ぶようなら良い分割
      • 逆にほとんどの呼び出し元がセットで呼んでいるなら、それは複雑になっているだけかもしれない
      • 新しいメソッドが元のメソッドより汎用的で、何をするかに焦点を絞ることができているなら良い兆候
    • 呼び出し元が分割したメソッドのそれぞれを使って、その間で状態をやり取りして、みたいな状態は分割しないほうが良い
      • 結局呼び出し側にとっては複雑さが増している

Chapter 9.8 クリーンコードは別の意見らしい

  • Robert Martinは、関数は長さだけで分割されるべきであると主張している
    • 筆者も短い関数のほうが理解しやすいという点には同意をしている
    • とはいえ、数十行の関数と十行程度の関数では、そんなに影響ないのでは?と
    • それよりも関数を分割して、システム全体の複雑さが軽減されるのか?を考えるべき
  • 「1つの大きな関数を読む」のと「いくつかの短い関数を読んで、それらがどのように連携しているのかを理解する」の、どちらが簡単か?
    • 機能を小さくしすぎると、それぞれの機能の独立性が失われ、一緒に読んで理解しなければならないような結合した機能になる
    • これなら、大きな関数を残して、一箇所にまとめたほうが良い
  • まずは深い関数を作る。次に読みやすいように短くする。
  • 長さのために深さを犠牲にしてはならない。

Chapter 9.9 まとめ

  • モジュールの分割や結合は、複雑さを考慮して決定する必要がある
  • 情報隠蔽性が最も高く、依存関係が最も少なく、インターフェースが最も深い構造を選ぶ
ゲントクゲントク

10 Define Errors Out Of Existence

  • 例外処理は、ソフトウェアシステムを複雑にする最悪の原因の1つ
  • 特殊な状態を扱うコードは、通常の状態を扱うコードよりも書きにくい
  • 開発者は、例外がどのように扱われるか考慮することを忘れがち
  • この章で説明すること
    • 例外が複雑さの原因になる理由
    • 例外処理を単純化する方法
  • この章で得られる教訓
    • 例外処理をしなければならない場所を減らす
    • 場合によっては、操作のセマンティクスを変更して、通常の動作ですべての状況を処理できるようにし、報告すべき例外条件をなくす

10.1 Why exceptions add complexity

この本における「例外」の定義(?)

  • 著者は「例外」という言葉を、「プログラムの正常な制御の流れを変えるような、一般的でない状態」を指すものとして使っている
  • あらゆる例外は、複雑さを助長する
    • 低レベルのコードによってスローされ、周囲のコードによってキャッチされる、正式な例外の仕組み
    • メソッドが正常な動作を完了しなかったことを示す特別な値を返す仕組み

例外に遭遇する場面

  • メソッドの呼び出し元が不適切な引数や設定情報を提供した場合
  • 呼び出されたメソッドが、要求された処理を完了できない場合
    • I/O操作に失敗した場合
    • 必要なリソースが利用できなかった場合
  • 分散システムを利用している場合
    • ネットワークパケットが失われた場合
    • ネットワークが遅延した場合
    • サーバーがタイムリーに応答しなかった場合
    • ピアが予期しない方法で通信した場合
  • コードが内部の不整合を検出した場合
  • 処理する準備が整っていない状況を検出した場合

例外が複雑さを増幅させる理由

  • 大規模なシステムでは、多くの例外的な状況に対処しなければいけない
    • とくに、分散システムである場合や、フォールトトレラントである必要がある場合
  • 例外処理はコードの大部分を占めることがある
  • 例外の言語サポートは、冗長で不格好になりがち
    • 例)Javaのオブジェクトシリアライズとデシリアライズを使って、ファイルからツイートのコレクションを読み込むコード
      • try-catchだけ(実際に例外を処理するコードを除いたもの)で、正常系のコードよりも多くの行数を占めている
  • 例外処理コードと正常系のコードを関連付けることは、難しい
    • それぞれの例外がどこで発生するか明らかでないから
  • tryブロックでコードを分割するやりかたもある
    • 例外が発生する場所を明確にできる
    • tryがコードの流れを分断し、読みづらくする
    • 例外処理コードが複数のtry間で重複する可能性がある
  • 最近の研究では、分散データ集約型システムにおける壊滅的な障害の90%以上は不適切なエラー処理に起因しているということが判明している
  • 例外処理コードの失敗の発生頻度は(通常のコードで発生する例外と比べて)非常に低いため、デバッグで突き止めることが難しい

例外処理が発生したときに対処する方法

  • 例外が発生しても、進行中の作業をすすめ、完了させる
    • 例)ネットワークパケットが失われた場合は、再送信する
      • 例外が連鎖し、複雑さが発生する
        • おそらくパケットは実際には失われたのではなく、単に遅延しただけ
        • その場合、パケットを再送すると、ピアに重複してパケットが届くことになる
        • ピアが処理しなければならない新たな例外状態が発生する
    • 例)データが破損した場合は、冗長化してあるコピーから回復を試みる
      • 例外が連鎖し、複雑さが発生する
        • 復旧中に発生する二次的な例外は、一時的な例外よりも微妙で複雑な場合が多い
    • 例外の連鎖が発生する場合、最終的には例外を発生させずに例外を処理する方法を見つけなければならなくなる
  • 進行中の処理を中断し、例外を上のレイヤーに報告する
    • 例外が発生する前に行われた変更の取り消しなどを行い、一貫性を回復する必要がある
sushidesusushidesu

10.2 Too many exeptions

  • 不要な例外を定義すると、システムの複雑さが増加する
    • 例: Tcl言語のunsetコマンド
      • 変数を削除するコマンド
      • 変数が存在しない場合はエラーになる
      • 一般的な用途(クリーンアップ)ではそのエラーが邪魔だったため、キャッチして無視しなければならなかった
  • 難しい状況に対処するために例外を使用すると、複雑さ増加する
    • 呼び出し側に権限を与えているという捉え方もできる
    • しかし、定義側でもどうすべきかわかっていない例外は呼び出し側もどうすれば良いかわからない場合が多い
  • クラスによってスローされる例外は、そのインターフェースの一部
    • 例外が多いクラス = インターフェースが複雑、浅い
    • 例外は、インターフェースの要素の中でも特に複雑
      • 伝播する可能性があり、より高いレベルの呼び出し元とインターフェースに影響を与えるので
  • 例外処理による複雑さを軽減する方法は、 例外を処理する必要がある場所の数を減らす こと
    • 例外の複雑さは例外処理(の難しさ)に由来するため

10.3 Define errors out of existence

  • 例外処理の複雑さを解消する最善の方法は、処理する例外がないようにAPIを定義すること
    • = エラーという存在を消す
  • 例: Tclのunsetコマンド
    • unsetコマンドの定義を変更することで、例外を発生させなくて済むようにする
      • 🙅‍♂️ unsetは変数を削除する
      • 🙆‍♂️ unsetは変数が存在しないことを保証する
        • 変数が存在しない場合、目的は既に達成しているためエラーを報告する必要がない

10.4 Example: file deletion in Windows File deletion

  • ファイル削除
    • windows: ファイルが開かれている場合削除できない
      • ファイルを削除できない例外が発生する
    • unix: ファイルをすぐには削除せず、アクセスしているすべてのプロセスによってファイルが閉じられると削除される
      • 削除できない例外は発生しない (常に削除できる)

10.5 Example: Java substring method

  • substringメソッド
    • 範囲外の場合, 例外を発生させる
    • この調整を自動的に実行するようにすると、便利になり、APIが簡素化され、メソッドがより深くなる
    • Pythonのスライスは範囲外の場合空の配列を返す
  • エラーの多いアプローチ
    • エラーを回避/無視のために追加のコードを作成する必要があり、バグの可能性が高くなる
    • 実行時に予期しないエラーがスローされる可能性がある
  • エラーを存在しないように定義する
    • API が簡素化され、記述する必要があるコードの量が減る
  • バグを減らす最善の方法は、ソフトウェアをよりシンプルにすること
プラハの社長プラハの社長
  • 自由だから良いとは限らない(開発者に判断の余地を与えても、その判断ロジックにバグが忍び込む余地が生まれる)
  • suspenseとの違いは元の状態戻れるかどうか、処理をその場でぶった斬っても差し支えないか
  • 過程を保証したいのか、結果を保証したいのか(削除することが大事なのか、存在しないことが大事なのか)
sushidesusushidesu
  • エラーに遭遇した時に、存在しないように定義を変えられないか?と考えてみるの良さそう
  • (モジュール内で解決できる)例外を投げる = 知識の流出 (情報漏洩) につながりそう
プラハの社長プラハの社長

例外を排除するためにはいくつかのアプローチが考えられる

例外マスキング

下位のレイヤーで起きた問題を下位で処理することで、上位コードが問題を意識する必要がなくなる

これって前回の「例外という存在を消す」とどう違うんだろう?

  • 前回は「選択肢があれば例外を投げない方を選ぶ」、今回は「投げられた例外を下位で隠す方法」について言及している

tcpの例

tcp通信しているデータは欠損することがあるが、欠損したからといってすぐに例外を投げるようでは使い物にならない。その代わりに自動的に再トライした方が役に立つ。

nfsの例

物議を醸す例としてnfsが挙げられる。nfsでもサーバーがクラッシュしたりレスポンスを返せないとき、クライアントはリクエストを何度も送り直し続ける。その間アプリケーションはハングする。一定時間を超えたらクライアントに「nfs server hogeはレスポンスを返しません。まだ再トライ中です...」というメッセージをプリントする。これも例外の存在を下位レイヤーが処理しているため上位レイヤーが意識しなくて済んでいる例。

一部のユーザーからは「例外を投げるべきだ」との声も上がっているが、例外を投げると状況は悪化する。

まず必要なファイルにアクセスできない時点でアプリケーションにできることはほとんどない。結局リトライしなければいけないし、リトライを上位レイヤーが実装することになると、今までnfsレイヤーに一箇所だけ実装されていたリトライ機能が、より上位のレイヤーの複数箇所に実装されることになり、複雑性が増す。

クライアント側はそのさらに呼び出し側に向けて例外を投げることもできるが、きっと呼び出し側も何をしたら良いか分からないのでアプリケーション全体を停止せざるを得ない。結果的には(自動的にnfsレイヤーで再トライをするのと)同じことが起きる。

それならアプリケーションが全くサーバー側の問題を意識する必要なく、待ちくたびれたらアプリケーションを手動で停止する作りの方が良い

どんな時も例外マスキングが役立つわけではないが、複雑性を下に引っ張る手法の一つとして覚えておきたい。

例外集成

エラーハンドラを沢山定義する代わりに一つの汎用的なエラーハンドラで対応しよう、というテクニック。

webサーバーの例

urlに応じてリクエストハンドラが呼び出されて、各リクエストハンドラがurlのparameterを抽出するgetParameterを呼び出す状況。getParameterが対象とするparameterを見つけられなかった時、あるいは期待する情報ではなかった時(数値を期待していたところに文字列が来たとか)、例外を発生させるとする。

それぞれのリクエストハンドラで個別にcatchするより、そのままエラーを上位に伝播させて、最上位で一箇所でキャッチした方が全体のコード量が減らせる。

parameter未定義だろうが型違いだろうが、できることは同じ(エラーメッセージを作成してクライアントに返すこと)なので、一箇所にまとめてしまった方が良い。

なぜ例外集成は良いのか

カプセル化と情報隠蔽:ハンドラはメッセージの中身を知らない。getParameterはhttpエラーレスポンスに関する知識を持たない。こうすれば新しいメソッドがwebサーバーに増えた時もハンドラ側に変更を加える必要はない。

例外集成の注意点

使い所:エラーが起きる場所と、それをハンドリングする場所が階層的に離れているとき

例外マスキングとの関係:マスキングはエラーが起きる場所とハンドリングする所を近づけているので、例外集成とは考え方が異なる。一方で「最も多くの例外を捕捉できるところにハンドラを設置する」という考え方は共通している。

RAMCloudの例

オブジェクトを複数のストレージサーバに分散して保存する機能を提供する。ストレージサーバーが壊れた時、あるいは個別のオブジェクトデータが破損した時、バックアップのストレージサーバーから状態を復元する。

特定のオブジェクトが破損した時、原理的にはそのオブジェクトだけを修正すれば良いが、RAMCloudはストレージサーバー全体を破棄してバックアップから再構築する。

なぜそうするのか

エラーハンドラの数を最小限に抑えてバグを減らすため。特定のエラー(オブジェクトデータの破損)をより汎用的なエラーに昇格させる(ストレージサーバーの異常)ことで、例外を集成している。

当然パフォーマンス上のデメリットはあるがデータの破損自体そう頻繁に起きることではないので大きな問題にはなっていない。より頻繁に起きるエラー(パケットロスなど)に対して同じアプローチを採用するのは推奨できない。

Kyosuke AwataKyosuke Awata

Chapter 10.8 Just Crash?

  • 4つ目のテクニックは、例外発生時にアプリケーションをクラッシュさせること
    • ほとんどのアプリケーションには処理する価値のない特定のエラーが存在する
      • このエラーは処理が難しいもしくは不可能で、頻繁に発生するようなものではない
      • そのためクラッシュさせたとして、アプリケーションの全体的な使い勝手に影響は与えない
    • このような際に、診断情報を表示してアプリケーションをクラッシュさせることは最も単純な方法
  • 例えば、メモリ不足エラーはこれに該当する
    • メモリが枯渇したときにアプリケーションにできることはあまりない。
    • 最近は多くのメモリを積んでいるマシンが多いので、メモリが枯渇することはほとんどなく、仮に枯渇した場合はアプリケーションにバグがあることがほとんど。
    • よってこのエラーを処理する必要はなく、無駄な複雑さを生み出してしまう
  • 他にもクラッシュさせた方が良いエラーはたくさんある
    • I/Oエラー、ネットワークソケットを開けないなどの外部に依存するもの
    • 不整合なデータが生まれてしまうといった内部エラー
  • ただしクラッシュさせて良いかどうかはアプリケーションの要件に依存する

Chapter 10.9 Taking it too far

  • 例外を定義しない、例外マスキングというやり方は、例外情報がモジュール外部で必要とされない場合にのみ意味を持つ
    • 以下のようなケースでは、インターフェースが複雑になっても例外を公開するべき
      • ネットワーク通信のモジュールでは、ネットワークエラーが発生すると、モジュール自身がそれをキャッチして破棄し、問題がなかったかのように処理を続行する作りになっていた
        • このモジュールを使うアプリケーションは、メッセージが失われたり、ぴあサーバーに障害が発生してもそれを知ることができない
        • これらの情報がなければ堅牢なアプリケーションの構築ができなかった
  • 例外処理も他の設計と同じで、何が重要で何が重要でないかを判断する必要がある
    • 重要でないものは隠すべきで、その数は多ければ多いほどよい
    • しかし重要なものは公開しなければならない(21章でまた出てくる!)

Chapter 10.10

  • 特殊なケースはコードを理解しにくく、バグが発生する可能性を高くする
  • 特殊なケースの最も大きな原因に例外がある
  • よって以下のような手法を使って、例外は処理しなければならない場所を極力減らすことが重要
    • セマンティクスを再定義してエラー状態をなくす
    • 低レベルでマスクして影響を限定的にする
    • 汎用的な単一のエラーハンドラに集約する
  • これによりシステム全体の複雑さを大きく下げることができる
ゲントクゲントク

クラッシュさせるかどうかの判断基準になるかも?

  • エラーの深刻度
  • エラーの発生頻度
  • コントローラビリティ
ゲントクゲントク

11. Design it Twice

  • ソフトウェアの設計は、難しい
  • 最初に思いついた設計が最良の設計になるとは、考えにくい
  • 設計上の重要な決定をするときには、複数の選択肢を検討する

design-it-twice原則

  • 設計上の重要な決定をするときは、いくつかの異なるアプローチの設計を複数考えてみる
  • 妥当なアプローチは1つしかないと確信していたとしても、とにかく2つ目の設計案を考えてみる
  • 考えたそれぞれの設計案の長所/短所について考え、他の設計の特徴と比較する
    • モジュールとして使いやすいかどうかをもっとも重要視する
    • シンプルなインターフェイスを持っているかどうか
    • インターフェイスが汎用的かどうか
    • 効率的な実装を可能にするインターフェイスかどうか
  • 最終的に選択する設計は、複数の案のうちの1つかもしれないし、複数の案の特徴を組み合わせた新しいものかもしれない
  • どの案もとくに魅力的でない場合、既存の選択肢で明らかになった問題点をもとに、新たな選択肢を考える

GUIのテキストエディタの例

2回設計することのうまみ

  • システムの多くのレベルで適用することができる
    • モジュールの設計では、インターフェイスの選択において役に立つ
    • モジュールの実装では、シンプルさとパフォーマンスを比較して追求するときに役に立つ
  • 2回設計したところで、1,2時間時間が余計に掛かる程度で、実装時にすぐペイできる
  • 頭がいい人は、最初に思いついたアイデアを実現しようとするあまり、本来の能力を発揮できないことが多い
    • 頭がいい人は、「賢い人は最初からうまくいく」と無意識のうちに信じていて、複数の設計を試すことを「頭が悪い事」と考えてしまう
    • 実際は頭が悪いのではなく、問題が難しいだけ
    • 難しい問題に取り組むことは、楽しいことである
  • design-it-twice原則は、設計を良くするだけでなく、設計スキルの向上にもつながる
    • 設計を行い、複数のアプローチを比較する仮定で、設計の良し悪しを判断する要素を学ぶことができる
sushidesusushidesu

12 Why Write Comments? The Four Excuses

  • コード内ドキュメント(コメント)は設計において重要
    • 抽象化のため
    • コメントを書くプロセスが正しく行われれば、設計が改善される
    • ドキュメントが不十分な場合、設計の価値が失われる
  • 大事なこと
    • コメントはソフトウェアの品質を上げる
    • いいコメントを書くことは簡単
    • コメントを書くのは楽しい!

12.1 Good code is self-documenting

  • 「コードが適切に書かれていれば、コメントは不要」 ← おいしい作り話
    • コードで表現できない大量の設計情報が存在する
    • 各メソッドの機能, その結果の意味, 設計の根拠, メソッドを呼ぶ条件 など
  • 「クラスの機能を知りたい場合、コードを読めばいい」 ←大規模なシステムの場合現実的ではない
    • コードを読むのには時間がかかり、苦痛を伴う
    • コードを読むことを期待する場合、浅いメソッドが生成される
    • 確かに、JSの組み込みオブジェクトの動作を調べるときはJSDocか大抵MDNを見に行っているかも ライブラリの使い方を知りたいくらいならドキュメントしか読まない
  • コメントは抽象化の基本
    • コメントを書くことで、実装の詳細(複雑さ)を隠しながら簡略化された情報を伝えることができる
    • コメントがない場合、メソッドの唯一の抽象はその宣言
      • 名前, 引数, 結果の名前, 型
    • 宣言にはあまりにも多くの重要な情報が欠けている
      • 例: 部分文字列を抽出するメソッドの引数 startend
        • 開区間なのか半開区間なのか
        • start > end の場合何が起こるか
  • コメントは、人間の言語で書かれているべき
    • 精度は低くなるが、表現力が向上する
    • シンプルで直感的な説明になる
    • human languageではないものって何? if A then B else C みたいな擬似コードとか?

12.2 I don’t have time to write comments

  • 長期にわたって効率的に作業できるようにするためには、作成するために事前に余分な時間を費やす必要がある
  • 良いコメントはソフトウェアの保守性に大きな違いをもたらすため、コメントに費やした労力はすぐに報われる
  • 良いコメントを書くのに時間はかからない
    • 10%の時間で保守性が上がる
  • ドキュメントを記述する行為は設計全体を改善する重要な設計ツールとして機能する (詳しくは15章)
Kyosuke AwataKyosuke Awata

12.3 コメントが古くなり誤解を生む

  • コメントが古くなることはあるが、実際にそれが大きな問題になることはない
  • ドキュメントを最新の状態に保つには、多大な労力は必要ない
    • ドキュメントに大きな変更が必要になるのは、コードに大きな変更があった場合だけ
    • コードの変更はドキュメントの変更よりも多くの時間を要する
  • 16章で、コードの変更後にできるだけ簡単にドキュメントを更新できるようにドキュメントを整理する方法について説明する
    • ドキュメントの重複を避けることと、ドキュメントを対応するコードの近くに置くことが大事
    • コードレビューは古くなったコメントを検出し修正する優れた仕組み

12.4 役に立たないコメントばかり

  • これはみんな経験があるはずだ
  • ただ1度しっかりとしたドキュメントを書く方法を知れば、それは難しいことではない
  • 次の章で良いドキュメントの書き方、維持の仕方を紹介する

12.5 良いコメントのメリット

  • コードでは表現しきれなかった部分の補足ができる
  • 2章で紹介した複雑さ(変更の増幅・認知的負荷・未知の未知)の後ろ2つの解決に役立つ
    • 必要な情報を提供し、不必要な情報を無視できるようなドキュメントを書くことで認知的負荷を下げる
    • システムの構造を明確にすることで、どのような変更にどのような情報やコードが関連するのかを明確にすることで、未知の未知を減らすことができる
    • 2章で複雑さの原因は依存関係と不明瞭さと指摘したが、良いドキュメントは依存関係を明確にし、不明瞭さを解消できる
    • 次章で良いドキュメントの書き方、設計プロセスにドキュメント作成を統合し、設計を改善する方法について紹介する

12.6 コメントは失敗である

Robert Martin「コメントはぜいぜい必要悪である。もしプログラミング言語の表現力が十分に足りていて、プログラミング言語を巧く操って自分の意図を表現する能力があれば、コメントは全く必要ない。」

  • コメントの正しい使い方は、コードで表現できなかったことを補うこと
  • コメントなしで表現できないからコメントが必要だが、喜んでコメントを書くべきではない
  • 良い設計をすれば、コメントの必要性を減らすことができることには同意するが、コメントは失敗ではない
  • コメントが提供する情報はコードが提供する情報とは全く異なるものであり、この情報はコードでは表現できない
  • コードとコメントは表現しやすい情報が違うため、コメントの情報をコードに取り込めたとしてそれが改善に繋がるかは分からない
  • Robert Martinはコメントをコードに置き換えることを提唱している
    • コメントの代わりにそれをメソッドとして切り出し、メソッド名をコメント代わりに使う
    • これによって、超長い名前になってしまう
    • 結局この長い名前は不可解で、良いコメントより少ない情報しか提供できない
  • コメント = 失敗 という哲学によって、だれもがコメントを避けるのではと心配している
  • 良いコメントは失敗ではない。コメントはコードの価値を高め、抽象化を定義し、システムの複雑性を管理する上で基本的な役割を果たす
プラハの社長プラハの社長

良いコメントは、コードを読むだけでは分からないことを伝える

コードでは表現しきれないことを伝えるのがコメントの役割。

全くコメントが書かれていないと結局最初から最後まですべてのコードを読まなければいけない。コードは読み手にとって不要な情報も大量に含むため、気にしなくても良い事まで気にしなければいけなくて読むのが大変。これではソフトウェアを抽象化できていない。

良いコメントの例

例えば2つのインデックスが範囲を指定するのであれば、そのインデックスの両端も含まれるのか否かを説明するなど。そもそもどうしてその処理が必要なのか説明するなど。必ずAと言うメソッドをBの前に実行する、といった暗黙的なルールなど。

コメントの記法を決める

言語固有の記法などに則って一定の書き方に統一した方が良い。 他の人が読みやすくなるし、何を書くべきか毎回考えるとコメントを書かなくなってしまうので、初めからルールが決まっていた方が着手しやすい

コメントの種類

コメントはいくつかのカテゴリに分類される:

  • インターフェイスに関するコメント。そのクラスなりメソッドを実行するための事前条件や事後条件、引数の方や戻り値の型、そもそもその処理が必要な背景など
  • 構成要素に関するコメント。クラスのパラメーター等
  • コードが内部的にどのように動作しているか説明するコメント
  • 複数のモジュールを横断する知識について説明するコメント

コメントはDRYに保とう

コメントすべきか否か

コードを読めばわかる事はコメントしなくても構わない。例えば「let counter = 0」の横に「カウンターをリセットする」みたいなことは書かなくても自明

意識すると良いかもしれないルール

  • このコードを初めて目にした人がすらすらとコメントを書けるか?
    • もし書けるのであればコードは相当わかりやすく自明なので、コメントを書く必要は無いかもしれない
  • コードに使っている単語とは異なる単語をコメントに使う
    • 例えば「addHorizontalPadding」という メソッドのコメントを書くのであればpaddingと言う言葉を使わずに説明してみる。こうすることでコードと全く同じようなことをコメントで繰り返すことを防げるし、 パディングについて知らない人がコメントを読んでも理解できるようになる。 余白が左右の両脇に追加されることをコメントしたり、引数の数字が「px」という単位を使用していることをコメントに追加すれば、挙動も予想しやすくなる。
    • 例えば「getNormalizedResourceName」のnormalizeとは何か?resourceとは何か? こういう疑問に答えるためにコメントが役立つ
ゲントクゲントク
  • 何を書くべきでないかはわかったので、何を書くべきかについての説明
  • コードと同じレベルのコメントは、コードの繰り返しになる可能性がある
  • 「異なるレベル」 … 上位レベルと下位レベルがある
    • 13.3では下位レベルのアプローチについて
    • 13.4では上位レベルのアプローチについて

13.3 Lower-level comments add precision

  • コメントは、コードとは異なるレベルの情報を提供することで、コードを補強する
    • コードの背景にある理由
    • コードについてのより単純で抽象的な考え方
    • 直感的な情報

宣言のコードにコメントを書く

  • 精度に関する情報は、クラスのインスタンス変数、メソッドの引数、戻り値などの変数宣言をコメントするときにもっとも有効
  • 変数宣言の名前と型は、あまり正確でないことが多い
  • 以下のような情報をコメントで補うことができる
    • 変数の単位
    • 境界条件は包括的か排他的か
    • null値が許容される場合、それは何を意味するのか
    • 誰がリソースのfree/closeに責任を持つか
    • このリストには少なくとも1つのエントリーが含まれている、というような特性はあるか
    • ほとんど型やクラスで代用できそうな気もする
  • コメントに書かれる情報は、コードをすべて読めばわかる情報かもしれないが、それには時間がかかるし勘違いをする可能性もある
  • 「宣言のコメントには、コードからではわからないことを書くべきだ」について、「コード」というのはコメント(宣言)の隣のコードのことで、「アプリケーションのコードすべて」のことではない

よくない例1

// Current offset in resp Buffer
uint32_t offset;

// Contains all line-widths inside the document and number of appearances.
private TreeMap<Integer, Integer> lineWidth;
  • "current"が何を意味するのか不明
  • 行幅と出現回数がそれぞれどちらのIntegerなのかわからない
    • TypeScriptの場合、(Genericsだと難しいかもだが、関数の引数であれば、1つのオブジェクトにkey/valueでまとめて名前をつけるなど、工夫の余地ある)
  • line-widthの単位は文字数なのかピクセルなのかわからない
    • Domain Primitive使おう

よくない例1の改善例

// Position in this buffer of the first object that hasn't been returned to the client.
uint32_t offset;

// Holds statistics about line length of the form <length, count> where length is the number of characters in a line (including the newline), and count is the number of lines with exactly that many characters. If there are no lines with a particular length, then there is no entry for that length.
private TreeMap<Integer, Integer> numLinesWithLength;
  • widthではなくlengthを使うことで、単位がpixelではなく文字数であることがわかりやすくなる
  • 各項目の詳細な意味だけではなく、その項目がない場合の意味も記述している
  • 変数名も見直した

よくない例2

/* FOLLOWER VARIABLE: indicator variable that allows the Receiver and the PeriodicTasks thread to communicate about whether a heartbeat has been received within the follower's election timeout window.
* Toggled to TRUE when a valid heartbeat is received.
* Toggled to FALSE when the election timeout window is reset */
private boolean receivedValidHeartbeat;
  • コードと同じレベルの情報(クラス内のコードによって変数がどのように変更されるのか)を書くことに終始している
    • 有効な心拍(?)を受信するとtrueにトグルする
    • タイムアウト時間がリセットされるとfalseにトグルする

よくない例2の改善例

/* True means that a heartbeat has been received since the last time the election timer was reset. Used for communication between the Receiver and PeriodicTasks threads. */
private boolean receivedValidHeartbeat;
  • 変数を文書化するときは、動詞ではなく名詞で考える
  • その変数をどのように操作するのかではなく、その変数が何を表しているのか、に注目する
    • 前回エレクションタイマーがリセットされてから、心拍を受信したことを意味する。ReceiverスレッドとPeriodicTasksスレッドの通信に利用される
  • 操作方法を書かなくても、そのように操作されることが、より短い文章で理解できる

13.4 Higher-level comments enhance intuition

  • コメントでコードを補強する2つ目の方法は、直感的な情報を提供すること
    • コードよりも高いレベル(抽象的にする?)で書く
    • 詳細を省略し、コードの全体的な意図や構造を読み手が理解できるようにする
  • 高いレベルのコメントは、低いレベルのコメントよりも書くのが難しい
    • コードについて、コードとは異なる視点で考えなければならないから
  • 自分自身に問いかけてみる
    • このコードは何をしようとしているか
    • このコードのすべてを説明できるもっとも簡単な言葉はなにか
    • このコードでもっとも重要なことはなにか
  • エンジニアは細部にこだわる傾向がある
    • 細部に拘ることはいいエンジニアであるために不可欠なことではある
  • 優れたソフトウェア設計者は、細部から離れ、より高いレベルでシステムを考えることができる
  • システムのどの側面がもっとも重要かを判断することは、抽象化の本質

よくない例3

// If there is a LOADING read Rpc using the same session as PKHash pointed to by assignPos, and last PKHash in that readRPC is smaller than current assigning PKHash, then we pu assigning PKHash into that readRPC.
int readActiveRpcId = RPC_ID_NOT_ASSIGNED;
for (int i = 0; i < NUM_READ_RPC, i++) {
  if (
    session == readRpc[i].session
      && readRpc[i].status == LOADING
      && readRpc[i].maxPos < assignPos
      && readRpc[i].numHashes < MAX_PKHASHES_PERPC
  ) {
    readActiveRpcId = i;
    break;
  }
}
  • あまりに低レベルで詳細すぎる
  • コードと同じことをコメントに書いている
  • コード全体の目的や、このコードを含むメソッドにどのようにフィットするかについて説明されていない - コード全体の意図を説明していないので、コードが正しく動作するかどうか判断するのが難しい
  • 結果として、コードを理解するための役に立たない

よくない例3の改善例

// Try to append the current key hash onto an existing RPC to the desired server that hasn't been sent yet.
  • 詳細は書かれていないが、コード全体の機能をより高いレベルで説明している
  • この高いレベルの情報により、読者はコード内で起こるほぼすべてのことを説明することができる

よい例

if (numProcessedPKHashes < readRPC[i].numHashes) {
  // Some of key hashes couldn't be looked up in this request (either because they aren't stored on the server, the server crashed, or there wasn't enough space in the response message).
  // Mark the unprocessed hashes so they will get reassigned to new RPCs.
  for (size_t p = removePos; p < insertPos; p++) {
    if (activeRpcId[p] == i) {
      if (numProcessedPKHashes > 0) {
        numProcessedPKHashes--;
      } else {
        if (p < assignPos) assignPos = p;
        activeRpcId[p] = RPC_ID_NOT_ASSIGNED;
      }
    }
  }
}
  • このコメントは2つのことを行っている
    • コードが実行される理由の説明
    • コードが何をするのかという説明
  • そのメソッドが呼ばれる可能性の高い条件を記述することは非常に有効
    • とくに、そのメソッドが異常な状況下でしか呼び出されない場合
sushidesusushidesu

13.5 インターフェースの文書化

  • コメントの重要な役割は抽象を定義すること
    • 抽象 = 重要な情報のみに単純化されたビュー
    • コードは実装の詳細が含まれているため不適切
    • → 抽象を説明する唯一の方法はコメントを使用すること
  • インターフェースのコメントを実装のコメントから分離する
    • インターフェースのユーザーが実装の詳細にさらされないように
    • インターフェースのコメント
      • クラスまたはメソッドを使用するために知っておく必要がある情報を提供
    • 実装のコメント
      • 実装するために、クラスまたはメソッドが内部でどのように機能するかを記述
  • 例: 抽象化されたインターフェースコメント ( class Http {} )
    • クラスの全体的な機能を説明
    • 各クラスのインスタンスが何を表すのか
    • クラスの制限 (シングルスレッド)
  • メソッドのインターフェースコメントには高レベルの情報と、低レベルの詳細の両方が含まれる
    • 動作を説明する短文 (高レベルな抽象)
    • 引数と戻り値、引数値の制約、引数間の依存関係
    • メソッドの副作用
    • メソッドから発生する可能性のある全ての例外
    • 呼び出すための前提条件
  • 例: Bufferオブジェクトからデータをコピーするメソッド
    • メソッドの説明
    • 引数、戻り値の説明
    • 10章に従ってエラーを返さずに0を返している
    • どのように実装されているかについての情報を提供していない

例: IndexLooup クラス

  • 分散ストレージシステムの一部
  • インデックスを使用して効率的にオブジェクトを検索する
query = new IndexLookup(table, index, key1, key2);
while (true) {
  object = query.getNext();
  if (object == NULL) {
    break;
  }
  ... process object ...
}

このクラスのインターフェースコメントに含める必要がある情報はどれ?

  1. IndexLookup クラスがインデックスとオブジェクトを保持するサーバーに送信するメッセージの形式。
  2. 特定のオブジェクトが目的の範囲内にあるかどうかを判断するために使用される比較関数 (整数、浮動小数点数、または文字列を使用して比較が行われますか?)。
  3. サーバーにインデックスを格納するために使用されるデータ構造。
  4. IndexLookup が複数の要求を異なるサーバーに同時に発行するかどうか。
  5. サーバーのクラッシュを処理するメカニズム。

改善前のコメントの問題点

  • インターフェースではなく実装に関するコメントになっている
    • ユーザーはリモートプロシージャコールの名前を知る必要はない
    • プライベート変数について言及している
  • 明らかなことが含まれている

改善版

  • このクラスは範囲クエリを作成するために使用されます
  • 各インスタンスが単一の範囲クエリを表す
  • クライアントは getNext() を呼び出して、目的の範囲内オブジェクトを取得できる
  • getNext() が返す情報

isReadyメソッドの改善前の問題点

  • 実装に関するものがほとんど
  • 実装者にしか通じない名前が含まれている ( RESULT_READY )

改善版

  • isReady が何を意味するのかを正確に記述している
  • どんな時に呼び出すべきかが書かれている
プラハの社長プラハの社長

13.6~13.9まで

実装コメント

何をやっているのか(what)、なぜやっているのか(why)を書くべき。どうやっているのか(how)は書かない。それはコードを読めば分かるから。大まかに「何をやっているのか(what)」が事前に提示されているとコードを読み進めやすくなる。

t_wadaさんの「whyはコミットログに書く」方針は、開発がある程度進んだサービスなら有用な気がした。ただ新規開発したばかりのサービスだとコミットログに理由を書こうにも「その機能が必要だったから」みたいなことになりがち。かつ大きなコミットの一部としてコードが追加されることも多いので、その特定のコードが追加された理由にコミットログが言及していないケースも多そう。

どういう時に書くべきか

  • 長いループ処理もwhatを事前にコメントで伝えておくと読み進めやすくなる
  • スコープが長く、かつ重要なローカル変数はコメントで補足する価値があるかもしれない
  • バグ修正のために追加したコードがぱっと見で理解し辛い時など。バグを詳細に説明するissueへのリンクを貼っておいても良いかもしれない

実装コメントを書かなくても良いケース

  • めちゃくちゃ短いメソッドは自明なので省略しても構わない
  • スコープが短くて使用箇所が簡単に見渡せるローカル変数も十分読みやすいので省略可。大体のローカル変数は良い命名によって意図を十分に伝えられる

モジュール横断コメント

完璧な世界では全ての変更はその都度一つのクラスに留まる。しかし現実はそうならない。一つの変更理由に対して複数のモジュールを変更しなければいけないことがある。例えばネットワークプロトコルを変更したら送受信側の両方に変更が求められる。変更漏れを防ぐためにはドキュメントが必要になる

どこに書くかが問題

複数のモジュールが絡む場合、どこに書くか決めるのが難しい。

RAMCloudの例

RAMCloudはStatusというenumを定義していた。このenumはいろんなところで使われる。例えばstatusに応じたエラーメッセージをクライアントに返したり、statusに応じた例外ハンドラーを定義したり。

全ての処理はenumを参照していたので、enumの方に「statusを追加したら、以下のファイルを変更してください」とコメントを書いておいた。こうすればコメントがstatusを追加する人の目に必ず触れる。

ファイル名を変えた時に追従できないしもう少しコンパイラに仕事をさせた方が良いと思った。あるいは「statusに応じたメッセージを生成する」といった機能をenumに持たせた方が良いのではないかと思った。ただし、そのままStatusに持たせると汎用的な機能に特化的な機能(クライアントのビューに関すること)が混じってしまうので、それならStatusMessageみたいな派生クラスを作り、statusが増えたらStatusMessageの実装も増やさなければいけない、みたいな作りにした方が、変更忘れをコンパイラが指摘してくれるので良いのではないか

ただモジュールが言語的にも異なる場合は上記の対策は取れないのでコメントに頼らざるを得ない。例えばstatusを増やしたらdbのマスタにもinsertしなければいけない、とか。でもこのケースならテストを書いた方が良いかもしれない(enumの全statusをdbから取得してみるとか)

その他の例

しかし上記のenumのような置き場所が見つからないことの方が多い。関連する全てのモジュールにコメントを重複記述することも可能だが、変更が増幅されるので好ましくない。

著者が最近実験している方法を紹介する。モジュールを横断するコメントを全て「designNotes」にまとめておく。そして関係するモジュールには「designNotesのxxxを参照せよ」とだけ書いておく。こうすればコメントを一元管理できる。

ただし関係するモジュールとコメント自体が異なる場所に置かれているため、問題が生じ得る。最新のシステムの状態がコメントに反映されていないとか。

アイデアは悪くないが実装方法にもう一工夫欲しいところ。designNotesの文字列に対して暗黙的な参照を作る(zombiesを参照せよ)よりも、githubのwikiに作成してheaderへのpermalinkを取得するとか。一つのdesignNotesにまとめるのではなく分けておいてリンクを取得するとか。これなら項目名が変わった時に変更しなければいけないところを検索しやすい

まとめ

コメントの価値は、読み手がコードを素早く理解して、容易く自信を持って変更できるようにすること。

コメントは読み手に対して意味を持つので、もし読み手に「分かり辛い」と指摘されたら反論してはいけない。何が分かりづらさを生み出したのか考えて、より分かりやすいコード、より分かりやすいコメントを書くことに努めなければいけない。

IndexLookupクラスのinterface commentに書くべきこと、書かなくても良いこと、の答え合わせ

  1. IndexLookupがサーバーに送信するメッセージのフォーマット:No. これは実装に関する詳細なのでコメントすべきではない
  2. lookupを実施する際の比較関数(どういうデータ型に対して比較を実施するのか):Yes. このクラスを使用する人が知らなければいけない情報だって書いてあったけど型づけ言語ならNoな気がした
  3. サーバーがインデックスを補完する際に使用しているデータ構造:No. ユーザーはこれについて知る必要はない。むしろIndexLookupすら、この情報を知る必要はない(DBに隠蔽されているから)
  4. IndexLookupが複数リクエストを並行実施するか:Maybe.パフォーマンスが気になるユーザーにとっては重要な情報かもしれない
  5. サーバークラッシュをハンドリングする仕組み:No. サーバは自動的に復旧するのでアプリケーションレベルのソフトウェアが気にすべきことではない。もしクラッシュしたことがアプリケーションに反映されるのであれば、どのように現れるのかinterface commentに記載した方が良いが、いずれにせよ復旧の仕組みまでは詳しく解説する必要はない

自分は2がNoだと思っていた。それ以外は概ね同じだった

プラハの社長プラハの社長
  • テストに関する言及がここまでないのどうしてだろう?結構設計に影響しそうだし、コメントでカバーしていた内容も一部テストで代用できそうなんだけど
Kyosuke AwataKyosuke Awata

14 名称の選択

  • 変数名やメソッド名を決めることは、設計において最も神経質になるべきことの1つ
    • 良い名前はコードを文書化し、理解しやすくする
    • 良い名前は補足ドキュメントの必要性を減らし、エラーの発見を容易にする
    • 悪い名前はコードを複雑にし、曖昧さや誤解を生み、バグを生む可能性がある
  • 名前の選択は「複雑さは漸進的である」という原則の一例
    • 特定の変数に平凡な名前を選択しても、システム全体でみれば複雑さはあまり変わらない
    • しかし、これを何千もの変数に対してやると、複雑さは増幅していく

14.1 例:悪い名前はバグの原因になる

  • SpriteというOSを作成していた時の話
    • blockという変数名を、「ディスク上の物理的なブロック番号」と「ファイル内の論理的なブロック番号」として、複数の意図で使っていた
    • blockという名前だけだと、使われている場所で必要な方に自動変換されてしまい、間違っていることに気づけなかった
    • diskBlockfileBlockのように、異なる変数名にしておくべきだった
      • もしくは型を分けてしまうというのもあり
    • blockという変数名も悪くはないが、まあまあ近い程度の名前に甘んじてはいけない
    • 少し時間をかけて「正確で曖昧さのない、直感的に理解できる名前」を選ぶ

14.2 イメージを作る

  • 読み手がその名前を読んで心の中で、そのものの性質についてイメージできるような名前を選ぶべき
  • 良い名前はその実態が「何であるか」と同時に「何でないか」ということについて多くの情報を伝える
  • 名前を考えるとき、自分に問いかけてみてほしい
    • この名前を単独で見た場合に、インターフェースやコメント、使われているコード等を見ないで、何を指しているのかをどれだけ詳しく推測できるか?
    • より明確なイメージを描けるほかの名前はないか?
  • 1つの名前につけられる情報量には限界があり、2~3語以上の単語を含む名前は扱いにくくなる
    • その実体の最も重要な側面をとらえることが大事
    • 名前付けも他の抽象化と同様に、基本的な実体について最も重要なことに注意を向け、重要でない細部を省略したものである
ゲントクゲントク

14.3 Names should be precise

  • 「良い命名」が持つ2つの性質
    • 正確さ
    • 一貫性
  • 今回は正確さについて
    • 命名について起こる問題のほとんどは「汎用的すぎる」とか「曖昧である」とかである
    • コードが何をしているのか誤解されてしまう

例1

/**
* Returns the total number of indexlets this object is managing.
* /
int IndexletManager::getCount() {...}
  • 何のcountなのか、ドキュメントを読まないとわからない
  • numActiveIndexlets であれば、ドキュメントを読まなくてもわかりそう
  • レッドフラッグ: 曖昧な名前
    • 変数名やメソッド名の指すものが広すぎる場合、開発者を誤解させ、誤用させる可能性が高まる

例2

  • ファイル内の文字の位置を表すのにx/yを使うのはよくない
    • x/yは座標(ピクセル)を表すのに使われがち
    • charIndex/lineIndexのほうがいいかも

例3

// Blink state: true when cursor visible.
private boolean blinkStatus = true
  • blinkValue
    • 何が点滅しているのかわからないし、statusもbool値としては曖昧すぎる
    • cursorVisibleとかがいいのでは
// Controls cursor blinking: true means the cursor is visible, false means the cursor is not displayed.
private boolean cursorVisible = true;
  • こっちのほうがよさそう

例4

// Value representing that the server has not voted (yet) for anyone for the current election term.
private static final String VOTED_FOR_SENTINEL_VALUE = "null";
  • VOTED_FOR_SENTINEL_VALUE
    • VALUEがなんなのかわからない
    • NOT_YET_VOTEDとかのほうがいいのでは

例5

  • 戻り値のないメソッドでresultという変数を使用している
    • resultが戻り値であるように誤解させる
    • resultが具体的に何なのかわからない
      • mergedLineとかtotalCharsのような名前であれば、どのような「結果」なのかがわかりやすい
      • 戻り値があるメソッドであれば、resultという変数名はむしろよいことがある
        • それが最終的にメソッドから返される値であることを意識してコードを読める

例6

  • Linuxカーネルのstruct socketstruct sock
    • struct sockstruct socketのサブクラスだが、名前が似ていて非常に紛らわしい
    • struct sock_basestruct inet_sock のように、2つの型の関係を明確にし、区別しやすい名前を選択するとよい

例7

for (i = 0; i < numLines; i++) {
  ...
}
  • 前述したルールにも例外がある
    • たとえばforループの中で使うイテレータは、スコープが非常に狭いので、ijを使っても良い

例8

void delete(Range selection) {...}
  • 変数名を具体的にしすぎてもよくない
  • この例の引数は、任意の範囲のテキストに対して呼び出せるので、selectionという命名では限定的すぎる
  • rangeとかでよい

命名が難しいとき

  • 正確で直感的、かつ長すぎない名前を思いつくのが難しい場合、これはレッドフラッグかも
    • 変数が明確な定義や目的を持っていないかも
    • 変数の分解を検討する
      • たとえば、1つの変数で複数のものを表現しようとしている場合、その表現を複数の変数に分けることで、各変数の定義がよりシンプルになる
    • いい命名を心がけることによって、設計が改善される
sushidesusushidesu

14.4 Use names consistently

一貫性 (適切な命名の2つ目の性質)

  • 一貫した名前づけは認知負荷を軽減する
  • 命名が一貫していると、コードを読む人が名前から知識を再利用して推測できる

一貫性の3つの要件

  1. 特定の目的のために常に common name を使用する
  2. 特定の目的以外に common name を使用しない
  3. その名前を持つすべての変数が同じ動作をできるくらい、目的が十分に狭い

補足

  • 同じ種類のものを指す複数の変数が必要になる場合は、prefixで識別する ( srcFileBlock, dstFileBlock )
  • ループ変数 ( ij など ) は常に同じ順番で使用する
    • → 名前を見たときに推測できる

14.5  Avoid extra words

余計な単語を含めない

  • 名前に含まれるすべての単語は、有用な情報を提供する必要がある
    • 変数の意味を明確にするのに役立たない単語は混乱を招く
  • 例1: ~field~object
    • オブジェクトであることは名前をつけなくても明らか
  • 例2: クラスのインスタンスがクラス名を繰り返す
    • class File のインスタンス変数 fileBlock
    • File クラスの一部であることは明らか
    • (複数の異なるblockがある場合以外) block で良い

ハンガリアン記法

  • 名前に型情報を含むコーディングスタイル
    • ハンガリアン記法など
    • arru8NumberListfilePtr
  • 著者は昔やっていたが、今は推奨しなくなった
    • 最新のIDEでは型情報を簡単に確認できるので

14.6  A different opinion: Go style guide

  • Go 言語の開発者の中には、名前は非常に短く、多くの場合 1 文字だけであるべきだと主張する人もいる
    • Andrew Gerrand は「長い名前はコードの機能をあいまいにする」と 主張している
  • (本で例に挙げた) 長い変数名のコードが短い変数名のコードよりも読みにくいとは思わない
  • 変数名 count については、わずかにこちらの方がわかりやすい
    • n が何を意味しているのかコードを一通り読む必要があった
    • n がシステム全体で一貫してカウントとして使用されている場合は、わかる
  • Go言語の文化では、複数の異なるものに同じ短い名前を使用することが奨励されている
    • ch : 文字またはチャネル
    • d : データ、差分または距離
    • 著者は、このような名前は混乱とエラーを引き起こす可能性があると考えている
  • 読みやすさは書き手ではなく読み手が決める必要がある
    • 変数名が短く読みにくいと苦情が寄せられ始めた場合は、長い名前の使用を検討する必要がある
      • Web検索したらいくつか苦情が見つかった
    • もちろん変数名が長いとコードが読みにくくなるという苦情が寄せられ始めたら、短い名前を使用することを検討する必要がある
  • ループ変数を短くすることについては同意

14.7 Conclusion

  • 適切に選択された名前は、コードをより明確にする
  • 誰かがその変数に初めて遭遇したとき、深く考えずに動作を推測できるかどうか、は一つの指標
  • 適切な名前を選択することは投資
  • ネーミングのスキルを磨くことも投資
    • 始めは時間がかかるが、経験を積めば時間はかからなくなる
プラハの社長プラハの社長

15章全部

書くのが遅れたコメントは大体悪いコメント

今まで会ってきたプログラマーの大半はコメントを書いていなかった。書かない理由を聞くと「コードは今後変わっていく物だから、コメントも追従して変更しなきゃいけなくなる。コードが安定してからコメントを書く」

今日コメントを書かないことを決めたら、明日もきっと書かない。今日より明日、コードは安定に近づくから「安定するまで待とう」を最後まで繰り返すことになる。それに本当にコードが安定したら今度は新機能の開発とかバグフィックスとか、他のタスクがコメントに優先されることになる。

そしてコードが安定した頃に遅れて書くコメントは大体質が低い。なぜなら

  • コメントを書く頃には内容を忘れている。コメントすべき「コードでは表現しきれない事実」などは特に忘れられやすい
  • コードを読みながらコメントを考えるので、コメントがコードと重複しやすくなる
  • 次のタスクに自分の関心が向かっているのでやっつけ仕事になりがち

コメントを先に書く

筆者は実装の前にコメントを書くコメント駆動開発を実施している。

  • まずクラスのinterface commentを書く
  • 重要度の高いpublic methodのinterface commentを書く
  • 構造に無理がないか考えながら何回か見直す
  • メソッドのbodyを埋めつつ、必要に応じてimplementation commentを追加する
  • 途中で新しいメソッドや変数が必要だと気づいたら、まずコメントを追加してから実装する
  • 変更を加えるときはコメントに変更を加えてからコードに手をつける

こうすれば、コメントを書く、みたいなアイテムがバックログに残り続けることなく、コードが完成したときにはコメントも完成している。実装を始めるタイミングでは実装のことは忘れて抽象化だけを考えれば良いので、より良いデザインにつながりやすい。TDDっぽい

rustだとコメントにテストコードを書いて、そのテストコードを実行できる。なので「実装を変えたけどコメントを変え忘れた」みたいなことも防止しやすそう

コメントはデザインツール

コードを書き始める前にコメントを書くとデザインが改善される。例えばメソッドコメントを記載するには、そのメソッドにとって最も重要な関心ごとは何か特定する必要がある。もしコメントが付けづらければ、それはメソッドや変数の責務が明確になっていないことを意味する。見直しのチャンス。

重要点の抽出はデザインプロセスの序盤で行うべきことで、これをせずコードを書き始めるのはコードをぶった切っているだけ。

"hacking"という言葉を原典が使用しているのは秀逸だと思った。hackはナタとかで木材をぶった斬るような意味も持つので、ハッキングとうまくかけている

抽象化がうまく出来ていることを判断する指標としてコメントは役立つ。もしクラスのインターフェースコメントが網羅的かつ簡潔にそのクラスを表現できていたら良い抽象化が出来ている証拠。

逆にifコメントが実装と寸分違わなければモジュールが浅すぎる兆候かもしれない。

早めのコメントは楽しいコメント

著者にとってプログラミングで一番楽しいのはデザインの工程。抽象化と構造化を検討しているとき。コメントの大半がこの時に書かれるので、正しい抽象化が出来ているか判断する指標でもある良いコメント(簡潔で網羅的なコメント)を書けた瞬間は、著者にとって誇りを持てる瞬間の一つ。

もしあなたが動くコードを書くことではなくよくデザインされたコードを書くことに喜びを覚える人種であれば、早い段階でコメントを書くのは楽しい仕事になるはず

本当にコメントを書くのはコストがかかることなのか

プログラマの仕事のうち、実際にコードやコメントを打ち込む時間が全体の10%だとして、コードとコメントを同じだけの時間を費やしたとしても全体の5%に過ぎない。この5%を遅らせたところで、大したコスト削減にはならない。

むしろコードの打ち込みと同時に抽象化やデザインについて考えていると手戻りに膨大な工数がかかるので、事前にコメントで抽象化やデザインを終わらせておいた方が、トータルで考えた時にコスト削減になっている可能性がある

まとめ

(まだ試したことないならコメントから先に)書いてみな。飛ぶぞ。

コメントから先に書く習慣を長く続けてみてから、コメントの質、デザインの質、そして自分の開発者としての楽しさがどう変化するか観測して、著者と同じような経験を得られるか否か、その理由も添えて筆者に教えてほしい

Kyosuke AwataKyosuke Awata

16 既存コードの編集

  • 既存コードを編集していく中で、複雑性が忍び込むのを防ぐ方法について説明する

16.1 戦略的であり続ける

  • 戦術的であるとは?
    • 優れたシステム設計を生み出すことを目標にする
  • 戦術的であるとは?
    • 複雑さが増しても何かを素早く動作させることが目標にする
  • なぜ戦術的になりがちなのか?
    • 大きな変更は新たなバグを引き起こすリスクが高くなることを懸念している
  • 戦術的になると、最終的にどうなるのか?
    • いくつかの特殊なケースや依存関係、その他の複雑な形態が導入される
    • 設計は少しずつ悪化し、複雑さが増えていく
  • どうなっていれば戦略的と言えるのか?
    • 各変更を終えたときに、最初からその変更を想定して設計された作りになっている
  • どうすれば戦略的になれるのか?
    • 現在の設計が最適かを常に考え、そうでなければリファクタリングを行う
    • 投資マインドの一例で、これも最終的にはペイできる
    • たとえリファクタリングがすぐに必要でなくても、設計上の問題がないか常に考えておくことが大事
    • 設計を改善しないのは、それはおそらく設計を悪化させている
  • リファクタリングをしないという選択肢を取らざるを得ないケースとは?
    • 締め切りに追われていて、どうしても時間内に終わらせないといけない
    • 他の多くの人やチームに影響を与えてしまう
    • このような場合でも「今の制約の中で良い設計をするには、これがベストか?」と自問することが大事
    • 締め切りの後にリファクタリングの時間を確保するなど、別のアプローチがあるはず

16.2 コメントを維持するためには、コメントをコードの近くに配置する

  • 既存のコードを変更したら、既存のコメントが嘘になることがよくある
  • 少しの規律といくつかのルールがあれば、大きな努力をせずコメントを常に最新の状態に維持できる
  • これらのルールをこのセクションと次のセクションで説明する
  • このセクションで言っていること:コメントをコードの近くに配置する
    • コードを変更したらコメントが目に入る
    • インターフェースコメント
      • ヘッダーファイルとコードファイルが分かれているような言語(C,C++)でも、極力コードの近くに書くべき
      • DoxygenやJavaDocのようなツールでドキュメントを自動生成できたり、IDEがコメントを表示してくれるような場合は、それに則ってコメントを書くべき
    • 実装コメント
      • メソッドの先頭にまとめてはいけない
      • 必要な個所に必要なだけコメントを書く
      • 限定的に、メソッドの最初に全体的な戦略を説明するコメントを記述するのはあり
      • ただし、一般的にコメントは記述するコードから離れれば離れるほど、より抽象的であるべきなので、メソッドの先頭に詳細なコメントを書いてはならない(これによって、コードの変更によってコメントが嘘になる可能性を減らすことができる)
ゲントクゲントク

16.3 情報はコミットログではなくコードコメントに残す

  • コミットメッセージに変更に関する詳細な情報を入れても、それをコードで文書化しなかった場合、それは間違い
    • その情報が必要な開発者は、リポジトリのログをスキャンしようと思うことはまずない
    • ログをスキャンしたとしても、正しいログメッセージを見つけるのは相当めんどうなはず
  • コミットメッセージを書くときは、開発者が将来その情報を使う必要があるかどうか考える
    • そうであれば、その情報はコードに書く
    • コード変更の動機となった微妙な問題について知らない開発者が、デグレさせてしまう可能性がある
      • コミットメッセージは気づかれない可能性が高いが、コメントだと気づきやすい
    • 開発者がもっとも目にする可能性の高い場所にドキュメントを配置する

16.4 コメントのメンテナンス: 重複を避ける

  • コメントを最新に保つための第二のテクニックは、重複を避けること
    • コメントが重複していると、更新が難しくなる
    • もっともわかりやすい1箇所を選び、その場所に文書を配置する
      • ある変数に関するトリッキーな動作があり、その変数が使用される複数の場所に影響を与える場合
        • その挙動は、変数の宣言の横にあるコメントで説明することができる
        • 変数を宣言している場所は、開発者がその変数を使ったコードを理解できなかったときに、自然に確認する場所
  • あるモジュールの設計上の決定を、別のモジュールで再びドキュメント化しないこと
    • メソッドの説明は、メソッド呼び出しの前に書くのではなく、呼び出すメソッドのインターフェイスに説明を書く
    • 優れた開発ツールは、この情報を自動的に提供する
      • VSCodeだったら、editor.action.showHoverをショートカットキーに登録しておくと便利です(マウスホバーはテンポ悪い)
  • ドキュメントが外部にすでに存在している場合、プログラム内ではそのドキュメントへの参照を用意すればよい
      • HTTPプロトコルの実装クラスに、HTTPプロトコルの説明を書く必要はない
        • web上にたくさんの情報がある
        • URL貼ればOK
      • ユーザーマニュアルに記述されている機能の説明はいらない
        • 「Fooコマンドを実装している。詳細はユーザーマニュアルを参照」
    • コードを理解しやすくするためのドキュメントを見つけやすくすることは重要だが、それは、すべてのドキュメントを自分で書かなければならないということではない

16.5 コメントのメンテナンス: 変更をチェックする

  • 変更をコミットする前に、(数分かけて)コミットに対するすべての変更点をスキャンし、各変更点がドキュメントに適切に反映されているか確認すること
  • デバッグコードやTODO項目を残したままコミットしてしまうというミスも防げる

16.6 高レベルなコメントは、メンテが楽

  • コメントはコードよりも高レベルで抽象的であるほうが維持しやすくなる
    • 高レベルで抽象的なコメントはコードの詳細を反映しないので、ちょっとしたコードの変更には影響されない
    • 13章で解説したとおり、詳細で正確なコメントが必要な場合もある
    • 結局、もっとも有用なコメント(単にコードを繰り返しているだけではないコメント)は、メンテナンスも簡単になる
sushidesusushidesu

17 Consistency

  • 一貫性は、システムの複雑さを軽減し、その動作をより明確にするための強力なツール
  • 類似したことは類似した方法で行われ、類似していないことは異なる方法で行う
  • 一貫性は認知力を生み出す
    • 一旦学習すれば、同じアプローチを使用する他の場所をすぐに理解できる
  • 一貫性はミスを減らす
    • 一貫性がない場合、見慣れたパターンから誤った仮定をしてしまう
    • 一貫性がある場合、より少ないミスでより早く作業ができる

17.1  Examples of consistency

一貫性を適用できる例

  • 名前: (14章)
  • コーディングスタイル
    • インデント、中括弧の配置、宣言の順序、名前付け、コメント、危険と見なされる言語機能の制限など、さまざまな問題に対応できる
    • スタイル ガイドラインはコードを読みやすくし、エラーを減らす
  • インターフェース
    • 複数の実装を持つインターフェース
    • 1 つの実装を理解すると、他の実装も理解しやすくなる
  • デザインパターン
    • 既存の設計パターンを使用できる場合、実装はより迅速に進み、機能する可能性が高くなり、より明白になる
    • 詳しくは 19.5章で説明する
  • invariant (不変条件)
    • 常に true (真?) な変数または構造体のプロパティのこと 変数や構造体の常に真実である特徴のこと
      • property はここでは特徴という意味
    • 例: テキスト行を格納するデータ構造の不変条件
      • 各行が改行文字で終了する
    • 考慮しなければならない特殊なケースの数を減らし、コードの動作の推論を容易にする

17.2 Ensuring consistency

  • 一貫性を維持するのは困難
    • 特に、多くの人が長期間にわたってプロジェクトに取り組んでいる場合
    • 知らない場合、慣習に違反したり、矛盾する新しい慣習を作ってしまう

維持するためのヒント

  • ドキュメント
    • 最も重要な全体的な規則をリストしたドキュメントを作成する (コーディング スタイルのガイドラインなど)
    • プロジェクト Wiki の目立つ場所など、開発者が目にする可能性が高い場所に置く
    • 不変条件など、よりローカライズされた規則は、コード内の適切な場所を見つけて書いておく
  • 強制する
    • どれだけ優れたドキュメントがあっても、すべての規則を覚えるのは難しい
    • 違反をチェックするツールを作成し、チェッカーを通過しない限りコードをリポジトリにコミットできないようにするべき
  • コード レビュー
    • 規則を強制し、規則について新しい開発者を教育するための別の機会を提供する
  • When in Rome, do as the Romans do.
    • すべての開発者が「ローマにいるときは、ローマ人が行うようにする」に従うべき
    • 新しいファイルで作業するときは、既存のコードがどのように構成されているかを確認し、従う
    • 設計上の決定を下すときは、既存の例を見つけて適用する
  • 既存の規則を変更しない
    • 既存の慣習を「改善」したいという衝動に抵抗する
    • 「より良いアイデア」だけでは、矛盾を導入する理由にはならない
    • 新しいアプローチを正当化する重要な新しい情報があるか?時間をかけて古い使用法をすべて更新するだけの価値があるか?
    • 確立された慣習を再検討することは、開発者の時間を有効に活用することにはほとんどならない
プラハの社長プラハの社長

18章全部

コードは明白であるべき

明白の定義は「そのコードを読んだ人が時間をかけず正しく意味を推測できること」なので、書き手ではなく読み手によって判断されるものである

どういう時にコードは明白になるのか

  • 良い名前をつけること(既出)
  • 一貫性。似たようなことを似たような手段で達成している場合、読み手は他のところで得た知識を活用することでコードをそこまで深く読まなくても安全な結論を導き出せる(既出)
  • 余白を賢明に使う。異なる意味を持つ情報は余白で区別することで読みやすくなる。
    • インターフェースコメントはパラメータ1つごとに余白を挿入して読みやすくする
    • 関数の処理はブロック単位に余白で分割する。余白の次に来るコードはコメントにしておくと、より次のブロックの抽象的な理解を持ちながら読み進められるので良い
function hoge() {
// 次の処理でaをする
...
...

// 次の処理でbをする
...
...

// 次の処理でcをする
...
...
}
  • ときには明白なコードを書くのが不可能なこともある。そんな時はコメントを使って欠けている情報を補足する必要がある。

どういう時にコードは明白でなくなるのか

イベント駆動

イベント駆動のプログラミングにおいてはアプリケーションはネットワークパケットの到達やマウスのクリックなど外部で発生したことに応じて処理を行う。 ひとつのモジュールはイベントの受け入れ、もう一つのモジュールはそのモジュールによって呼び出される関数やメソッドが定義されている。

イベント駆動プログラミングは制御フローが追いづらくなる。イベントハンドラーは直接呼び出されるのではなく間接的に呼び出される。そのためイベントが発生する場所だけを呼んでも、そのイベントが次にどのハンドラを呼び出すのかはわからない。この曖昧さを補填するためには、呼び出されるハンドラにインターフェースコメントで「このハンドラはどういうイベントに反応するのか」と書いておく必要がある

イベント駆動であるにも関わらずakkaがそこまで読みづらいと感じない理由を考えてみた。 まずイベントには型があるので、その特定の型のイベントに購読している処理はideの補完機能ですぐにわかる。逆にjQuery みたいなコードは相当読みづらい。「イベントの定義」「イベントの購読」「イベントハンドラ」の3つの要素の関係性がコードでは表現できないからだと感じた。

ジェネリックなコンテナ

関数から複数の戻り値を返したい時、pairのような組み込みクラスを使うことがある。これはコードを曖昧にしてしまう。なぜなら呼び出し側ではこの結果に対してgetKeyやgetValueを実行して値を取り出すことになるが、それぞれのメソッドから帰ってくる値に関しては何のヒントもないから。

ジェネリックなコンテナを使うのではなく特定の用途に特化した新しいクラスや構造体を作成する方が良い。 意味のある名前を要素に付与することで(クラスや構造体の)宣言に補足情報を詰め込むことができる

これはプログラミングに関する一つの普遍的なルールを表している。ソフトウェアは読みやすさのためにデザインされるべきであって、書きやすさのためではない。ジェネリックなコンテナは書き手にとっては手軽だが、読み手にとっては混乱の元になる。 書き手に少し時間を費やすとしても読み手の楽を優先すべき

スコープの短い処理の中でタプルを使うこともこの人は避けるのだろうか?

getkeyとかgetvalueした結果帰ってくる型がドメインプリミティブなら、新しいクラスを定義しなくてもなんとなく推察できそうな気がした

Pairの代わりに「TermActivity」みたいなクラスを定義するのは浅いクラスを量産することに繋がらないのだろうか?それともこの人が使ってる言語にそういう機能がなかっただけで、この人がtsを書いたらクラスではなくtype定義で済ませるのかな

異なる型を代入する

親クラスの型を定義した変数にクラスの変数を代入すると、挙動が異なることがあるので、ユーザーの期待を裏切ってしまうことがある。厳密な型を代入するのなら最初から厳密な型を定義しておこう

Repository Interfaceを引数に定義しておいて実際使うときはrepositoryの実態を代入するのもこの人は避けるのだろうか?抽象に依存してる変数に詳細を代入するのと、詳細に依存している変数に別の詳細を代入するのは意味が違うから、前者は流石に許容されるかな?

リスコフの置換原則に違反しているクラスだと尚更注意が必要そうだとは思った。そして全然関係ない疑問だけどlspは継承より合成においても守るべき原則なんだろうか?継承に限定したお話と考えていた

読み手の予想を裏切る

その問題を解くために用いられる一般的な解法からそれてしまうとユーザーの期待を裏切ってしまう。たとえば Java のメインメソッドは必ずリターンすることが期待されているが、著者の例では別のスレッドで処理が続けられるため、一般的な Java のメインメソッドの挙動とは大きく異なる。他のスレッドでアプリケーションが継続することをコメントで示すべきである。

結論

情報の観点から曖昧さについて考えることもできる。コードが曖昧であるということは読み手にとって重要な情報が提供されていないということを示している。javaの main メソッドの例ではraftclientが新しいスレッドを作ることを読み手は知らないかもしれない。ペアの例では読み手はgetkeyが今のtermの数を返すことを知らないかもしれない。

コードを明白にするためには読み手が常に必要な情報を持っていることを保証しなければいけない。これには三つの良い方法がある

  • 特殊なケースを限りなく排除するために適切な抽象化を行う
  • 読み手が他のコンテキストで持っている情報(例えば特定の問題を解くために周知となっている解法)を用いることでユーザーが新しい情報を学ばなくてもすむようにする
  • 重要な情報を良い命名やコメントによってコートで表現する
プラハの社長プラハの社長

arraylistの代わりにlistを使ってアプリケーションが壊れるのが問題だとしたら、それはjavaの実装がlspに違反しているのが問題なのであって、より抽象的な型に具体的な型を代入する問題ではないのではないか?という感想

windows formもイベント駆動だった。購読がわかりづらかった。そこが読みづらさの違いを生んだのかなと思った

Kyosuke AwataKyosuke Awata

ソフトウェアのトレンドと複雑性の関係性

この数十年の間に一般的となったいくつかのトレンドとパターンが、ソフトウェアの複雑性とどのように関連しているかを評価する

オブジェクト指向プログラミングと継承

  • 過去30年~40年のソフトウェア開発における最も重要な新しい考え方の1つ
  • より優れたソフトウェア設計に役立つ
    • カプセル化を行って情報を隠蔽する など
  • 重要な要素の1つに継承があり、それも2つのパターンに分類される
    • インターフェース継承
      • スーパークラス(インターフェース)はシグネチャの定義のみ行い、サブクラスで実装を行う
      • これにより同じインターフェースを複数の目的で再利用することができるため、複雑性の低減につながる
        • 1度覚えたインターフェースの使い方を他のサブクラスでも同様に使える
      • あるインターフェースの実装が多いほど、深いインターフェースを定義できていると言える
      • 深いインターフェースを定義するためには、全ての基本的な実装の本質的な特徴を捉え、実装間で異なる細部を排除する必要がある
    • 実装継承
      • スーパークラスが定義したデフォルトの実装をサブクラスは継承するかオーバーライドするかを選択する
      • メリット
        • サブクラスで重複した処理を記述する必要がなくなり、サブクラス間の依存を排除できる
      • デメリット
        • スーパークラスのインスタンス変数にサブクラスからもアクセスされることがあり、スーパークラスとサブクラス間で情報漏洩が起きる
        • スーパークラスに変更を加える場合、全てのサブクラスを調べて問題がないことを確認しなければならない
      • 実装継承を多用したクラス階層は複雑になる傾向があり、注意が必要
      • 継承の前に移譲でそれが実現できないかを検討するべき
      • 実装継承が必要な場合でも、スーパークラスとサブクラスのインスタンスはそれぞれ別で管理するようにし、情報漏洩や依存性を排除することが必要
    • オブジェクト指向の仕組みは良い設計に役立つが、良い設計を保証するわけではないので、複雑性を排除するには良いインターフェースを提供し、カプセル化を行う必要がある
    • 例えばスーパークラスで定義してある実装が final(変更不可)で、インスタンス変数も全てprivateだった場合、移譲と比較してどんなデメリットがあるのだろう?

アジャイル開発

  • ソフトウェア設計とは対照的に、ソフトウェアの開発プロセスに関する手法だが、漸進的かつ反復的であるという考え方はどちらにも共通している
  • ただし、アジャイル開発は戦術的なプログラミングになる可能性も含んでいる
    • 開発者を抽象化ではなく機能の実装に集中させる傾向がある
    • 可能な限り早く実用的なソフトウェアを作ることが優先され、設計を後回しにしがち
    • まずは最小限の特殊な機能を!その後に汎用化を!という考え方の人もいる
    • その結果、戦術的なプログラミングを続けることで、急速に複雑さが増す可能性が高い
  • インクリメンタルな開発は良いアイデアだが、増えていくものは機能ではな抽象化であるべき?(ちょっと何が言いたいのか分からんかった)
  • 抽象化を先延ばしにするのは構わないが、必要になったらリファクタリングの時間を必ず取った方が良い。
Kyosuke AwataKyosuke Awata

インクリメンタルな開発は良いアイデアだが、増えていくものは機能ではな抽象化であるべき?(ちょっと何が言いたいのか分からんかった)

リファクタリングまでをやってから次の実装に進めという指針
機能ができた=タスク終わりではなく、抽象化されたものを提供できた=タスク終わりとする?

ゲントクゲントク

19.3 Unit tests

  • テストは、以前は開発者ではなく別のQAチームが書くものだった
  • アジャイル開発によって、プログラマがテストを書く習慣は広く浸透した
    • テストは開発と一体になるべきであり、プログラマは自分のコードのためにテストを書くべきである

ユニットテストとシステムテスト

ユニットテスト

  • 最も頻繁に書かれる
  • それぞれのテストは、単一のメソッドにおけるコードの小さなセクションを検証する
  • 本番環境をセットアップすることなく、単独で実行することができる
    • t_wadaさんが似たようなこと言ってて、「ユニットテストはコードのフィードバックサイクルを短くするためのものでもある」的な内容だった
    • 静的解析であればユニットテストよりも早くフィードバックサイクルを回せるからよりよい、的な話にすごく納得した
  • テストカバレッジツールと一緒に実行されることが多い
  • 開発者は、テストカバレッジを維持するためにユニットテストを更新する責任を負う

システムテスト

  • アプリケーションの異なる部分が全て適切に連携して動作することを確認する
  • 実稼働環境に近い状態でアプリケーション全体を実行することを含む
  • システムテストは、QAなどの別のチームによって書かれることが多い

テストの分け方の話(本に書いてある話ではない)

よいユニットテスト群はよい設計を生む

  • ユニットテストはリファクタリングを容易にする
  • テストがないのにシステムに大きな構造的変更を加えることは危険である
    • コードがデプロイされるまでバグが発見されない可能性が高い
    • 発見と修正に多大なコストがかかる
    • リファクタリングが避けられるようになったり、開発者は変更を最小限にすることに一生懸命になってしまい、複雑さが蓄積されていく
    • テストがあれば自信を持って変更を加えられ、結果としてよりよい設計を行うことができる
  • ユニットテストはシステムテストよりもコードカバレッジが高くなり、バグを発見する可能性が高い
    • 例: Tclというスクリプト言語のインタプリタをバイトコードのコンパイラに置き換えるリファクタリングをやった時の話
      • パフォーマンス向上のため
      • Tclエンジンのコア部分の殆どに影響を与える大きな変更
      • ユニットテストがたくさんあったので、非常に効果的にバグを発見できた
      • バイトコードのコンパイラのα版をリリースし、リリース後に見つかったバグは1つだけだった

19.4 Test-driven development

  • TDDは、コードを書く前に単体テストを書くという、ソフトウェア開発のアプローチ
  • まず単体テストを書いて、テストが落ちて、コードを書いて、テストが通ったら完成
    • これはテストファーストの説明であって、テスト駆動開発の説明ではない(後述)

テスト駆動開発の問題点

  • 著者は、ユニットテストは大好きだがテスト駆動開発は好きじゃない
  • テスト駆動開発の問題点は、最適な設計を見つけることよりも特定の機能を動作させることに開発者の注意を集中させていること
    • まんま戦術的なプログラミング
    • テストをパスするために機能の実装をハックしたくなるもの
    • 設計を行う明確なタイミングがない
  • TDDとテストファーストを混同しているし、TDDの目的をわかっていない
    • TDDにはレッド・グリーン・リファクタリングというリズムがある
    • 毎回、現時点の実装でのリファクタリングを行うのがTDD
    • TDDはよい設計のためというよりも、よい設計を思いついたときにすぐに実現できるように備えるためのアプローチ
    • テストファーストはリファクタリングを行わずにいきなり設計→実装するので、間違った抽象化をしやすい、それはそう
    • TDDはいったん(雑なものであっても)実装を挟むので、設計上の問題がより明確にわかるようになり、適切な抽象化をやりやすい
  • 19.2で述べたように、機能ではなく抽象の単位で開発をすすめるべきである
  • 抽象化の必要性を発見したら、時間をかけて断片的に作成するのではなく、一度にすべてを設計するべき
  • そうすることで、断片がうまく組み合わされた、きれいな設計ができる可能性が高くなる
    • 設計はインクリメンタルじゃなくてもっと大きな間隔でやるべき、っていう意味?

テスト駆動開発が有効な場面

  • テストを最初に書くことが理にかなっているのは、バグ修正のとき
  • バグを再現するテストを書いて、バグを修正して、テストがパスすることを確認する
  • テストを書く前にバグを修正してしまうと、そのテストがバグを再現しているのかどうかがわからない

19.5 Design patterns

  • デザインパターンとは、イテレータやオブザーバなどの、特定の問題を解決するためによく使われる手法のこと
  • 有名な本によって、現在ではオブジェクト指向ソフトウェア開発に置いて広く使われている

デザインパターンのいいところ

  • 新しい仕組みをゼロから設計するのではなく、よく知られたパターンを適用すればよいという、設計の代替案を示している
  • デザインパターンは一般的な問題を解決するために生まれ、一般的にきれいな解決策を提供するという事実は、合意されている
  • デザインパターンが特定の状況でうまく機能する場合、より良い別のアプローチを考え出すのはおそらく難しい
  • 開発者間の共通の語彙としてある、というのもデザインパターンの大事な役割だと思った

デザインパターンを使うときに気をつけること

  • デザインパターンの最大のリスクは、過剰な適用
  • すべての問題が、既存のデザインパターンできれいに解決できるわけではないし、自動的にソフトウェアが改善されるわけではない
  • デザインパターンはよいものだが、デザインパターンをたくさん使えば使うほどいいということではない
  • デザインパターンを使ったほうがいい場合と使わないほうがいい場合、どっちがいいのかを判断するスキルが無いと、デザインパターンを誤用してしまう可能性があるということは、頭に入れておかないといけない

19.6 Getters and setters

  • ゲッター/セッターメソッドはJavaでよく使われるデザインパターン

ゲッター/セッターメソッドのいいところ

  • インスタンス変数の変更時に他の変数もあわせて更新できる
  • インスタンス変数の変更時にリスナーに変更を通知できる
  • インスタンス変数の変更に制約を設けることができる

ゲッター/セッターメソッドを使うときに気をつけること

  • そもそもインスタンス変数は公開しなくてすむなら公開しないほうがいい
    • 情報隠蔽の考え方に反している
    • クラスのインターフェイスの複雑さを増幅させる
  • ゲッター/セッターは浅いメソッドになりやすい
  • ゲッター/セッターはできるだけ避ける
  • デザインパターンは使えば使うだけいいと思ってる開発者が、ゲッター/セッターを乱用して問題を起こしている

19.7 Conclusion

  • ソフトウェア開発の新しいパラダイムの提案に出会ったら、複雑性の観点からその提案に挑戦してみる
    • その提案は、大規模ソフトウェアシステムの複雑性を最小化するのに本当に役に立つのか?
    • 多くの提案は表面的には聞こえがいいが、もっと深く見てみると、複雑さを改善するのではなく、悪化させるものがあるということがわかることがある
sushidesusushidesu

20.1 ~ 20.3

Chapter 20 Designing for Performance

  • クリーンな設計を犠牲にすることなく高いパフォーマンスを実現するためにはどうするか
  • シンプルさはシステムの設計を改善するだけでなく、通常はシステムを高速化する

20.1  How to think about performance

通常の開発プロセス中にパフォーマンスについてどの程度心配する必要があるか

  • すべてのところで最適化を行おうとするとしようと、開発が遅くなり多くの不要な複雑さが生じる
  • しかしパフォーマンスの問題を完全に無視すると、コード全体に多数の重大な非効率性が広がる
  • 最良のアプローチは、「自然に効率的」でありながらクリーンでシンプルな代替設計を選択すること
  • 重要なのは、どの操作が根本的にコストがかかるかを認識すること

比較的コストのかかる操作の例

  • ネットワーク通信:
    • データセンター内であっても、往復のメッセージ交換に 10~50 マイクロ秒かかることがある (= 数万回の命令に相当)
    • 広域往復には 10~100 ミリ秒かかることがある
  • セカンダリ ストレージへの I/O
    • ディスク I/O 操作には通常 5~10 ミリ秒かかる (= 数百万回の命令に相当)
  • 動的メモリ割り当て (Cの malloc、C++ や Java の new )
    • 割り当て、解放、ガベージコレクションに大きなオーバーヘッド
  • キャッシュミス
    • DRAMからデータを取得してキャッシュするためには数百回の命令に相当する時間がかかる
    • 多くのプログラムでは、全体的なパフォーマンスは計算コストと同じくらいキャッシュミスによって決まる

どうするか

  • マイクロベンチマークを実行することでどの操作のコストが高いかを知る
    • RAMCloudプロジェクトではマイクロベンチマークを簡単に追加できるフレームワークを作った
  • パフォーマンスについての一般的な感覚を身につける
    • 多くの場合、より効率的なアプローチは、より遅いアプローチと同じくらい単純です
    • 例: ハッシュ テーブル or 順序付きマップ
      • ハッシュ テーブルの方が5~10倍速くなる
    • 例: 構造体の配列 (CやC++など)
      • 配列が構造体へのポインターを保持場合、最初に配列にスペースを割り当て、次に個々の構造体にスペースを割り当てる必要がある
      • 構造体を配列自体に格納する方がはるかに効率的 (1つの大きなブロックのみを割り当てるだけで済むので)

パフォーマンスを改善する唯一の方法が複雑さを追加することである場合どうするか

  • 追加すべき複雑さが僅かで、インターフェイスに影響を与えない場合、価値があるかも (ただし複雑さは増加するので注意)
  • 高速な設計によって実装やインターフェイスがより複雑になる場合は、単純なアプローチから始めて後でパフォーマンスが問題になる場合に最適化するのが良い
  • ただし、パフォーマンスが重要になるという明確な証拠がある場合は、高速なアプローチをすぐに実装するのも可
  • RamCloudプロジェクトでの例
    • 測定の結果から、複雑さが増すのがわかっていたがネットワーク用の特別なハードウェアを使用することにした
    • RAMCloud システムの残りのほとんどは、シンプルに設計することができた
    • 1 つの大きな問題を正しく解決することで、他の多くのことが容易になった
  • 一般に、単純なコードは複雑なコードよりも高速に実行される傾向がある
  • 深いクラスは、各メソッド呼び出しでより多くの作業を実行できるため、浅いクラスよりも効率的
    • 浅いクラスでは層の交差が多くなり、層の交差ごとにオーバーヘッドが追加される

20.2  Measure before (and after) modifying

  • 何が遅いのかという直感に基づいて、急いでパフォーマンスの微調整を開始したくなるものだが、これをしてはいけない!
    • パフォーマンスに関するプログラマーの直感は信頼できない (経験豊富な開発者でも)
    • 直感に基づいて変更を開始すると、実際にはパフォーマンスが向上しないことに時間を浪費し、その過程でシステムをより複雑にする可能性がある
  • 変更を加える前に、システムの既存の動作を測定すべき
    • 測定により、パフォーマンス チューニングが最も大きな影響を与える場所が特定さる
    • ベースラインがわかるので、パフォーマンスが実際に改善されたことを確認できる
    • 推測するな、計測せよ

20.3  Design around the critical path

  • パフォーマンスを向上させる最善の方法は、根本的な変更(キャッシュ導入など)を行うか、別のアルゴリズムアプローチ (バランスツリーorリストなど) を使用すること
  • 根本的な修正がない場合、既存のコードを再設計して実行速度を向上させる方法をとる必要がある (最後の手段)

既存のコードを再設計して実行速度を向上させるやり方

  • 重要なのは、クリティカルパスを中心にコードを設計すること
    • クリティカルパス: 最も一般的なユースケース (?)
    • クリティカルパスを実装するための最小のコードをもとに設計を考える
  • クリティカルパスのみを実装した理想的なコードは、最も単純で高速
    • これを維持しながら再設計する
    • クリーンでシンプルでありながら、理想に非常に近い設計を見つけることは(ほとんどの場合)可能
  • クリティカルパスから特殊なケースを取り除く
    • 追加の条件やメソッド呼び出しが増えるたびに少しずつコードは遅くなっていく
    • なので、チェックするケースの数を最小限に抑えるべき
    • (理想) コードの初めの一つのifで特殊なケースを検出して分岐
      • → パフォーマンスが重要ではない特殊なケースを考慮せずにクリティカルパスを実装できる
プラハの社長プラハの社長

シンプルなデザインがパフォーマンスも改善した例

背景

RAMCloudはBufferクラスを実装していた。役割は可変長配列を表現すること。可変長配列は一見すると連続するバイトの配列に見えるが、実態は非連続なメモリのchunk(塊)に分割されている

それぞれのchunkはinternal/externalに分類される。

internalなチャンク

  • バッファが持つbuilt-in allocation(組み込みのメモリ割り当て)に直接保存される
  • リクエスト/レスポンスヘッダなど(メモリコピーのコストが無視できるような)小さめの情報に向いている
  • built-in allocationが不足していたらextra allocationを作成する

externalなチャンク

  • 呼び出し側に保持されていて、その参照のみをバッファは保存する
  • 大きめの情報に適している

このように分類することで、例えばRAMCloudに保存されている何かを取得する際、ヘッダはinternal、実際の大きなコンテンツはexternalチャンクとしてバッファが保持しておくことで、大きなコンテンツをメモリコピーせずに済んだ

やりたいこと

根本的な問題は解決できたものの、パフォーマンス上の問題が生じてきたので改善する必要が生じた。

まず、バッファクラスのクリティカルパスを特定した。もっとも頻繁に実施されるのは「小さなデータのためにinternalチャンクを作成すること」(リクエストメッセージのヘッダを追加するときなどに使用)だったので、これをクリティカルパスに設定した。

最もシンプルなケースでは、直前のバッファに最後に確保されたinternalチャンクを拡張することで、新しいinternalチャンクのためのメモリ領域を割り当てられる。ただし前提条件として「最後のチャンクがinternalであること」「新しいデータを格納するために十分な領域がallocationに確保できること」が満たされる必要がある。理想的なコードには、一度のチェックでこれらの前提条件が満たされていることを確認して、既存チャンクのサイズを調節することが求められる

元のコードの問題

元のコードにはいくつかの問題があった。

1つ目の問題は、幾つもの特殊ケースが個別にチェックされていること。

  • 今のバッファがallocationを持っているか
  • 今のallocationが必要なスペースを確保できるか二度チェック
  • メモリの割り当てが成功したか

そもそも最後のチャンクを考慮することなく新しいスペースを追加してから、最後のチャンクと隣接する場合にマージするような作りにしていたため、チェックが多い作りになっていた。クリティカルパスで6つのテスト(条件分岐のこと)が発生していた。

2つ目の問題は浅いレイヤーが多すぎること。

呼び出しのレイヤーを跨いでいるはずなのにシグネチャが変わらないし、抽象化の粒度も変わらないため、パススルーメソッドに近い状態になっている。

解決策

最も頻繁に実行されるクリティカルパスを特定して、それを達成するために必要最小限のコードで済むようにリファクタリングした。

新しい例は以下の点で優れている:

  • クリティカルパス全体が1つのメソッドで完結している
  • 1つのテスト(条件分岐)で特殊ケースを全て除外している(クリティカルパスだけに専念している)

このリファクタリングにおいて重要な役割を果たしたのは「extraAppendBytes」変数。最後のチャンクの直後に残っているスペースを管理する変数。最後のチャンクがexternalだったり、バッファにチャンクがなかったり、容量がない場合は0になる。この変数を用いた条件分岐を導入することで、common case(クリティカルパス)を必要最小限のコードで実現できた

この改善によりバッファクラスのスピードは2倍程度に向上した

メモ

totalLengthは排除しようと思えばできる。必要になったタイミングでバッファから再計算すれば済むため。ただしこの処理は大きいバッファには負荷をかけるし、バッファのtotalLengthが必要になるケースは非常に多いので、allocのたびに少しだけオーバーヘッドが増えるけどtotalLengthを更新する作りを採用した

結論

本章で最も伝えたかったのはパフォーマンスと設計には互換性があること。バッファクラスを書き直すことでコードサイズは20%削減しつつ速度は2倍になった。複雑なコードは無駄、あるいは重複した作業を行うため遅くなりがち。綺麗でシンプルなコードを書くことを考えれば、そこまでパフォーマンスを心配する必要も生じない。

パフォーマンスを本当に心配しなければいけないレアケースにおいても、大切なのはクリティカルパスを見つけて、最もシンプルにそれを解決すること。

思ったこと

  • 元の例には条件分岐が4つあった。「最後のチャンクが拡大可能か」「今のバッファにallocationが存在するか」「allocationに成功したか」「今のallocationに新たなスペースを割り当てられるか」
  • 新しいコードには「extraAppendBytesが十分か」の1つしかない
  • そもそもこのコードは何がクリティカルパスなんだっけ?どうすれば最もシンプルにクリティカルパスに入れるんだっけ?を考えた末にこの変数にたどり着いて、よりシンプルに特殊ケースを切り分けられた
  • このアプローチは良いデザインにたどり着く普遍的な行動に見えるけど、パフォーマンスに問題を抱えている時に限定した方が良いのだろうか?

思ったこと

  • allocateAppendの中でassertする意味あるのかな?
  • 浅いメソッドをたくさん作ると、そのメソッドの中で処理が整合性を保って完結したことを保証したくなるからチェックが増える。けど呼び出し側にもより詳細なチェックが求められる。だから全体でチェックが増えやすいよね、という問題点もありそうなのかな
Kyosuke AwataKyosuke Awata

21 重要なことを決める

  • ソフトウェア設計の重要な要素の1つは、重要なものとそうでないものを分けること
    • 重要なものを中心に構成し、あまり重要でないものは他の部分に与える影響が少なくなるようにする
    • モジュール設計において、インターフェースには利用者にとって重要なものを反映し、重要ではないものは内部に隠蔽する
    • 変数名は、値に関する最も重要な情報が伝わるような言葉を選ぶ
    • パフォーマンスが重要なモジュールがあるのなら、パフォーマンスが良くなるような構成を第一に選ぶ

21.1 何が重要かを決めるには

  • 何が重要かを決めるのは設計者自身
  • 以下のようなものを探すことで重要なものが見つかるかもしれない?
    • ある問題を解決することで、他の多くの問題も解決できる
      • テキストクラスの例では、より汎用的なインターフェースを定義したことで、複数の問題を解決できるようになった
    • ある情報を知ることで、他の多くのことを容易に理解できる
      • 変数や構造体が不変条件を満たしていれば、どのように振る舞うかを予測しやすい(?)
  • 重要な情報を見つけるために
    • 変数名を決めるときは、変数に関する言葉をリストアップし、その中から最も情報が多い言葉を選ぶ
    • これが一番重要だと思う、という仮説を立てて考える

21.2 重要なものを最小限にする

  • 可能な限り問題を少なくすることでシステムはシンプルにできる
    • オブジェクトを構築するためのパラメータの数を最小限にする
      • 最も一般的な使用方法を反映したデフォルト値を提供する
  • 重要なものについては、重要なものとして扱わなければいけない場所を最小限にする
    • モジュール内に隠蔽されていれば、モジュール外から見ると重要な情報ではなくなる
    • 例外をモジュール内で完全に処理できるのであれば、モジュール外から見ると重要な情報ではなくなる
    • ある設定パラメータがシステムの動作に基づいて自動的に計算されるなら、それは重要な情報ではなく、手動で設定する必要すらなくなる

21.3 重要なことを強調する方法

  • インターフェースのコメントや名前、よく使うメソッドのパラメータなどに記載する
  • そのアイデアを何度も使う(?)
  • 中心性を強調する
    • 最も重要なものはシステムの中心にあるべきで、それに依存して周囲の構造を決める
      • OSのドライバとかが例に挙げられている
  • 重要でないものを極力隠蔽し、システム全体の構造に影響を与えないようにする

21.4 間違い

  • あまりにも多くのことを重要視しすぎる
    • 重要ではないものが設計を複雑化させる
      • 不必要な引数を持つメソッド
      • JavaのI/O(バッファ付きとバッファ無しを区別することは重要ではなかった)
    • 浅いクラスはこれによって生まれることが多い
  • 何が重要であるかを認識していない
    • 重要な情報が隠蔽されてしまっている
    • 重要な機能が使えない状態になっている
    • 未知の未知を生み出す原因となる

21.5 より広い視野で考える

  • 重要なことに集中するのは、他の領域でも使える考え方
    • 文章を読みやすくするには、冒頭でいくつかの重要な概念を特定し、それを中心に残りの部分を構成する
  • 自分にとって重要なこをといくつか決め、重要でないことややりがいのないことに時間を浪費しない
  • センスがいい=重要なものとそうでないものを見分ける能力があること
ゲントクゲントク

Conclusion

この本の唯一のテーマは「複雑さ」

複雑さに対処することはソフトウェア設計における最も重要な課題。複雑さはシステムの構築、維持を難しくし、動きを遅くすることがある。

  • 複雑さをもたらす根本的な原因を説明した
    • 依存関係
    • 不明瞭さ
    • など
  • レッドフラッグについて説明した
    • 情報漏えい
    • 不要なエラー条件
    • 一般的すぎる命名
    • など
  • よりシンプルなソフトウェアシステムを作るための一般的なアイデアを紹介した
    • 深くて汎用的なクラスを目指すこと
    • エラーが存在しないように定義すること
    • インターフェイスのドキュメントと実装のドキュメントを分離すること
    • など
  • シンプルな設計を生み出すために必要な投資マインドセットについて述べた

「複雑さ」について考えるということ

  • この本で提案したアイデアの欠点は、プロジェクトの初期段階で余分な作業を発生させてしまうこと
    • 設計の問題について考えることに慣れていなければ、設計のテクニックを学んでいる間は、ペースダウンしてしまう
    • 現在のコードをできるだけ早く動作させることだけが重要なのであれば、これらの作業はただの邪魔な雑用に思えてくるはず
  • 一方で、優れた設計を手に入れることが目標であるならば、この本のアイデアはプログラミングをより楽しくしてくれるはず
    • 設計は魅力的なパズル
    • ある問題を可能な限りシンプルな構造で解決するには?
    • さまざまなアプローチを模索するのは楽しい
    • シンプルかつパワフルな解決策を発見したときの気分は最高
    • クリーンでシンプル、かつ明快なデザインは、美しい
  • さらに、優れた設計に投資した時間は、すぐに回収できる
    • プロジェクトの最初に注意深く定義したモジュールは、あとで何度も再利用する際に時間を節約できる
    • 明確なドキュメントがあれば、新しい機能を追加するために半年前に書いたコードに戻るとき、時間を節約できる
    • 設計のスキルを磨くために費やした時間は、次の設計のときに時間を節約できる
    • スキルと経験が増えるにつれて、よい設計をより早く作れるようになる

優れた設計者であること

  • 優れた設計者であることの報酬、それは設計段階に費やす時間の割合が増えることで、それは楽しいこと
  • 下手な設計者は、複雑で脆いコードのバグを追いかけることにほとんどの時間を費やしている
  • 設計のスキルを上げれば、より質の高いソフトウェアをより早く作れるだけでなく、ソフトウェア開発プロセスがより楽しくなるはず
このスクラップは2022/09/13にクローズされました