世界を動かす技術を、日本語で。

認知負荷が重要である

概要

  • 認知負荷 がソフトウェア開発における最重要課題であることを解説
  • 認知負荷の種類 とその削減方法を具体例で紹介
  • 深いモジュール浅いモジュール の違い、設計原則への影響を説明
  • マイクロサービスや言語機能 が認知負荷へ与える影響を考察
  • 実用的なアドバイス と実体験をもとに、より良い設計選択を提案

認知負荷が重要な理由

  • ソフトウェア開発における 混乱や遅延の主因 は認知負荷にあり
  • コードを読む際、 変数値・制御フロー・呼び出し順序 などを短期記憶に保持する必要性
  • 一般的な人間は 4つ程度の情報 しか同時に保持できない制約
  • 複雑なコードや設計は 理解コスト・修正コスト の増加要因
  • 認知負荷の削減 が、開発効率や品質向上の鍵

認知負荷の種類と削減

  • 本質的認知負荷 :タスク自体の難しさに起因、削減困難
  • 余分な認知負荷 :情報提示方法や設計による負担、削減可能
    • 本稿では 余分な認知負荷 の削減に重点
  • 例:複雑な条件式は 中間変数の導入 で分かりやすさ向上
  • 例: 早期リターン によるネスト削減で「ハッピーパス」重視の思考負荷軽減
  • 継承の多用 は負荷増大要因、 合成の推奨

モジュール設計と認知負荷

  • 小さなメソッド・クラスが正義」という神話の見直し
    • 浅いモジュール :インターフェースが複雑、機能が小粒、相互作用の理解が困難
    • 深いモジュール :インターフェースは単純、機能は強力、内部に複雑さを隠蔽
  • UNIX I/O のようなシンプルなインターフェースの例
  • 情報隠蔽 の重要性、浅いモジュールでは十分な隠蔽が困難
  • モジュール責任の誤解 :「1つのこと」より「1人のユーザー・ステークホルダーへの責任」が本質
  • 認知負荷の観点から 責任範囲の設計 を見直す必要性

マイクロサービスと認知負荷

  • 浅いマイクロサービス の乱立は、診断・統合の難易度と認知負荷を増大
  • 分散モノリス の問題点:修正時の影響範囲拡大、デプロイ・運用コスト増
  • 適切な論理境界 の見極めには時間と情報が必要
  • ネットワーク層の導入は慎重に :必要になるまで単一モジュールを維持
  • 大規模サービスの事例 (Linuxのモノリシック設計)から学ぶ柔軟性と維持性

言語機能と認知負荷

  • 新機能の追加 は一時的な学習コストだけでなく、 将来的な再理解コスト も増
  • 機能の数より、互いに独立(直交)しているか が重要
  • C++ など複雑な言語では、仕様変更や例外的挙動の理解が長期的負担に
  • 選択肢を絞ることで認知負荷を低減

実体験からのアドバイス

  • 浅いクラス80個 のプロジェクトは、1年半後に理解困難
  • 深いクラス7個 のプロジェクトは、再開時も素早く把握可能
  • 「Clean Code」や「Small Functions」信仰の再考 を推奨
  • 認知負荷 という視点で、設計・分割・責任範囲を見直す重要性

まとめ

  • 認知負荷の最小化 が、最も普遍的かつ実践的な設計指針
  • 設計原則や流行語よりも、人間の制約 に着目した判断が重要
  • シンプルなインターフェース・適切な情報隠蔽・責任分担 が維持性と拡張性の鍵
  • 複雑さの隠蔽 ができているか、 認知負荷を増やしていないか を常に自問
  • 設計選択に迷ったら、認知負荷の観点から見直す ことが最良のアプローチ

Hackerたちの意見

おそらく、私は「変わり者の賢い開発者」の一人だと思う。抽象化を作ることを試みているんだけど、業界が「if文の山アーキテクチャ」に戻っているのを見て、ちょっとモヤモヤしつつも興味を持っている。シンプルだと思い込むのは簡単だし、理解している気になるのも簡単だし、割り当てられたJiraチケットを閉じるのも簡単だから、みんながこれを好む理由はわかる。タスクが割り当てられたら、関連しそうな場所を探して、if文を追加するだけ。テストして、失敗したらまたif文を追加。最終的にQAに送って、問題が見つかれば、別のif文で解決。リリースされたら、十分な割合で動作するから、失敗例には気づかない。実際にコードが正しい確率はほぼ0%。if文を追加し続けて、正しさに近づく感じ。もし数百万の人の個人データを漏らしても責任は問われないし、認知負荷も常に低い。でも、実際にもっと良い代替案があるのかはわからない。派手な抽象化やアーキテクチャを作ることはできるけど、それがコードの正しさを高めるとは限らない。特に企業環境では、ビジネスロジックのオーナーがそれを大切に扱わないから、美しい抽象化を作るのは難しい。「一つの注文が一つの住所に送られる、シンプルに、作って、あ、実は営業マンが大口顧客に約束したから、一つの注文が複数の住所に送れるようにしなきゃ」みたいな話、聞いたことあるでしょ?企業環境では、慎重でバグのない抽象化を作るのは無理。じゃあ、if文の山がビジネスソフトウェアにとって最善の策なの?

