コードの可読性を下げる視覚的複雑性パターン (2023)
(seeinglogic.com)- コードが論理的に優れていても、長く読むのがつらいことがあり、この文章はそうした疲労の原因を 目に見える複雑さ に求めている
- Halstead Complexity Metrics は演算子と被演算子の個数を数えて Volume と Difficulty を計算し、同じ動作でも変数や演算子が多い実装をより難しいとみなす
- SonarSource の Cognitive Complexity は、省略構文、線形フローの中断、ネストした制御フローを中心に、条件分岐・ループ・例外処理・論理演算子の組み合わせ・再帰・
gotoの負担を評価する - 変数の観点では、変数シャドーイング(variable shadowing)、似た名前、長い変数生存期間、見慣れない使用パターンが、読者のデータフロー追跡コストを高める
- 小さな関数、見慣れたパターン、長いチェーンのグループ化、単純な条件式、限定的な
goto、浅いネスト、区別しやすい変数名、短い変数生存期間は、言語やフォーマットを超えて適用できる可読性の基準になる
コード可読性指標の限界と目標
- コード可読性には 広く使われ、合意された単一の指標 がない
- 参考にできる資料は、現実にはあまり広く使われていない学術論文や意見に近いものが多く、コードレビューでそのまま使える、より具体的な議論の道具が必要だった
- 目標は新しい指標を発明することではなく、コードが読みやすいかを話すときに誰でも使える 視覚的パターン を集めることだった
- 検討対象は、次の条件を満たす測定やアイデアだった
- ソースコード片や単一関数に適用できる
- Cyclomatic Complexity のように、実装アルゴリズムそのものと切り離しにくい 本質的複雑さ だけに集中しない
- 変数名の長さ、空白、インデント、括弧配置のような表面的なスタイル要素だけにとどまらない
Halstead Complexity Metrics
- Maurice Halstead は 1970 年代後半、ソースコードの経験的測定値を作るために Halstead Complexity Metrics を提案した
- この指標は言語やプラットフォームを問わず適用でき、実装されたアルゴリズムそのものより、コードがどのような形で書かれているかに焦点を当てる
- 中核となる測定値は、演算子と被演算子に基づく 4 つの個数である
- 固有演算子数
n1 - 固有被演算子数
n2 - 総演算子数
N1 - 総被演算子数
N2
- 固有演算子数
- Halstead はこれをもとに、プログラムの length、volume、difficulty といった関連指標を作り、実装に含まれるバグ数を推定する値まで導き出そうとした
- 直感的には、演算子が多いほど潜在的な相互作用をより多く検討する必要があり、被演算子が多いほどデータフローの可能性を理解しにくくなる
-
JavaScript の例
- 同じ偶奇判定関数でも、
ifとreturnを使うシンプルな実装は演算子と被演算子が少ない - 固有演算子 4 個、総演算子 7 個
- 固有被演算子 5 個、総被演算子 6 個
Volume33.30、Difficulty2.50- 配列、
Number、比較式、インデックスを使う実装は、より多くの演算子と被演算子を持つ - 固有演算子 7 個、総演算子 10 個
- 固有被演算子 9 個、総被演算子 12 個
Volume71.35、Difficulty3.75- 1 つ目の実装は見た目にもより単純で、Halstead の
VolumeとDifficultyの値もそれを裏づけている - 欠点は、あらゆる言語で何を operator と operand とみなすかが明確でない点であり、測定では特定のツールや実装を決めて一貫して使うほうがよい
- 同じ偶奇判定関数でも、
-
Halstead から得られる実用的パターン
- 関数が小さく変数が少ないほど、一般に読みやすい
- 言語特化の演算子やシンタックスシュガーは読者に追加の負担を与えるため、乱用しないほうがよい
map、reduce、filterのような関数型コンポーネント、ラムダ、イテレータ、内包表記を長いチェーンでつなぐと、簡潔でも 可読性 が下がることがある- こうした長いチェーンは、JavaScript や Rust、あるいは Python で
itertoolsに深く依存したコードに、より頻繁に見られることがある
Cognitive Complexity が見る読みづらさ
- SonarSource が作成した Cognitive Complexity は、読みづらさをより正確に捉えるための指標である
- この指標の中核となる考え方は 3 つある
- 文を結びつける省略構文は難度を下げる
- 線形フローから外れるたびに難度が上がる
- ネストした制御フローは難度を上げる
- 名前が科学的または客観的な指標のように聞こえるという批判はあるが、実用面では有効なヒューリスティックとみなせる
-
省略構文の密度問題
if (a != null) { myObj = a.myObj; }の形より、MyObj myObj = a?.myObj;のような省略構文のほうが短く、読む時間も少なくて済む- ただし両者は実際には完全に同じではない可能性がある
- 前者では
myObjはa.myObjまたはnullになる - 後者では
myObjはa.myObjまたはundefinedになる - TypeScript や Rust のように型検査が強い言語でも、抜け漏れの可能性を減らせるだけで、すべてのケースを正しく処理する保証にはならない
- 一般的な JavaScript のように型検査支援が弱いと、こうしたコーナーケースが処理されない可能性はさらに高くなる
- 省略構文は書きやすく読みやすいこともあるが、簡潔さと密度 の間にはトレードオフがある
-
線形フローを壊す要素
- 条件分岐のない線形コードは、条件分岐のあるコードよりざっと見やすい
- Cognitive Complexity は条件分岐、ループ、
gotoだけでなく、条件付きマクロ、try/except、論理演算子の連なり、再帰も難度を上げる要素とみなす switchは 1 つのグループとして数える一方、else-ifチェーンはelse-ifが増えるごとにより難しいとみなす- これは
else-ifでは各分岐で 2 つ以上の比較を行えるためである - ただし
switchの fall-through やbreakの欠落も、読みづらさを高めうる - 条件式の中で同じ論理演算子を連続して使う場合と、
&&、||、!を混在させる場合では難度が異なる debug || verbose || consoleModeは単純な条件式であるdebug || (verbose && consoleMode)は演算子の混在により、より読みにくいdebug || !(verbose && consoleMode)は否定まで含むため、さらに複雑になる
-
例外処理と goto
try/catchは Cognitive Complexity では難度を上げるが、複数のcatchブロックを 1 つのcatchより難しいとはみなさず、tryとfinallyは無視される- 例外を投げる行為そのものも読み取りコストを生むことがある
- 例外処理が関数境界をまたぐと、関連する関数同士の複雑さが絡み合う
- 読者はその例外がどこで捕捉されるかを探さなければならない
gotoは通常、難度を上げる要素として計算される- ただし、エラー条件でリソースを解放しながら関数を抜ける
goto outやgoto doneの形は、有用でありうると考える専門家もいる - 一方で、
continueやbreakで表現できない形でループ境界をまたぐgotoは、読者が新たな制御フローを再構成しなければならないため、読む負担が大きい
ネストと関数の形
- 条件分岐自体が読みにくいなら、ネストした条件分岐はさらに読みにくい
- Cognitive Complexity は条件分岐やループ自体の点数に加え、ネストの深さごとに追加の難度を与える
- この考え方は "Level of Indentation" や "Bumpy Road" のような名前でも呼ばれる
- ネストが 2 段階を超えると特に読みにくくなり、早期リターンでネストを減らしたコードのほうが、より平坦に読める
- この指標は関数の長さを直接反映しないが、他の条件が同じなら、長い関数は短い関数より読む労力が大きい
変数名、生存期間、見慣れたパターン
-
区別しやすく説明的な名前
- 説明的な名前は、コードが何をしようとしているかを理解するうえで重要であり、重複した名前や暗号のような名前は逆効果になる
- 変数シャドーイング(variable shadowing) は危険である
- 読者がスコープ規則を追って、どの変数が使われているかを見分けなければならない状況は避けるべきである
- 視覚的に似た識別子も避けるべきである
iとj、itemとitemsのように、目で見て混同しやすい名前はミスを招きうる- 1 つの関数内で
node、_node、thisNodeのような同じ変数名の変形を複数使うコードは、バグを生みやすい形である
-
短い変数生存期間
- ライブ変数解析(live variable analysis) は、変数が最初に使われる地点から最後に使われうる地点までの範囲を見る
- 変数生存期間が長いと、読者はより多くの変数とその取りうる値を頭の中に保持しなければならない
- 変数を関数の先頭ですべて宣言するより、実際に使う直前で宣言するほうが生存期間を短くできる
- 最悪なのは、変数が複数の関数にまたがって生き続け、あちこちで使われる状況である
- 複数の変数の生存期間が実質的に同じなら、オブジェクトのほうが適切かもしれない
- オブジェクトが適切でない場合は、読者が値を理解するために読まなければならない関数や行数を最小限にするほうがよい
-
長いチェーンと中間変数
- 関数型プログラミングのスタイルは変数生存期間を短くするが、長すぎるチェーンやコールバックのネストは読む負担を生むことがある
- 長い関数チェーンは小さなグループに分け、適切に名前づけした中間変数やヘルパー関数を使うと、読者の認知負荷を減らせる
- 中間変数を使う版は、わずかに効率が劣る可能性はある
- ただし性能ツールがその行が実際のボトルネックだと示していない限り、その種の微小な効率差は重要ではない
-
見慣れたコードパターンの再利用
- 見慣れたコードや変数の形を再利用すると、読者は既知のパターンを認識できるため、より楽に読める
- これは Principle of Least Surprise と通じる
- コードベース内では、条件分岐の書き方のような繰り返し現れる形を一貫して保つほうがよい
- パターンから外れる必要があるときは、変数名やコメントで違いを示せる
- この考えを突き詰めると、繰り返し現れるパターンを読者が再認識しなくて済むよう、テンプレート関数やジェネリック関数を使う方向につながる
可読性を高める 8 つの視覚的パターン
- Line/Operator/Operand count: より小さな関数と、より少ない変数・演算子のほうが読みやすい
- Novelty: 関数の形、演算子、シンタックスシュガーで目新しさを避け、コードベースの共通パターンを再利用する
- Grouping: 長い関数チェーン、イテレータ、内包表記は、ヘルパー関数や中間変数で論理的なグループに分ける
- Conditional simplicity: 条件式は可能な限り短く保ち、1 つの条件内では異なる論理演算子を混在させるより、同じ演算子の連続を優先する
- Gotos: 特定のエラー処理パターンに従い、代替案のほうが悪い場合でない限り
gotoを使わない - Nesting: ネストしたロジックと大きなインデント変化を最小限に抑え、深いネストが必要なら大きな関数の中に埋め込まず、別関数に分離する
- Variable distinction: 説明的で視覚的に区別しやすい変数名を使い、変数シャドーイングを避ける
- Variable liveness: 変数生存期間を短く保ち、とくに関数境界をまたぐ長い生存期間に注意する
実際のコードベースで表れた問題
- 精神的疲労を大きく生むコードベースは、複数のアンチパターンを同時に抱えている
- 具体的には、長い関数、多様な言語構成要素の混在、ヘルパー関数に分離すべきだった多くの関数チェーンがあった
- その結果、大きな関数の中で ネスト複雑性 と長い変数生存期間が同時に増大していた
- コードと作者の品質が高かったにもかかわらず、1 つ以上の致命的なバグが見つかった
- そのうちの 1 つは見ればわかるバグだったが、長く複雑な関数の真ん中にあって推論しにくかったため、見逃された可能性が高い
- 1 か月後に自分のコードを最も多く読む可能性が高いのは自分自身であり、読みやすい形で書くことは、直接的な実務コストにつながる
1件のコメント
Hacker Newsのコメント
記事で、map/reduce/filter のチェーンが長かったり複数あったりすると可読性を損なう、と述べている点は、他の内容からはまったく導かれていない
慣れていないから悪いと言う、ありがちな不満をこっそり差し込んだように感じるし、少し慣れれば代替案よりずっと読み書きしやすい場合が多い
例えば
books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()より読みやすいコードを提示するのは難しいと思う複雑度の指標で見ても、このようなコードはほぼどんな別の方法よりも優れており、関数型プログラミングの基礎くらいは学ぶべきだ。モナドを説明する必要まではないが、
mapとfilterをむやみにけなさない程度には慣れているべきだおおむね5つの呼び出しが続くチェーンあたりから読みにくくなり始めると見ていて、記事の例もそのくらいの長さだった
“multiple” はネストしたチェーンがある場合や、操作している型が変わる場合を指しており、そのような場合は読む速度が落ちる
関数型の構成要素はエレガントになり得るが、使いすぎることもある
チェーンが長くなりすぎ、条件が複雑になり、リストや変数が増えると、一度に理解するのが難しくなる
手続き的なループでは中間結果に名前を付けることができ、その名前のおかげで前に行った処理を忘れて次の段階に集中できる
SELECT DISTINCT author FROM books WHERE pageCount > 1000;個人的には中間結果に名前を付けるためにチェーンを分けることが多く、変数名をコメントのように使う感覚だ
var longBooks = books.filter(book => book.pageCount > 1000)var authorsOfLongBooks = longBooks.map(book => book.author).distinct()?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).Prologデータベースの構造によっては、もっと短くもできる
?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).良いコードには根本的に、定性的で文学的な側面がかなり大きいと思う
数学的思考に慣れていて定量的な答えを求めるプログラマーや学界の人たちは、この点を不快に感じることが多い
Dostoyevsky と Wodehouse のどちらも好きだが、どちらも素晴らしいものの、そのやり方は大きく異なる
コーディングがそこまで開かれた場ではないとしても、質的にまったく違って感じられる良いコードベースをいくつも扱ったことがあり、新しい作家の文体に慣れるように、コードベースのスタイルも「感じ」がつかめるまで時間がかかることが多い
ファイルを上から下へ読むと物語が自然に追えるように関数の順序を決め、読者に語りかけるような宣言的な実装を作った、という意味だった
純粋関数型プログラミングのパラダイムに従っているが、この方法はより物語的なスタイルによく合うと思う
関数の依存関係や入力は引数か他の純粋関数に限定され、出力は戻り値の型だけに収まるため、複雑さを段階的に案内しやすい
隠れた状態のような複雑さがある他のパラダイムと違い、皮肉なことに、最も数学的に厳密なパラダイムが、より物語的なスタイルにも最もよく合うと思う
形は関係なく、長い開発経験がなくても妥当な時間内に関数が何をするのか分からないなら、それは単に悪いコードだ
例えば記事に出てきた三項演算子
return n % 2 === 0 ? 'Even' : 'Odd';は、人間の脳には逆向きに読まれる感じがあり、人間よりもコンパイラが構文木を処理するのに向いている人間の数学者なら、
n mod 2 = 0なら'Even'、そうでなければ'Odd'のように区分関数の形で書くだろうし、そのほうがはるかに明確だ新しいチームメンバーが入ってきたときに一貫したスタイルを身につけさせ、チームのスタイルも適度に一貫して保てる
.editorconfigに関するコメントも参考になる: https://news.ycombinator.com/item?id=43333011プルリクエストでスタイルの細部をめぐって議論することを減らしてくれる
記事は良いが、コードを読むときに最も精神的に疲れる要素である可変性を見落としていると思う
メソッドを読みながら変数の意味を一度だけ「固定」し、残りを推論している間それをそのまま保てるのは大きな贈り物だ
メソッドの理解は0%から100%へ単調に増えていくべきで、特定の反復でループ本体がアキュムレータをどう変えたのか分からなくなって、メソッドを頭の中で最初からやり直す必要があってはならない
GOTOが有害である本当の理由もここにある。メソッド内で頭の中の命令ポインタを動かすのが難しいのではなく、GOTOがあると可変変数の状態が分かりにくいのが問題だ
可変変数も不変変数もその移動を助けることも妨げることもあり、コードがその空間にどれだけきれいに対応しているか次第だ
不変変数には、値が変わったり誤解を招く形で変わったりする心配がないという戦術的な利点が少しあるが、経験上「常に不変性を使え」というルールにするほど大きくはない
ときには可変性のほうが、その情報空間をはるかにきれいに表現できる
呼び出し地点ではなく呼び出される側の観点で見ると、誰かが特定の行へジャンプできる場合、その前に何が起きていたのかを逆追跡できない。どこから来た可能性もあるため、局所解析ではなくグローバルなプログラム解析が必要になる
可変性がGOTOの複雑さの本当の源なら、
if文やforループにも同じ問題があるはずだ可変性と状態が複雑さを直接生むことには同意するが、GOTOはまったく別の、はるかに有害なカテゴリだと思う
個人的に嫌いなパターンは、
ifで即座に返し、残りを暗黙のデフォルト経路にするやり方だif (n % 2 === 0) return "Even"; return "Odd";のほうが短いのは確かだが、if ... return "Even"; else return "Odd";のほうをずっと好む理由は前者が非対称に感じるからだ。
"Even"と"Odd"は対称的な選択肢なので、elseがあるほうがより直感的だreturn (n % 2 === 0) ? "Even" : "Odd";のように書くほうがボイラープレートが最も少なく、いちばん読みやすい三項演算子がある言語なら、簡単に認識できるはずだ
経験上、
elseは追加のネストを生み、正常系の直接的な範囲を超えた境界条件や変数を評価し続けさせやすい。その文脈をすべて抱えていなければならない関数名のすぐ下、1段階インデントしたところで戻り値を視覚的に確認でき、早期終了がなければ保証された結果があるという感じがする
戻り値がもっと下に埋もれていると、どこかぎこちなく感じる
return "Odd";がifから離れて見えるように空行を入れ、言語が許すならif本体に波括弧も追加するだろうelseを許容する状況もあるが、多くは副作用がある場合で、通常はなくなるまでリファクタリングするとより明確になる複雑なコードが、重要度や実行コストの順に抜けていくガードのシーケンスに変わることはよくあり、実際の関数/メソッドのロジックを終了条件から分離してくれる
最初の方式は下へ流れていき、2つ目の命令集合を実行することになる。
returnは制御フローを断ち切る特殊な演算子なので、最初の方式の通常の制御フローは2つの場合の完全性をうまく表せていない慣用的なRustでは、メソッドの制御フローを壊す例外的な場合でなければ
returnを使わず、2つ目の例は通常、戻り文なしでより頻繁に見られるPythonも通常、不正な引数や状態では序盤で早期リターンし、末尾位置のreturnが実際の戻り値になる形で書く
こうした慣習のため、完全な
if-else構造を崩すと、インデントされたreturnが例外的な状況のように見える。この慣習に従うと、return文は制御フローを断ち切る場合を除いて自然と重複のように見え、Rustの慣習が理解できる。どの言語でもreturnはbreakと同等の文だもしかすると自分だけかもしれないが、TypeScriptはコードを読みづらくすることがある
データモデルをある程度「アトミック」に保ち、開発者が型をきちんと宣言し文書化するなら問題ない
しかしユーティリティ型で型が型から派生し始め、明示的な型を省略して型推論に頼ると、すぐにほころび始める
4〜5段階の型の間接参照のような深いスタックでは、フィールドがどこから来たのか追跡するのが非常に難しくなる。一部は推論で、一部は明示的で、一部は派生型で、フィールドのエイリアスも混ざる
大きなデータモデルと深い呼び出しスタックでは、
function checkDogs(dogs: Dog[]) { ... }のように戻り値の型を省略した形はまったく使い物にならず、本当に気が狂いそうになる主な理由は、その関数から返されるすべての経路がその型を守るよう強制するためだ
新しい条件を追加したときに、別の分岐とは少し違う型を返してリグレッションが起きた例をたくさん見てきた
ただし、変数宣言に型を付けることには大きな価値はないと思う
例で言えば
const checkedDoggos = checkDogs([])は良く、checkedDoggosが関数の型を継承するようにしておけばよいリンターが
const checkedDoggos: DogBreedAndSize[] = checkDogs([])を強制するコードベースを扱っているが、かなりばかげていて、ほとんど価値がないJavaScriptでは確信が持てず、スタックの上下を行き来し続けながら頭の中に保持しなければならない
「変数が少ない小さな関数は一般に読みやすい」という言い方には注意が必要
可読性の議論がミクロな可読性だけに集中するのは嫌いです。そうなると、マクロな可読性よりミクロな可読性のほうが重要だという誤った前提のもとで、コードが過度に細かく分割されがちです
こうした教条化は、森を見ずに木だけを見るプログラマーを生み、過度に非効率なコードやデバッグしにくいコードを生みます
APL 系の言語は反対の極端にありますが、実際の最適点は中間のどこかにあり、個人によって大きく異なるのだと思います
慣れていないコードで定義へ移動を3〜4回するだけでもきつくなります。自分の問題かもしれませんが、ほとんどの人が自分よりずっと得意だとは想像しにくいです
.NET の文化、とりわけ「clean architecture」では、この問題は衝撃的なほどです。機能を修正したり問題を追跡したりしようとすると、4つのレイヤーと15個のファイルに散らばっていて、ファイルによっては60%以上がキーワードだけだったりします
どこで線を引くべきかは分かりませんが、細切れすぎて5行ごとに上下へスクロールしなければならないコードよりは、他の推奨事項に従いつつ順番に読める長い関数1つのほうを、たいていは好みます
型やクラスも同じで、この DTO でしか使わない4値の enum をわざわざ別ファイルに置く必要はありません
興味深い記事ですが、満足はできません
結論へ飛ぶのが早すぎて、また個人の好みに戻っています。いくつかの好みには同意しますが、記事自体は明示的に好みを超えようとするものでした
「言語固有の演算子やシンタックスシュガーは読者の負担になるので避けるべきだ」という話は、指標から導かれていません。関数に異なる演算子が3つあり、言語固有の演算子1つがその3つを一度に置き換えるなら、関数の「労力」は減ります
map/reduce/filterのような構成要素も、うまく使えば他の演算子を置き換えて「体積」を減らすので、どちらにも転び得ます?.の例は、JavaScript の読みにくい言語設計を指す、非常に言語特化の診断に見えます。多くの言語ではnullとundefinedが分かれていないため、一般に null-safe operator と呼ばれます「変数のシャドーイング(variable shadowing)はひどい」と「生存期間が長いほど、より多くの変数を頭の中に保持することになる」は互いに衝突し得ます
文脈によっては変数のシャドーイングがとても好きです。以前のインスタンスにアクセス可能なままにする代わりに、スコープから取り除いてくれるからです
VS Code 向けの Highlight という素晴らしいプラグインがあります
ユーザー定義の正規表現でコードに別の色を適用でき、よくある用途は
//TODOを黄色にすることだと思います私はログを薄く表示するために使っています。あちこちにログを入れると視覚的なノイズが大きいからです
保守しているライブラリでは
this.logger?.info('Some logs here');のようなログを書いており、これに 0.4 の透明度を適用して背景へ退かせていますそれでも見えはしますが、ぱっと見では実際のビジネスロジックのほうがより目立ちます
設定は次のように変更して使えます:
"highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }https://marketplace.visualstudio.com/items?itemName=fabiospa...
長い関数チェーンやコールバックを小さなグループに分け、良い名前の変数を使うやり方がやや効率が悪いという話には同意しません
2つのバージョンは同じくらい効率的になり得ます
どちらの場合も同じオブジェクトが割り当てられ、ヒープに保存され、ガベージコレクションの対象になります。効率の差はコンパイラ次第です
2つ目のバージョンでは、コンパイラは各変数が宣言直後にしか使われないことを見て、チェーン呼び出しと同じようにそれらのオブジェクトをスコープ外として扱えるはずです
もちろん、変数の型を推論させるという前提です
実際に目にするコストは、中間値をデバッガで見るために明示的に具体化する場合です。たとえばリストにすると、避けられたはずの割り当てが発生してコストがかかります
「可読性」を定量化しようとする試みは良いです。こういうアプローチがもっと必要です
今もっとも一般的な可読性の定義は、「自分が読みやすいもの」に近い感じです
非常に多くの人にコードを見せ、そのコードが何をするかを説明する文を選ばせながら時間を測れば、可読性の実際の次元を見つけられるかもしれません
最も多くの人が最も短い平均時間で正解した問題が、現実世界で読みやすいコードの例になり、さらに重要なことに、本当に読みづらい慣行を特定する助けになるかもしれません
回答者は「プログラミング経験」「パラダイム X を理解しているか」といった軸でクラスタ化されそうですし、流行が変わるにつれて結果も時間とともに移動し得ます
何を読み書きする方法を学んだかが、何を読みやすいと感じるかを形作ります
何をしようとしているのか、誰と働いているのか、コーディングの前に何ができたのか、他にどんな言語を知っているのかなど、多くの要因が影響します
取りやすい果実を取り尽くした後、たとえば変数名を恣意的・無関係・誤解を招くものにしないというレベルを超えると、多くの「可読性」問題は結局、合意形成に関するものなのかもしれません
一緒に働こうとしている特定のプログラマー集団を超える正解はないのかもしれません
コードの可読性は言語の可読性に似ていて、たいていはその言語をよく知らない人にとって問題になり、時間をかければ解決します
プログラミングの本当の問題はコードの複雑さであり、これは個々のコード片の指標だけでは判断できません
問題は関数本体の中の実装上の選択よりも、関数同士の関係にあります
コードが何をするのかを把握するのはたいてい簡単で、難しいのはそのコードを修正したり機能を追加したりすることです
複数の抽象化レイヤーが互いにどう接続されているかを隠すため、こうした難しさが生まれます