概要
- 『レガシーコードからの脱却』(オライリー社, 2019)のメモです。
本書の対象者
開発者,運用者,PM,PdM,ソフトウェアの利害関係者(非開発者)
本書の目的
- ソフトウェア開発には「レガシーコード」を作らないためのいろんなプラクティスがある。それらを状況に応じて 適切に適用できる人を増やしたいと言うのが著者の思いであり,本書の目的である。
- レガシーコードの定義 👉「修正や拡張,作業が難しいコード」のこと。
- 本書は開発者・非開発者のコミュニケーションギャップをなくし,ソフトウェア開発に関する共通認識を 築けるようにデザインされている。
- プラクティスは,XP,スクラム,リーンなどの開発手法から導かれている。
全体の構成
以下の2つの部から構成される:
第1部 レガシーコード危機
レガシーコードがなぜやばいのか,それによって我々がどれほどの損失を被っているのかという事実をつきつける。
第2部 ソフトウェアの寿命を延ばし価値を高める9つのプラクティス
「第1部」で提示した問題への解決策(9つのプラクティス)を提示する。一応プラクティスについて説明はするが, 必ず読者自身でなぜそれが有効なのか説明できるようにすること。そうでなければ,実際に適用してもうまくいかない。
第1部 レガシーコード危機
1章 何かが間違っている
筆者の武勇伝
著者がかつてコンサルタントとして担当した組織
- コードモンキーが存在(アーキテクチャや設計を考えず,ただ仕様通りにコードを書く人)
- 開発チームの大勢が,技術プラクティスの背後にある理由を理解していない
- 作られたソフトウェアは良い標準から乖離
- チームが違えばチームごとの標準
- でも開発の中心人物・マネージャーは優秀
- 開発・QA(品質保証)・運用の対立主義
この結果,
- 「変更を加えづらいコード」
- 「変更を行うことのコミュニケーションコストが大きい」
- 「変更を加えるのにものすごい時間がかかる」
という状況になり,みんなやる気がなくなっていった(死のスパイラル)。
筆者は,マネージャーらと協力して,業務の20%を上記の現状を改善する活動に充てるようにした。 すると,アジリティの高いソフトウェアを作れる組織に変化させることができた。しかも,これには再現性があり 何回も同じような経験をしたらしい(武勇伝)。
余談。筆者は「自動ユニットテストのないコードはレガシーコードである」と言う。
「ウォーターフォールモデル」
ウォーターフォールモデルは,以下のような状況には適している。例は自分で考えてみた。
まとめてリリースするのが適切なモノ
- iPhone:部品の性能やサイズの組み合わせを高度に最適化する必要がある。人々を飽きさせないため, 年オーダーの周期で新製品を発表する。
- 自動車:大量生産のために大規模な製造ラインが必要
リリースするものや,ビジネス状況がある程度予測可能であるモノ
- 橋や道路などの社会インフラ:長期的にニーズがあり,安定してそこにあって欲しい
- 個人住宅の設計:新しい部屋を追加したいと思うことはめったにない
しかし,ソフトウェアを取り巻く環境は,上記に当てはまるだろうか?
ソフトウェアはまとめてリリースすべき?
- 今やほとんどのソフトウェアはインターネットの接続を前提に利用されるので,新しいバージョンを逐次配布できる。 (iPhone で同じことをやろうとすると,個人宅に iPhone 分解マニュアルと部品を郵送して行うことになるけれども)
- ソフトウェアは「大量生産」する必要がない。一度作れば同じものを容易に「複製」できる。
リリースするものや,ビジネス状況は予測可能?
- 顧客の刻々と変化するニーズに,素早くサービスを適合させなければ競争上の優位を保てない時代になった。 そしてこの変化を予測することは困難である。
- ソフトウェアには日々脆弱性が見つかったりバグが見つかるので,対応していく必要があるが, どこにこれが発現するかというのは予想できない。
一か八かの勝負
ソフトウェア業界におけるウォーターフォールモデルは,いろんな点で「大博打」である。
- 複数の機能が「統合」されるまで,それらの各機能にバグがあるかどうかわからない。 コーディングしてしばらく時間が経ってようやく最初のバグに気づくことができる。 小さく作ってすぐに検証できていれば早い段階で気づけたのに。
- プロダクトをじっくり時間をかけてリリースしても,もしそれを顧客がそれに「No」を突き付ければこれまでの開発は 無に帰する。例えば,これを避けるために『リーン・スタートアップ』では,MVP をベースに「構築・検証・学習」を高速に 繰り返し,早期に顧客からのフィードバックを得る。
ソフトウェアを作るのに,こんなにリスキーで間違いやすい方法はほかに想像できない
ウォーターフォールはなぜ機能しないか
「刺激」と「反応」が遠い
開発・運用・QA は縦割りで分離していることが多く,コミュニケーションコストを減らすために,タスクを一度にまとめる。 これはチームを縦割りにして,プロセスを厳密に分けることで構造的に発生すると私は考える。
余談:de:code 2020 の GitOps に関するセッションで,「GitOps 導入により,開発者にプロダクションのオーナーシップ を移譲することができるようになった。これにより,開発者のプロダクトに対する当事者意識が高まった」という話が あり,これに通ずるものを感じた。
バッチサイズが大きすぎる
まとめてデカいものをリリースするようにすると,それに最適化した結果,おのずと拡張性を考慮しないコードがかかれるようになる。 しかし,ソフトウェアの特性上,現実にはコードには変更が伴う。もし,バッチサイズを小さくして,継続的に統合・デプロイ するようにしておけば,保守性の高いコードがかかれるようになるだろう。
プロセスを重視して目的を見失わない
著者は,過去の体験から,プロセスを重視することを批判している。例えば,IBM では過去に「ソースコードの全ての行に プログラマーのコメントを入れること」という規則があったそうだ。 大事なのはプロセスではなく,本来の目的を達成することである。
ウォーターフォールの功罪
コーディングよりも最新のドキュメントの維持に膨大な労力が割かれることがある。
ソフトウェア開発はリスクが高く,ドキュメントもコードも書いた瞬間から時代遅れになる。 ウォーターフォールのプロセスがよくないのは,初期の設計フェーズが終わったらもう事実上コードを変更するのは 不可能なので,変更を避けるようになってしまうこと。
そしてテストも「高価」なので一度で成功させるために最後まで待つ。
こうして,痛みや困難を意図的に避けることが良い戦略ということになり,文化も保守的になる。
ソフトウェア開発に銀の弾丸はない
ソフトウェア開発において,最善の選択を下すための道標はなく,状況に応じていく通りもの解があり,厳密にその効果を 見積もることはできない。やってみてはじめてわかるものばかり。
本章の振り返り
- ウォーターフォールは,変更が起こらないことを仮定しているが,現実にそんなことはなく, いざ修正を加えようとするとその追跡と変更コストは高くつく。
- 機能をまとめてリリースするのは,逆説的なことに,非効率である。
- ウォーターフォールはレガシーコードの温床になる
- ソフトウェア工学には,現実の問題にうまく対処できるような基本原則や共通の知識体系が確立されていない。
第2章 CHAOSレポート再考
CHAOS レポート
スタンディッシュグループの CHAOS レポートという,US 最大規模のソフトウェア開発に関するレポートがある。 ここでは,ソフトウェア開発のプロジェクトを「成功」「失敗」「問題あり」の3つにカテゴライズしており,その年次推移を報告している。 この結果によると,1994年には「成功」したプロジェクトは全体の16%にすぎなかったが,2012年には39%にまで増えている。 これは業界の成熟とアジャイルの普及によるものだと考えられるが,依然として,6割のプロジェクトは何らかの問題を抱えている。
なぜプロジェクトは失敗するのか。
プロジェクトが失敗する要因には,ビジネスの優先度の変化や市場ニーズの変化など,外部要因によるものもあるが,ここでは 技術的な側面についてみていく。筆者は以下のようなもの,プロジェクトを失敗させる技術的要因としてを挙げる:
- コードの変更
- バグの修正
- 複雑さの扱い
コードの変更
一般に,コードの変更それ自体の時間よりも,既存の設計・コードを読んで理解するのに時間がかかる。 そのため,変更が大規模な場合は,全体を理解するのではなく,自分たちの方法で要求された機能を継ぎ足してしまおうとするケースが増える。 その結果,システム全体をテストするのが困難になり,品質は低下し,以降の拡張も難しくなる。
既存のソフトウェアに機能を追加するコストは桁外れに高い。というのは,世のほとんどのソフトウェアは 機能拡張をうまく扱えるように設計されておらず,ひとたび機能を追加するならば全体の再設計が必要になるためだ。 このようなハイリスクで高価なソースコードをいじりたくないので,既存のコードをいじらず新しくコードを追加し,継ぎはぎのシステムができる。
開発者は,素朴にコードを書くことで変更ができないコードを生み出していることに気づくべきだ。
バグ
バグの修正自体は些細ですぐ終わることもあるが,それを見つけ出すのには決まって長い時間がかかる。 MacOSX は 8500万行のソースコードから成る。我々は,バグが見つかりやすいように心がけるべきだ。 また,バグを書くのを防げればもっとよい。
コードの複雑性
2002年,NIST によれば
ソフトウェアの複雑さは増大し続ける。(略)ソフトウェア開発者はすでにほぼ 80% の開発コストを障害の特定と障害のために使っている。 これを以てしても,ソフトウェアほど不具合が多い状態で出荷されている製品はほぼない。
つまり,我々が価値を生み出している時間は開発時間の 20% しかない。
ほとんどのソフトウェアは,読みやすさよりも書きやすさを優先している。こうして書かれたコードには依存性が埋め込まれ, ある部分が別の部分に依存し … を繰り返して,依存性は追跡できなくなる。これが,メンテナンスコストが 80% にも膨れ上がる原因となる。
使われないコード
CHAOS レポートによると,デプロイされた機能の 45% は使われないということがわかった。 つまり,我々はコストをかけて無駄を作り込んでいることになる。
ウォーターフォールでは,ソースコードが後から追加されることは無いか,極めて少ないという前提がある。 そのため,1回の貴重なリリースのタイミングで「今を逃すまい」とたくさんの機能がブチ込まれる。
リーンやアジャイルの考え方を使えば,小さく作ってすぐ顧客に使ってもらえるのですぐにフィードバックが得られる。 これにより,無駄を作り込むリスクを回避できる。
コスト
少し古いが,2001年の研究(G.Robert, 2001) によるとソフトウェア開発にかかるコストは以下のようになっている:
工程 | 割合 |
開発 | 20% |
バグ | 11% |
拡張 | 38% |
その他 | 31% |
見ての通り,リリース後のコストはかなり高額である。 これは,ウォーターフォールが「変更を前提としないので,保守性を考慮していない」からである。
しかし,実際には表を見てわかるように,ソフトウェアには拡張やバグに伴う修正が入る。 保守性の高いコードを書こう!
本章の振り返り
- ソフトウェア開発の失敗による損失は,低く見積もっても年間100億ドル(US のみ)にのぼる。
- ソフトウェア開発の非効率なやり方はビジネスに多大な損失をもたらしてきた。我々はこの改善に取り組むべきだ。
- CHAOS レポートで指摘されるように,この課題を乗り越えるための道のりは遠い。
- 私たちの誰もがレガシーコードを作りうる。
このようにソフトウェア開発はめちゃくちゃリスクが高い。それでも,成功しているプロジェクトはあり, 彼らは別のアプローチをとっている。次章以降では,そのプラクティスをみていく。
第3章
2000年の終わり頃,成功を収めたソフトウェア開発者は,「余計なもの」に気づいていた。 そしてソフトウェア開発に適した,「軽量な」開発手法の体系が生まれようとしていた。
そこで,「軽量ソフトウェア開発プロセス」とかいうと真面目に受け止められないだろうと考え, 「アジャイルソフトウェア開発プロセス」という命名がなされた。 アジャイルの背後には,XP(エクストリーミングプログラミング),リーンなどの概念がある。
これらの先人の取り組みは,「レガシー」との闘いの始まりだった。
アジャイル
アジャイルマニフェストには,
私たちは,ソフトウェア開発の実践あるいは実戦を手助けする活動を通じて,よりよい開発方法を見つけ出そうとしている。
とある。これを筆者なりに言うと,以下の考え方が根底にあるらしい:
- 顧客満足を最優先し,価値のあるソフトウェアを早く提供する
- プロセスを増やすよりも,よりプロセスを少なくし,開発者が集中してエンジニアリングプラクティスを適用できるようにする
このエンジニアリングプラクティスというのは,XP における TDD や ペアプログラミングである。 これらを取り入れ,デプロイ,運用,拡張が簡単で変更可能なソフトウェアを作れるようになるという信念があったらしい。 そして実際こっちの方が効果があったらしい。
小さいほど良い
長距離マラソンランナーは,「近めのゴール」を設定して走っていると言われる。「あの街灯までいくぞ!」と思えば, 42.195km に比べればなんとかなりそう,簡単そうだと思える。例えばスクラムでは,1スプリントで 「大きなタスクを,目に見える成果を作り出す単位に落とし込む」。これにより,タスクの見積もり,実装,検証 が容易になる。
そして,このタスクのライフサイクルがすぐに終わるほど良く,作業に取り掛かって進展が無い状態(顧客に提供していない=無駄な状態) を短くした方が良い。
やり方ではなく,目的と理由
アジャイルでは,仕様書ではなく,プロダクトオーナーと開発者の会話を重視する。 ユーザーストーリー という概念が登場するのは,これを会話のきっかけにするためである。 そして,プロダクトオーナーはユーザーストーリーをもとに顧客と会話する。 ここで顧客の要求から詳細な仕様を聞き出して,その先も仕様書を維持管理しているとすれば,それは プロダクトオーナー付きのウォーターフォールに他ならない。
本章の振り返り
TODO: 書く
多くのアジャイルチームは技術プラクティスを認識していないか,適用の仕方が間違っているために,期待した利益を得られないでいる。 プラクティスを適切に適用するには,その背後にある原則を理解しなければいけないのだ。
第2部 ソフトウェアの寿命を延ばし価値を高める9つのプラクティス
すげー開発者は,普通の人がやらないちょっとしたことを普段から心がけることで優れた開発者となっていて,その技能は学習可能である。 我々も,彼らが身につけた原則やプラクティスを理解して実践しよう。
第4章 9つのプラクティス
プラクティスの背後には原理がある。ソフトウェア開発では,この原理が次第に確立されようとしている。
その例として,
単一責務の原則 「クラスを変更する理由は一つでなければいけない」
開放・閉鎖の原則 「ソフトウェアのエンティティ(クラス・関数・モジュール)は拡張に対して開き,変更に対して閉じなければいけない」
がある。こうした原則(本書では2つを挙げるにとどまっているが)を実現することがソフトウェア開発の崇高なゴールであり,すべてのプラクティスが これを実現するようなものでなければならない(つまり憲法)。
本書紹介するプラクティスは,こうしたソフトウェア開発で確立されようとしている原則を実現する9つのプラクティスを紹介するが,それらは以下の配慮されている:
- 多くの場合に価値がある
- 学ぶのが容易で,教えるのが容易である
- 実施がシンプルで,考えなくてもやれる
これらの条件を満たしていないと,プラクティスは継続しづらいものとなる。人間は怠惰なので,極力やりやすい仕組みを作る必要がある。
さて,先ほど原則の話が出たが,プラクティスはそれだけを盲目的に取り入れれば良いというものではない。きちんと原則を理解して,なぜそのプラクティスが有効なのか ちゃんと説明できる必要がある。そうすることでより効果的に適用できるようになる。
良いコード
価値のあるソフトウェアには変更が伴う。つまり,ユーザーがソフトウェアから価値を引き出す新たな方法を見つけたことを意味するからだ。 価値のあるソフトウェアは将来変更されることになる。こうした背景を踏まえると,ソースコードはどのような状態にしておくのが良いだろうか。
筆者は次のようにいう。
「既存ソフトのバグ対応・機能追加のコストを考えると,何を差し置いても,ソフトウェアは当初の機能要件を満たし将来のニーズに 対応できるように変更しやすくすべきだ。ソフトウェアを変更可能にできれば,初期開発の ROI を改善できる」
「ソフトウェアは資産であり,資産価値は現在生み出せる価値だけでなく,将来に生み出される価値にも依存する。ソフトウェア開発にも ライフサイクルマネジメントが必要だ」
また,筆者は次のような「品質」こそ重視すべきだという。
ソースコード・ソフトウェアには,「カバレッジ」「速さ」などの外部指標が存在するが,これはブラックボックスなソフトウェアの ある一面が数値化できたにすぎない。本書でいう「品質」は,「変更を加えやすいコードか」「わかりやすいコードか」 「リファクタリングされているか」といった 内部品質 を「品質」と読んでいる。これを作り込んだ結果,外部品質が向上する。
これは,「アジャイルをやっている」だけでは実現できない。アジャイルの背後にある原則を理解してはじめて効果的に 内部品質を向上させることができる。
9つのプラクティス
筆者は,こうしたいろんなスクラムや XP のプラクティスから,9つを抽出した。
- やり方より先に目的,理由,誰のためかを伝える
- 小さなバッチで作る
- 継続的に統合する
- 協力し合う
- 「CLEAN」コードを作る
- まずテストを書く
- テストでふるまいを明示する
- 設計は最後に行う
- レガシーコードをリファクタリングする
全てを厳密に徹底してやる必要はないが,ソフトウェア開発や保守におけるコスト削減の出発点になるだろう…
本章の振り返り
- 使われるソフトウェアには必ず変更が起こる。変更可能となるように書かれるべきだ。
- 何かを正確にモデル・システムにするためには,システム対象を理解しなければならない。
- すごい人がやってることは学習可能である。
- 未来を完全に予測できる能力より,必要になった時にすぐ変更を加えられるような能力の方が大事だ。
- ソフトウェア開発には独特の課題があり,これらに対応するためにはプラクティスの背後にある原則を理解する必要がある。
第8章 プラクティス4 協力し合う
平行遊び
幼児が同じ部屋で,別々のおもちゃで遊んでいることがある。しかし,彼らは互いの様子を観察していて,何をやっているか知っているが,お互いにふれあうことはない。
3~4 歳になると,「中年に至るまで続く複雑な人間同士の相互作用のニュアンスを理解し始める」。
我々も,仕事で 平行遊び をしていないだろうか?
コミュニケーションと協働
ソフトウェア開発は社会的な活動である。
「多くのコミュニケーションと相互作用を伴う。常に学び,常に交流し,抽象を扱い,抽象を語る。したがって,人と人との調整が極めて重要になる。」
ソフトウェア開発には人と人の円滑なコミュニケーションが不可欠であるにもかかわらず,人と人をスペーサーで区切り,平行遊び をしている職場は多い。
確かに,常にお互い顔が見えるようなデスク(共同体空間)で仕事をすることを,個人スペースの侵害やプライバシーがないと感じる人も多い。
しかし,ソフトウェア開発における人間的で抽象的な営みの効率を最大化するのは,空間をオープンにし,風通しをよくすることである。
エクストリームプログラミング
これは,ケント・ベックが自身のソフトウェア開発の成功体験から導かれた手法で, 「アジャイル開発でうまくいくことを,理論的に極限まで突き詰めた」ので,エクストリームプログラミングと名付けられた。
ペアプログラミング
開発者2人が1台のコンピュータを使って協働をすること。XP のプラクティスの中で最も価値がありながら過小評価されている。
2人で1つのことをやるので,「リソース」を半分浪費することになると誤解されている。実際は,以下のようなメリットがある。
- プログラミングにおける暗黙知が共有され,属人化したコーディングスタイルを回避できる。
- 「監視」
- 手抜きコードを書かなくなる(適切な命名,コメントをつけるようになる)。
- いい緊張感があり,仕事に専念できる。
- 知識共有における文書化のコストを減らせる。
- 「小さなバッチ」で仕事をできる(小さなプルリクエストとコードレビューをやるイメージ)。 - 協働の気持ちが高まり,コミュニケーションが円滑になる。
- ソースコードの「共同所有」
「デカいピアノを1人では運べないが,2人でやれば運ぶことができる」
理想的には,コーディングスタイルについて文書を書かず,ペアプロで共有できる方が良い。 コード自体が,コーディングスタイルのガイドになっている状態が望ましい。
ペアプロというのは,本質的には「書きながら行うコードレビュー」である。コードレビューのエクストリーム版である。 なので,ペアプロにおいてもコードレビューと同じ観点で議論をする。 例えば,コーディングスタイルの議論よりも,設計の理由や,設計のトレードオフなどについて議論した方が良い。
ペアの組み方
- 開発者の強み,弱みを補完しあう関係(フロントエンド&バックエンド)
- 経験者がある人と,ない人
- ランダムに人を割り当てる 👈 著者推奨
バディプログラミング
「ペアプロはエクストリームだ」と感じる人もいる。とはいっても,開発者には頻繁にフィードバックがあった方が良い。 ということで,段階的にエクストリームになるために,まずは「タスク完了時」「夕会」など,書いたコードについてレビューをする。
レトロスペクティブ
イテレーションの終わりに,チーム全員で集まって,「よかったこと」「わるかったこと」をざっくばらんに話す。 フォーマルで堅苦しい感じにならない方がよい。 些細な問題でも共有したほうがよい。いろんな問題から,アンチパターンの発見に繋がる。
この取り組みは,医療では「ポストモーテム」,軍事では「デブリーフィング」と呼ばれる。
学習を増やし,知識を広げる
以前はスペシャリストになれば仕事は保証され,替えの効かない知識があれば解雇されることもなかった。知識は硬貨のようなものだった。 こうした構造では,なかなか自分の知識を広く共有しようというモチベは生まれづらかった。
しかし今日,勉強会に参加したりカンファレンスで知識を共有する人がプレゼンスを高め,キャリアアップする世の中になっている。 学んだことを人に教える(教えられる程度に理解する)ことで,自分も業界全体もよくなる。そういう方向になっとる。
ペアプロ 7つの戦略
- とにかくやってみる
ペアプロは正しい方法でやれば,絶対好きになる。
- ドライバーとナビゲーターが参加する
ドライバーはコードを書き,ナビゲーターはテストを書いたり,全体を見通して議論する。
- 役割を頻繁に交代する
20〜60分ごとにドライバーとナビゲーターを交代する。双方向で学ぶ。
- 正直な1日を過ごす
ペアリング=一日中「オン」の状態になるので,一緒に濃密な時間を過ごす。隠し事をすると無駄にエネルギー使うぞ。
- すべての組み合わせを試す
いろんな組み合わせ,ペアリング時間,役割交代時間を試して,チームに合うやり方を模索しよう。
- 詳細はチームで決める
マネジメント側から強制されて決めるのではなく,チームの価値観にあうやり方をみつける。
- 進捗を追跡する
ペアリングの価値を計測する。ベロシティ,障害,コード品質を追跡することで,これらが改善するのがわかるはず。
レトロスペクティブ 7つの戦略
- 小さな改善を探す
組織は,大きくかつ同時多数の変化を好まないが,小さな変化を小さなステップで繰り返すことならやりやすい。 こうした習慣を付ければ,変化に順応しやすい組織風土ができてくる。
- プロセスを責めよ,人は責めない。
人を責めても問題は解決しない。多くの場合,人の問題は,組織やプロセスによって構造的に引き起こされ,再現性がある(これは私の持論)。 人を責めず,問題の発生を許しているプロセスの問題を見つける。
例)レビューで同じような指摘がおこるのを防ぐために,PR テンプレートを用意して,最低限チェックすべき項目を用意する。
- 5回の「なぜ」
「なぜ」を繰り返して根本的な課題を探す。繰り返した先に,予想しなかった問題が見つかる。
- 根本原因に取り組む
対処療法ではなく,問題の根本原因に対処する。
- 全員の意見を聞く
チーム全員がレトロスペクティブに参加する。声の大きい人だけでなく,みんながフェアに発言できるようにする。 継続的改善は全員の責務である。
- 人に権限を
改善に必要な物を人に与えること。変化をサポートすること。変化に対する率先を評価することを示すこと。
- 進捗を測る
誰もが目標とできる計測可能な成果,メトリクスを用意し,ゴールに向かってチームが進んでいることを全員が確認できるようにすること。
本章のふりかえり
ソフトウェア開発は,抽象的な営みで,人同士のコミュニケーションが欠かせない。 知識をグループに広めるために,協働しよう。
- 質の高いコミュニケーションとチームへの知識拡散のために,適切なテクニックを使う(ペアプロ,バディプログラミングなど)
- 未知の探究,学習の増幅,知識の伝搬に,協働スキルを活用しよう
- コードレビュー,レトロスペクティブで常にフィードバックがもらえる仕組みをつくろう
- チーム全員がメンターであり,メンティーであれ。みんなが学び,教え合う関係になろう
第9章 プラクティス5 「CLEAN」コードを作る
コードの品質指標,CLEAN とは
- ロバート・マーチンが『Clean Code: アジャイルソフトウェア達人の技』で提唱。
- コードの品質の指標についてのアクロニム(頭文字)
それぞれ,
- Cohesive : 凝集性
- Loosely Coupled : 疎結合
- Encapsulated : カプセル化
- Assertive : 断定的
- Nonredundant : 非冗長
簡単にいうと,オブジェクトは特定が明確に定義されていて(凝集性),はっきりした責務を担い(疎結合),実装は隠蔽されているべきだ(カプセル化)。 オブジェクトの状態は自分自身が管理し(断定的),オブジェクトの定義は一度だけにすべきだ(非冗長)。という原則。
高品質のコードは凝集性が高い
それぞれの部品は一つのものだけを扱う。それぞれのクラスが自分の責任に集中し,自分の仕事をうまくやるように設計する。 こうしておけば,何か1つの変更が必要が生じた時に,対応するクラスだけ変更すればよくなる。 変更による影響調査とテストが簡単になる。
そのようなクラスには,その責任を表す端的な命名が可能である。もし命名できないなら,色々やらせすぎている可能性がある。
高品質のコードは疎結合である
オブジェクト間の関係を明確な意図を持った状態に保つことを「疎結合」という。 疎結合なコードは,それを利用しているコードに対して 間接的にしか依存しない 。よって,分離,検証,再利用,拡張が楽になる。
疎結合は通常,間接的な呼び出しによって実現される。サービスを直接呼び出すのではなく,中間層を通じて呼び出す。 これにより,サービスに変更があっても呼び出し元の変更の影響を減らすことができる。
高品質のコードはカプセル化されている
実装の詳細は,外部の世界からは見えなくなっている。
より厳密にいうと カプセル化 とは,以下を切り離すことである。
- インタフェース(自分がやろうとしていること)
- 実装(どうやってやるか)
中で何やっているかを隠蔽し,入出力だけを公開するので,他のコードに影響を与えずに自由にコードを変えられるようになったり, コードがモジュール化されるので扱いやすくなるなどのメリットがある。
アウトサイドインプログラミング
コンシューマの観点で機能を設計する。サービスはクライアントのニーズに基づいて設計される。 サービスが何をやっているかを示す名前をつけ,それがどう動くかは隠す。 これによって,サービスの高レベルの結合を意識できるようになる。
インサイドアウトプログラミング
問題を小さなかたまりに分解し,それらを縫い合わせて1つのソリューションを作るというもの。 実際にコーディングにいきつくまでに必要になる。ただし,全体像への配慮が欠けると責任が明確でない壊れやすいコードが生まれがち。
良いカプセル化のためには,上記の2つの視点が必要だが,やはり,高レベルの視点で眺めることが肝要。
そのコンポーネントは,何なのか,何のためにあるのか から始める。
こうした高レベルの視座があれば,コンピュータのバックグラウンドがないドメインエキスパートとのコミュニケーションも可能になる。 詳細を切り離しておけば,チーム全体でシステム化対象の理解がしやすくなり,コミュニケーションコストも下がるし,柔軟性,一貫性の向上に役立つ。
ソフトウェアがどう作られているかではなく,ソフトウェアを使うことによって得られる体験から始める。
カプセル化の方法
色々ある。
- 概念を抽象化して隠す
- メソッド化する
- インタフェースを導入する
ソフトウェア開発においていつでも通用する「知らぬが仏」パターンがある。依存が少なければ少ないほど,コードの変更は簡単になる。
しかし,カプセル化したものを公開する必要がある時もある。その時は,必要に応じて小出しに公開する。
とりあえずクラスのデータを private
で宣言しておいて,必要があれば getter
, setter
などでアクセスできるようにする。
カプセル化を習慣化することで,呼び出し元を意識して設計するようになる。入出力が明確になり,システムの相互作用における副作用が減り, コードを説明する明確なドキュメントにもなる。
メソッドシグネチャなどは,カプセル化の良い方法である。これは「実装ではなくインタフェースをプログラムする」というパターンである。
必要なものだけを公開し,それ以外は隠すこと。
高品質のコードは断定的である。
断定的とは?
👉 自分が何をやるコードなのか,自分の責任はどこなのかはっきりしている。
問 🔍
Docs
の印刷を制御するコードは,Docs
クラスの一部にするべきか,Printer
クラスにすべきか。
- 一見
Printer
クラスが良さげだが,印刷する中身はDocs
が持つ。つまり,印刷を制御する要素の一つがPrinter
以外にあり得ることを示す。Printer
は,Docs
が *.pdf か *.jpg というDocs
特有の事情を扱いたくない。そんな実装をしていたら, 新しい拡張子が追加されたときに,Printer
まで影響を受けてしまう。
上記のような問題は,SOLID 原則の 依存性逆転の方速 = 具象は抽象に依存しなければならない とも関連する。
筆者は,他のオブジェクトとの依存性が多いオブジェクトを「好奇心旺盛なオブジェクト」という。
オブジェクトが機能するために,常に何らかのオブジェクトを参照する必要があり,いずれそれは人で追えなくなる。
これは先ほど述べた カプセル化 の原則とも反する。
なので,オブジェクト自身で状態を管理できる方が良い。
こうした問題はなかなかわかりづらいので,普段から適切な場所に移すようにした方が良い。
高品質なコードは冗長でない
ミッションクリティカルなアプリケーションは冗長でも良い。人の命がかかっているから。そしてこれは,意図的な冗長である。
ここでは,意図しない冗長 について語る。c.f. DRY原則 (Don’t Repeat Yourself)
長ったらしいコードがあって,それを別の場所で使いたい時,コピペした方が良いだろうか?おそらく,関数名をつけて使いまわした方が良い。
💡余談:『リーダブル・コード』では,
/util
(ユーティリティ用のコード置き場)を用意することを推奨している。
コードを書いた当事者なら,「ああ,これはこのパターンをコピペしたんだな」とすぐわかるが,時間が経てば一目ではわからなくなる。
ちゃんと一つの関数にまとめておけば,そんな労力は必要なかったのに〜ということになりかねない。
1つで済むもの冗長になっていると,テストもビルドも読む時間も冗長になってつらくなる。できるだけまとめて抽象化しよう。
コード品質が私たちを導いてくれる
CLEAN コード の原則は,ちまちましたものかもしれない。でも,これらは,
- オブジェクトははっきりと定義された特徴を持つ
- オブジェクトは自分の責任に注力する
- オブジェクトの実装の詳細は隠されている
- オブジェクトの自身の状態は自分で管理する
- オブジェクトは一度だけ定義される
という恩恵を与えてくれる。
長ったらしいが,
- コードに凝集性があれば,理解もバグを見つけるのも簡単になる。それぞれのエンティティは1つのことしか扱っていないから。
- コードが疎結合であれば,エンティティ間の副作用が起こることも少なくなる。テスト・再利用・拡張がより簡単になる。
- コードがカプセル化されていれば,複雑さを管理し,呼び出し元がその先の詳細を知らなくても良いように維持できる。詳細を変更しても,呼び出し元に変更は必要ない。
- コードが断定的であれば,振る舞いを配置する場所が,依存データがある場所であることを示す。
- コードが冗長でないなら,バグ修正や変更を1箇所で1回だけやればよい。
これらの原則は,相互に独立しているわけではないので,どれかを達成すると他のも達成できるような感じになっている。 コードをチェックするときの観点として1個2個妥当そうなものについて着目すると良さそうである。
筆者が好きな原則は Cohesive(凝集性) だそうだ。これは簡単に見つけて直せる。コードがやっていることに名前をつけることを 手助けしてくれる。
明日のベロシティのために今日品質を上げる。
技術的負債
技術的負債とは,開発中に学習したことをコードに反映しなかったときにおこる「負債」を説明するものだ。
その時に対応しておけばすぐ終わるものを,先延ばしにすることで,同じリファクタリングをするのに数倍もの時間がかかってしまう。 コードの知識が失われてしまうためだ。
「確かに,カプセル化のために新しいクラスを作って波カッコを余分に入力しなければいけないかもしれないが,これまでソフトウェア開発において タイピングがボトルネックになったことなどあるだろうか?」
ベロシティが高く,ビジネス状況に対応s知恵最高のソフトウェアを顧客に届けられる組織では, みな CLEANで高品質のソフトウェア を土台にしていた。(著者談)
次の章では,ちゃんといいコードを書けているかチェックする方法を伝授する。それが,テスト・ファーストなプラクティスだ。
コード品質を上げる7つの戦略
人によって品質の定義は様々だ。やはり,開発の前に,品質について合意が取れておくのが良い。
1 品質の定義を明確にする
高品質のコードとはどういう特徴を持つべきか,確認する。
2 品質のためのプラクティスを共有する
どうやったら,高品質を維持できるか。そのプラクティスについて考えて,共有する。
#### 3 完璧主義を手放す
- 完璧に作るのは不可能である。
- 限りある,必要最小限の受入基準をもっておけば,作り込みすぎる(金メッキ)を避けることができる。
4 トレードオフを理解する
開発者だけでなく,ステークホルダーを把握などプロジェクトの全体像を把握し,どこにどういうトレードオフが存在するか理解する。
5 「やり方」を隠す
実装の詳細をカプセル化し,インタフェースを公開する。呼び出し元は,どうやって実現されるかを気にせず,欲しいものが得られるようにする。
これよって,開発者としては,あとから実装の詳細を変更する自由が得られる。 もし,呼び出す人が実装の内部に依存したものをすでに作ってしまった場合,彼らに配慮する必要が出てくる。
6 良い名前をつける
プロジェクトで最も重要なドキュメントは,ソフトウェア自身だ。
- エンティティやふるまいには,「何をやっているのか」を示す名前をつける。
- 命名には一貫性を持たせ,略語や一部のメンバーしかわからない造語は避ける。
- 名前は長くても良い。ソフトウェア開発でタイピングはボトルネックにならない。
- 「説明的」「能動的」「肯定的」な命名をする
7 コードをテスト可能に保つ
テストコードが動くだけでなく,すぐいつでも動かせること(いろんな前提条件がないと動かないとかはダメ)。
保守しやすいコードを書く7つの戦略
保守しやすいコード = 理解しやすく扱いやすいコード
コードの共同所有を取り入れる
- チームメンバーの誰もがコードベースに変更を加えられるようにする。
- コーディング規約を共有し,一貫性を持たせる。属人性を排除する。
- 共通のドメインモデルを使って作業し,共通の開発プラクティス,設計を表す共通の用語を使う。
リファクタリングを熱心に行う
- リファクタリングは絶対常に行う。
- コードについて学んだことを今反映させなければ,「負債」となり,今後の時間の大多数を「新規開発」ではなく「負債の返済」にあてることになる。
常時ペアで行う
- ペアプロが,知識の伝達・維持に有効である。
- 毎日でもいいからペアを変える。
- 少なくとも「設計」「コーディング」「リファクタリング」「デバッグ」「テスト」の際にはペアでやるべき
頻繁にコードレビューする
- ペア以外でもいいからレビューを頼む
- なぜその方法を選んだのかの意思決定の理由に着目し,設計の選択肢やトレードオフについて議論する
他の開発者のやり方を学ぶ
他人のコードを読んで,他人の書き方を学ぶ。これはスキル向上の良いプラクティスである。
ソフトウェア開発を学ぶ
- 20年前はソフトウェアの本なんてほとんどなかったが,今はたくさんある。
- 医師は週に8~10時間勉強している。開発者も同じだ。
コードを読み書きして,コーディングの練習をする
スティーブン・キングはこう言っている
「作家になりたいなら,絶対にしなければいけないことが2つある。たくさん読み,たくさん書くことだ」
本章の振り返り
コードを読む時間は書く時間の10倍と言われている。「CLEAN」なコードを書けば,その時間を節約できる。
本章のまとめ〜CLEAN コード〜
- 凝集性:似たコードを一つにすることで副作用を減らせる
- 疎結合:テストや影響範囲の特定が容易になる
- カプセル化:実装の詳細を隠し,拡張がしやすくなる
- 断定的:責任を明らかにし,ソフトウェアがモジュール化される
- 非冗長:保守の問題を減らす。
10章 プラクティス6 まずテストを書く
TDD はちゃんと適用するのが難しいので,じっくり理解しよう。
テストとは何か
受入テスト = 顧客テスト
- ストーリーのふるまいを明確にし,開発者がプロダクトオーナー・顧客と本質的な対話をすることができるもの。
- 「ほら,このテストが動いているから,ちゃんと期待するものができましたね」ってみんなで合意できるもの。
- このテストは,「どうなれば完成か」を示すと同時に,「ストーリーの作りすぎ」を防いでくれる。
- 受入テストでは,given,when,then = 前提,トリガー,結果 という基準で記述する。
ユニットテスト = 開発者によるテスト
- ストーリーよりも小さいユニットをテストするもの。TDD は必ずこれを作る。
- UT は内部ドキュメントとしても機能する。テストとドキュメントが1個で済むので時間の節約にもなる。
- そして,あらかじめこれがドキュメントだとチーム内で共有されていれば「ドキュメントどこ?」ということにもならない。
それ以外のテスト =QAテスト
- 実際の依存関係を使用してコンポーネント間の相互作用をテストする(結合テスト)。
- 依存関係が多い分複雑で,動作は重いので,ユニットレベルでできることはユニットテストでするべきだ。
- で,なんであっても依存関係は少なくした方が良い。特にソフトウェア開発プロセス。プロセスにチームを跨いだ人間の介入が必要になると,それだけ遅くなる。
QA(品質保証)とは
QAテストについて紹介する
- コンポーネントテストとは,ユニットがどのように連携するのかを調べるものだ
- 機能テストとは,ユニットをまとめて e2e のふるまいを調べる
- シナリオテストは,ユーザーが実際にシステムと対話するテスト
- パフォーマンステストは,負荷とかに耐えれるか調べるテスト
- セキュリティテストは,コードの脆弱性を探す
プロジェクトに必要な QA テストはリスクによって異なる。なんでも全部やれば良いというものではない。
それで,この QA テストもリリース直前とかではなく,自動化して普段から確認できるようにすべきだ。 でないと,リリース前になって見つかったバグを対処する時にコストがかかる(このバグなんだっけ,と思い出したり,ドキュメントやソースコードを1から読むハメになる)。
マイクロソフトとかアマゾンとかは,もともと QE という専門のエンジニアを置いていたが,こうしたマニュアルテスターを「自動化を行える開発者」に置き換えていった。
テスト駆動開発は,QA の代わりではない
テストを後から書くのは,スキャンダルが発覚してから監査役を置くようなもの。あとから問題を探すのはコストがかかる。はじめから問題を解消するようにコーディングすれば,時間を節約できる。
テストファーストの目的は,「ふるまいを検証する」のではなく,「ふるまいを客観的な方法で表す」こと。
ユニットテスト is not 万能,でも書こう
ユニットテストは e2e に比べれば不十分だが,でもユニットテストはほんとに些細な小さな問題に時間を費やすのを防いでくれるから大事。
システムは一般にテストがないものが多いが,もし権限があるなら,アーキテクチャを変更してでもテストしやすいコードにしよう。
いいテストを書く
「自分が実装したものと,そのフィードバック」を繰り返す最小単位が,テストファーストの真髄であるといえる。
リーンやアジャイルには,「早期に顧客からフィードバックをもらうことは善である」という哲学が流れている。
テストファーストは,自分一人で実践可能な「実装→フィードバック」のプラクティスなんだね。
すごい人の曰く,一般にありがちな「テストアフター開発」は効率が悪いそうだ。偉い人がいうからおそらく正しい。
テストファーストは,常にコードカバレッジ100%が確保できるのもいいところ。なぜなら,テストをパスするための最小のコードしか書かないから。
コードもシンプルになるし,可読性もアップする。
テストファーストって何のためにするの?
「テストを書きやすいコードを書く習慣をつけるためにする」
優れた受入テストのための7つの戦略
受入テストは,ストーリー・タスクなどのあらゆるレベルで適用可能。これに関する7つの戦略を紹介。
- 作っているものが何の役立つのか明確にする。
- 誰が何のために何をしたいのかを知る。(c.f. 単一責任の原則)
- 受入基準を自動化する。
- エッジケース,例外,代替パスを表す。予め,エッジケースを想定して作業できる。
- 例示を使って詳細を具体化し,矛盾を一掃する。
- 受入基準とふるまいの分離。受入基準が実装に依存するものであってはならない。🙅♀️ 例)実装の変更でテストが変更される
- 各テストを一意にする。全てのテストは固有であり,他のテストから独立していなければならない。
優れたユニットテストのための7つの戦略
- 呼び出し側の視点に立つ。常に呼び出す視点からサービス設計する。入出力から考える。
- テストを使ってふるまいを表す。ユニットテストは,システムの生きた文書になる。これが最も最新の仕様となる。
- 新しい違いを生み出すテストだけを書く。念のため冗長にするとかはだめ。ユニットは一意で他から独立して重複しない方が良い。
- 失敗したテストにパスするためのコードのみを書く。無駄なものを書かない。これによって,全てのコードがテストでカバーされていることを保証できる。
- テストを使って,ふるまいを作る。
- コード・テストをリファクタリングする。「実装ではなくふるまいをテストしていれば,コードをリファクタリングするときに,テストを追加,変更する必要がない」
このように,良いユニットテストがそろっていれば,
- リグレッションの確認ができる
- リファクタリングしやすい
- これが内部文書のいちばん信頼できる形式になる
かくして,「テストファースト」で「保守コストの低い高品質なソフトウェア」を作れるようになったのであった。
本章の振り返り
最初にテストを書き,次にテストに合格するのに必要なコードだけを書く。これによって,開発するソフトウェアの焦点を絞り,テスト可能になる。
- あまりにも多くのテストを書いたり,実装依存のテストを書いたりすると,テストファースト開発は簡単に失敗する
- テストはコードのリファクタリングをサポートする必要があり,ふるまいをあらわすのに必要なテストだけを記述する
- テストファースト開発は QA に役立つが,QA に代わるものではない
- テストファースト開発は失敗するテストを作成し,合格するのに十分なコードを書くことで機能を作る。そのあと,必要に応じてリファクタリングし,失敗する別のテストを書くことを繰り返す。
11章 プラクティス7 テストでふるまいを明示する
12章 プラクティス8 設計は最後に行う
- 全ての設計を最後まで先延ばしにすべきとは言わない。ホワイトボードでやる,ざっくりとした議論は全然良い。
- ソフトウェアの設計活動の中には,開発サイクルの最後に実施したほうが効果的・効率的なものがある。
- 詳細までは初期の段階で決めない方がよい。なぜなら,コードを書いて得られた学びをもとに,保守性の高い設計を作りこむ方がコスパが良いから。
- 適用するデザインパターンやシステムの理解は,一通り作ってしまってからの方が得られやすい。
- テストを最初に書いて,最後に設計をしよう。
変更しやすさへの障害になるもの
「変更しやすさ」の観点から「やらない方が良いこと」
- カプセル化の欠如
あるコードがほかのコードを「知って」いると,直接・間接に依存性が増える。こうなると,小さな変更が無関係に思えるところに予想外の問題を引き起こす。
- 継承の過度な利用
景勝は OOP において有用な機構だが,過度の利用や誤った利用は,無関係な問題を紐付けたり,深い継承ツリーを生み出して保守上の問題を起こす。
- 具体的すぎる凝り固まった実装
重要部分の抽象化はした方が良い。これをしないと,冗長なコードや複雑なコードになってしまい,変更が困難なコードになる。
- インラインコード
メソッドに切り出さず,コードをインラインでコピペした方が組み込み系のプログラミングでは効率的であるとされてきた。 しかし,最近はコンパイラがメソッド呼び出しを最適化してくれるので,可読性のために,詳細すぎる処理はメソッドとして切り出した方が良い。
- 依存性
一緒に扱う必要のない問題を,1箇所で行わない。1つのコードには,1つの役割を持たせる。
- 作ったオブジェクトを使うか,使うオブジェクトを作るか
インスタンス化するユーザーが,オブジェクトの細かい部分まで知っている必要があると,辛い感じになる。 ユーザー側のコードが,呼び出されるコードの実装の詳細に依存してしまうので,呼び出されるコードに変更が加わると,ユーザー側のコードにも変更が必要になってしまう。
ソフトウェアは,ちょっと雑なままでも 大丈夫 である。なので,上記のような好ましくない慣行があっても,一応ソフトウェアは動いてしまう。 だが,PJ が大きくなると,こうした悪行から逃れる必要がある。
持続可能な開発
変更しやすいコードを書くためのプラクティスを示す。
- 死んだコードを消す
コメントアウトされて使われないコードは開発者の注意を逸らす何者でもない。何らかの理由で必要になったら,バージョン管理システムから戻せば良い。
- 名前を変更する
メソッドやクラスの名前を,意図のわかるいい名前に変える。コードの変更によって,コード中のメソッドの機能もちょっとずつ変わるので,体を表す名をつけるようにする。
- 判断を集約する
判断を集約し,一回だけで済むようにする(?)(よくわかってない)。
- 抽象化
全ての外部依存性には,抽象を作成し利用する。モデルにエンティティは作成する。モデルはモデル化対象の特性を正しく抽象化している必要がある。
- クラスを整頓する
クラスが完全かどうかチェックし,正しいふるまいと正しい属性を持つようにクラスを整頓する。モデル化対象のドメインからエンティティを見逃さないようにする。
コーディングとクリーニング
- コーディング:特定のタスクの解決方法を模索する。
- クリーニング:動くコードを,保守可能にする。
このように,コーディングとクリーニングでは目的が異なる。筆者的には,これらは別のタスクとして実施するのが良いぽい。
技術的負債は,短期的にも長期的にも返済を続けよう。短期的には,テストファースト開発におけるリファクタリングにおいて。長期的には,チームの学びをコードに取り入れるための定期的なリファクタリング作業において。
コードは書かれる回数より読まれる回数の方が多い
コードはかかれる回数より10倍読まれるらしい。なので,コードを読みやすく保っておくことには合理性がある。
コードにおける命名やコメントは,「意図」がわかるようにする。コードの「やり方」ではなく,そのコードによって何を成し遂げたいのかを意識する。
「やり方」はコードそれ自体が語るべきだ。もし「やり方」がわかりづらいコードなら,その説明を行うコメントをつけるのではなく,コード自体を理解しやすく変更する。
意図によるプログラミング
コードにおいて,抽象度は揃えたほうがよい。抽象的なコードはそれとして1つのコードに書き,詳細の実装は他のコードに移譲する。 こうすれば頭の中で抽象レベルのスイッチングが起こらず,理解しやすくなるし,このように階層化されたコードはバグの追跡が容易になる。
循環複雑度を減らす
循環複雑度は,トーマス・マッケイブが「A Complexity Measure」で発表した概念で,コード中の実行経路の数を示す。これはテスト容易性に関する指標となる。
条件分岐が1つのコードの循環複雑度は2。コードは2種類の異なる振る舞いを示しうる。
循環複雑度は少ないに越したことはない。一般的に,この数はユニットテストのメソッドの数に等しいからだ。
ポリモーフィズムなどのテクニックを使い,循環複雑度を減らし,コードを疎結合にしよう。
生成と利用を分離する
オブジェクト指向言語では,オブジェクトを生成(インスタンス化)してから,オブジェクトを利用する。
TODO: あんまり理解していない。
本章の振り返り
- 品質は保証できない,品質は作り出す。品質を検証するのではなく,作り込むことに集中しよう。
- 設計を作り込むことにリソースをさくよりも,読みやすく理解しやすいコードを作ることの方が,柔軟で変更しやすいシステムのために重要だ。
- オブジェクトの生成と利用を分離することで,テスト容易性を改善し,依存性を下げよう。
設計を書くなら,一度コードを作ってそこで得た学びを反映するために行うのがよい。その設計に基づいてリファクタリングし,より保守しやすいコードにすることで, レガシーコードの「死のスパイラル」から脱出することができる。