じゃあ、if文の山がビジネスソフトウェアにとって最善の策なの?「Big Ball of Mud」の論文を楽しんでみて。実際のシステムは劣化しやすい。まずは、何を作りたいかわからない状態で大きな泥の塊から始める。そしてシステムの一部が成長するにつれて、設計を改善していく。でもまた状況が変わって、美しい抽象化が崩れてしまう。生産ソフトウェアは常に変わっている。それが面白いところ。あなたの仕事は、ドメインモデリング、十分な抽象化、そして建設的な破壊を組み合わせてこれをサポートすること。村から成長する街のように。

先週Codex CLIで遊んでたんだけど、バグを修正するために特別なケースをコードに追加するのが好きみたい。パターンを指摘して新しい抽象化を作るように頼まないと、見えてこないんだ。彼は「ヒューリスティック」と呼ぶものを追加し続けるだけで、それはバグのときに出てきた特定の条件をテストするif文だった。特定のタイプのバグに対して10個のテストを書いたら、彼はそれを全部喜んで修正してくれる。でも、同じタイプのバグで別のテストを追加すると、当然失敗する。なぜならCodexが考えた修正は最初の10個のテストに合ったif文の山だったから。

この種のソフトウェアで素晴らしい抽象化を作れると思うけど、それを生き残らせたいなら、ビジネスロジックそのものには関わらせないようにしなきゃ。これは製品のようなものにしかできない:認証、監査ログ、データベースの抽象化(CQRS、イベントソーシング)、コンテンツ/翻訳管理、メッセージングインフラ、実際のインフラ。ビジネスそのものがそれらの抽象化に影響を与えたり、指示したりすることを許すと、また混乱が生じる。ビジネスロジックはごちゃごちゃになるのが当然で、それは誰も本当に気にしないからで、責任を開発者や他の誰かに押し付けられるから。逆に、「良いコード」と「悪いコード」を分けることもひどい結果を招くことがある。私が働いていたフィンテックで見た「解決策」の一つは、ビジネスの人々自身にロジックを持たせることで、意思決定エンジンの形で実現された。基本的に、ビジネス自身が自分の泥の塊を維持することを強制された。テストするのも理解するのもシミュレートするのも不可能だった。最終的に、ソフトウェアオペレーターが雇われて、基本的にグラフィカルインターフェースを使ってコードを書くジュニアレベルの開発者だった。何度か書き直されたけど、いつも2、3年後にはすべてが混乱してしまう結果になった。

コードはif文があってもシンプルにできる方法がたくさんある。if文をあちこちに散りばめているなら、それを持ち上げてみて。最終的に同じ場所に集まるから、すべての変動性が一つの場所で実装されて文書化される。抽象化は必要ない。入力と出力を正確にモデル化するのが非常に役立つ。統一データ型を決めるのはできるだけ後回しにして、その決定に合わせてプログラミング言語を使いやすくする。クラスの階層やパターンは、実際に何が起こっているのか確信が持てるときの最後の手段だ。さらに言えば、プログラミングが管理しやすい限り、関数やファイルは必要ない。別のファイルが必要になるのは、バージョン管理システムが使えない場合か、これらの日時ハンドラがどこでも一貫して再利用される必要があると確信している場合だけだ。現代のフルスタックプログラミングは、モデル、ミドルウェア、コントローラー、ビューなどで溢れていて、誰もがそのすべての分離を最初から必要としているわけではない。

ビジネスロジックの大半は「ラストマイル」ソフトウェアだよね。美しい抽象の上に構築されてるけど、それは正しいっていう抽象的なアイデアからじゃなくて、現実との痛い衝突から生まれたものなんだ。最終的に、良い抽象を作るための明確さを得ることができる。時にはラストマイルソフトウェアがその抽象に変わることもあるけど、そうならないことが多い。俺は、早すぎる段階でこういう抽象を作ろうとする賢い開発者たちと一緒に働いたことがあるけど、現実にぶつかると、ただの混乱した「if文のスープ」になっちゃうんだよね。

最近、同僚とこの話をしてたんだ。俺の意見では、たくさんの(ソフトウェア)エンジニアリングの知恵やベストプラクティスは、ビジネスの要件やロジックの前ではうまくいかないと思う。ハードエンジニアリングでは、もっと強く反発できるけど、それはもっと永続的で、命がかかっていることが多いからなんだ。でもソフトウェアの場合は、そうはいかない。俺は、本当に速く動くビジネスの制約や、短期的な利益のための馬鹿げたリクエスト(製品を維持するために)のおかげで、適切なソフトウェアエンジニアリングがほぼ不可能になっていると思うし、実際にはこれらのif-elseのネストが必要になってくるんだ。だから、ソフトウェアエンジニアリングとプロダクトエンジニアリングを区別すべきだと思う。

個人的には、最後の部分で本質に触れてると思う。現実の世界やビジネスはごちゃごちゃしてて、まさに「if文の山」なんだよね。問題自体が技術的だったり一般化できる場合は、抽象化によって何千ものif文を書く開発者が必要なくなるけど、ドメイン自体がごちゃごちゃしていて仕様が不明瞭なら、抽象化(やツール)が役立つのは柔軟性を持たせることだけだよ。矛盾がバグじゃなくて機能かもしれないからね…

抽象化はOKだけど、SomethingFactoriesはバカげてる。もしコードが実際のロジックよりも抽象化が多くて、その抽象化を管理するためにロジックが必要なら(例えばFactoryFactoriesや2つ以上の継承レベル)、戦略を見直した方がいいよ。

意図を表現したり、基盤となるドメインを説明する方が、単なる「抽象化」よりも価値があるかもしれないね。

ビジネスの人たちがビジネスロジックをちゃんと理解して、実装者に説明できるとは思えない。絶対に無理だよ。彼ら自身は理解してるかもしれないけど、コーダーじゃないし、コードの要件を書くことはできない。だから、少なくとも一人の実装者はアプリケーションの実態をしっかり理解する必要がある。めちゃくちゃ汚れるくらいにね。本当にユーザーが毎日どんな体験をしているのかを知り、気にかけるようになるまで。まあ、ほとんどの会社にとっては現実的じゃないけど、別々のサイロを作って、「ビジネスロジック」は「どうでもいいこと」って意味になって、if文で遊ぶことになるんだよね。(あるいは、まあ、モナドにハマってそれで遊ぶのもいいけど。そっちの方がクールだし。)

これの一部は、素晴らしいプログラミング本の一つ「Code Complete」の推奨を思い出させる。

この文章は、私がMicrosoftで過ごした初期の頃を思い出させる。私は開発部門(DevDiv)で8年間働いていた。Microsoftには、最終的に「コンテキスト内の人々」というもっと複雑なペルソナフレームワークに置き換えられたソフトウェアエンジニアのための3つのペルソナがあった(この記事とのアイロニーは私にはわかる)。でも、そのオリジナルのペルソナは今でも私の中に残っていて、他のエンジニアと効果的に働くために非常に価値がある。モート - ビジネスの結果を最も重視する実用的なエンジニア。「if文の山」で素早く仕事を終わらせて要件を満たすなら、モートは不名誉な言葉になってしまった。VB開発者はしばしばモートだったし、Access開発者もそうだった。エルビス - 新しくて刺激的なことをすることを最も重視するロックスターエンジニア。最新のフレームワークや技術を最初に使うこと。革新に対して注目を集めて称賛される。コードは少し不安定かもしれないけど、速く動いて壊すのが正しいよね?エルビスは自分のコードの見た目の素晴らしさにも気を使っていて、4層の抽象化?それを理解するには天才が必要だし、エルビスは自分が書いたから理解できる。みんなが自分が天才だと知ることになる。Microsoftの多くのエンジニア(特にキャリアの初期)にとって、エルビスが昇進するのは、エルビスが目立っていて常に革新しているからだというのが前提だった。アインシュタイン - アルゴリズムを重視するエンジニア。アインシュタインは、最もパフォーマンスが良く、最もエレガントで、最も技術的に正しいコードを書くことを望んでいる。アインシュタインは、ビジネスの問題を解決するかどうかよりも、「Pythonic」なコードを書いているかどうかを気にする。アインシュタインは、コードベースを一貫させるために新しい条件を追加するために200行のコードをリファクタリングする。アインシュタインは関数型言語が大好き。これらのペルソナは実際のエンジニアを表しているわけではないけど、ほとんどのエンジニアはこの3つのどれかが主なものとして数日間のPRと単一のデザインレビューで特定できる。

マイクロソフトって、開発部門、開発掛け算、開発足し算、開発引き算(解雇される前に移されるところ)があるくらいバルカン化してるの?

DevDivでめっちゃ楽しかった!

明らかに、20年間他の人のひどいコード(自分のも含めて)をレビューしてきたエンジニア、アマンダがいなくて寂しかったんだろうね。彼女は、シンプルに保つことを学んできた。彼女は、コードは主に人が読むために書いていることを知っている。アマンダたちの小さなチームが欲しいな。

アニマルファーム、でもひねりがある。

ソフトウェアエンジニアのための三つのペルソナ これは避けるべきサイコバカの話で、互いに尊重し合っていれば起こらないことだね。マイクロソフトから来てるのは驚きじゃないけど。

「何をするかではなく、なぜするのかを文書化せよ」と言われている。私は「なぜ」と「何」を分けるのが難しいから、両方を文書化している。「何を文書化するか」の最大の犯人は、x = 4 // xに4を割り当てるみたいなやつ。そういうのはやめて。コードにたくさんのコメントを混ぜるのは良くない。読みづらくなるし、コードとコメントの間でのコンテキストスイッチが難しい。代わりに、こういう風にするべき:// 何かをするつもりだ。コードは// それをする。// それをする必要があるのは、// ビジネスが// ウィジェットやその他のものを必要としているからだ。setup(); t = setupThing(); t.useThing(42); t.theWidget(need=true); t.alsoOtherStuff(); etc(); etc(); コードとコメントは分けておくべきだけど、何をするかを述べるのは全くコメントがないよりはマシで、認知負荷を減らすのに役立つ。

俺は、必要な時には両方をドキュメントするのは全然気にしないよ。時には「なぜ」を明確にする必要があるし、時には「何」を明確にする必要もある。コメントは一般的に過小評価されてると思う。毎行に注釈を付ける必要はないけど、逆にほとんどの自己文書化されたコードはそうじゃないからね。

俺はこのスタイルのコメントにもっとシフトしようとしてるんだ。俺はプログラマーとしての教育を受けてないからね。物理学者で、ほとんどの物理学者は「コードにコメントが欲しいから、こうやってやって」って感じなんだ。そして学生としては、良い成績を取るために必要なことをするんだよね。

x = 4 // xに4を代入 うん、チャットGPTスタイルのコメントだね。 > 代わりにこういうのをやってみて: 唯一のマイナスは、コメントが古くなってコードと同期しなくなる可能性があること。コードに触れたら、他のコーダーはコメントを更新するように気をつけるべきだよ。

一時変数を使って認知負荷を下げるのは、ここで評価されている以上に考えやスキルが必要なんだ。特に、これらの変数は非常に良い名前を付ける必要がある。そうしないと、コードを読む人は、言葉が自分のビジョンにぴったり合わない場合、何が抽象化されているのかを思い出さなきゃいけなくなるからね。例えば、> isSecure = condition4 && !condition5 だと、実際の適切な名前は「shouldBeSecureBecauseWeAlsoCheckedCondition3Before」みたいになることが多い。ある程度、抽象を避けてコメントを入れる方が読みやすくなることもあるよ。著者の「スマートな」コードはこうなるかもね。if (val > someConstant // 有効 && (condition2 || condition3) // 許可されている && (condition4 && !condition5) // 安全 ) { ... }

AIエージェントの一つの特徴は、コメントよりも isValid = val > someConstant に移行したことだね。Cursor(おそらくClaudeも)って、コメントを頻繁に削除したり書き直したりするから。条件チェックが大きくなる場合は、isValid = checkForValidity(val, someConstant) にすることもある。

関数やバリデーションを、関数の最上部に早期リターンリストのように構造化するようにしてる。もし(val って著者もこのテクニックについて言及してるね。特にコントローラーAPI関数で役立つと思う。コードがかなり監査しやすくなるから。条件が何度も繰り返されるのを見ると、それがミドルウェアの候補になるか考えるようにしてる。これを新しい開発者に説明しようとするんだけど、全然理解してくれないか、目をひんむいてくる。この記事を送ったら、少しは助けになるかな。

みんな正しいかもしれない。変数を割り当てることは、良いコメントよりもあまり役に立たないと思う。でも、同じスコープで何度も必要ならやる価値はあるよね。でも、「is valid」、「is allowed」、「is secure」を異なるスコープで何度も使う必要があるのかな?それなら関数にするべきだよね。いつもこの三つを一緒に考える必要があるの?それなら一つの関数にまとめるべきだね。condition2やcondition3が許可されない場所ってあるのかな?もっと複雑になるね。こういうシンプルな例でも、現実の世界では複雑になるんだよね。

俺の一時変数、例えば「isValid」や「isSecure」の最も重要なユーザーは、後の自分なんだ。6ヶ月後に新しい機能を追加したり、1週間後に顧客から報告された問題をデバッグしたりすることがあるからね。特に後者の場合、プレッシャーが大きくて時間が限られているから、若い頃の自分が明確にするために余分な時間をかけてくれたことが本当に嬉しいんだ。これが他の人を助けるかもしれないっていうのは、ちょっとしたおまけだね。

認知負荷は有効でも役に立つ概念じゃないよね。https://edtechdev.wordpress.com/2009/11/16/cognitive-load-th... https://www.tandfonline.com/doi/full/10.1080/00131857.2024.2... ここには別の文脈が関わっているんだ:コーダー、コンパイラー、ランタイム、コードを理解しようとしている人(この記事の文脈)、などなど。ある文脈にとって良いことが、別の文脈にとっては良くないかもしれないし、プログラミング言語は特定の文脈を優遇することが多い。今回の場合、プログラミング言語は主にコンパイラーを楽にすることを優先していて、デザインや使いやすさは50年近く改善されていないから、コーダーもリーダーもサードパーティのツールを使って助けてもらうべきだと思う。AIはリーダーがコードを理解するのを助けたり、コーダーがより明確なドキュメントやラベルを生成するのを助けたりすることができるし、リンターやテスト駆動開発、リテラルドキュメンテーションプラクティスなどを使うことも大事だよね。

リンクされた記事は、主に意味的負荷理論について三つのことを批判しているみたいだね; - 測定が難しく、実証的に研究するのが難しい、または不可能。(悪い科学理論) - 教育や学習理論への適用、他の技術がもっと証明されているところ。 - 人間の学習の主要なメカニズムだという考え、これには反対の研究がたくさんある。これらのポイントは妥当だと思うけど、この記事はこの概念について深く掘り下げてない。例えば、「認知負荷」の代わりに「メンタルストレイン」や「限られた短期記憶」を入れても、主張は成り立つと思う。要するに、コードを読む(または書く)ときに考慮すべきことの量を最小限にすべきだって主張してるんだ。これは、CTLの科学的根拠に関係なく、かなり合理的な主張だと思う。だから、君の批判はこの記事にはあまり関係ないと思うけど、指摘することで他の人が使われている言葉の問題について知る助けにはなるかもね。

業界で数十年過ごして、いろんなチームに関わってきたけど、コードの質ってチームメンバーの認知負荷やスキルをうまく伝えられるかどうかに強く関係してると思う。プロジェクトによっては、スキルアップの必要性を指摘するのがタイミング的に難しいこともあって、みんなPRで何でも受け入れちゃうから、質が全然良くならないんだよね。一方で、「これをもっと理解しやすくするためには、...」っていう言い方をするチームもある。こういうチームは、時間が経つにつれてどうなると思う?

「私たちはずっと間違ってやってた」っていう投稿に対して、毎回1ドルもらえたらなぁ。こういう考え方の問題は、ゼロサムゲームじゃないってこと。やってるタスクに認知負荷が全くない状態にはならないし、何らかの負荷は常にある。負荷を減らすために物事を先延ばしにすると、社会保障データベースがS3に載っちゃうんだよね。混乱は複雑さから生まれるもので、認知負荷が高いからじゃない。高い負荷があっても、全体の仕組みが分かっていることもある。これを言い換えるなら、認知負荷が増えると、頭の中で wrestle することが増えてストレスが増すって感じかな。混乱を増やしたり減らしたりはしない(そういう人なら別だけど)、ただ複雑さを増減させるだけ。条件付けによって認知負荷がほとんどない複雑な例は、自動車の運転。逆に、認知負荷がすごく大きいのに複雑じゃないことは、ゴルフだね。

人間が作る大規模ソフトウェアプロジェクトは、常に失敗する運命にある。なぜなら、人間は新しいものを作るのが好きで、古いものを維持するのは誰も好きじゃないから。