valid,invalid

関心を持てる事柄について

状態、結合、複雑性、コード量の順に最適化する

There’s No Such Thing as Clean CodeHacker Newsコメント経由でコードやシステム設計・最適化についての良いコメントを見つけた。どうやらHacker Newsで何度も引用されているらしいが日本語で言及された記事が見つからなかったので取り上げてみる。

コメントは2016年のSandi MetzThe Wrong Abstractionに関するもので、発言者のcurun1rいわく「私は設計の優先順位をこの順序で学習することで、優れた開発者になれた」。*1

4つの基準と優先順位のガイドライン

状態 > 結合 > 複雑性 > コード量

私は状態 (state)、結合 (coupling)、複雑性 (complexity)、コード量 (code) の順に削減することでコードを最適化する

  • コードがよりステートレスになるなら、結合を増やすこともいとわない
  • 結合を減らすためには、コードをもっと複雑にすることもある
  • コードの複雑さが軽減されるなら、コードをコピーする
  • コードの重複排除をするのは状態・結合・複雑性を増さない時のみに限る

ステートレスを最優先する理由

ステートレスなコードを最優先する理由は、それが最も推論可能なものだからだ。

  • ステートレスロジックは、通常のコード実行・並列処理・分散処理のいずれでも同じように機能する
  • 状態を再現するためのセットアップコードがほとんど必要ないので最もテストしやすい
  • コピーを実行するだけなので、スケーラビリティも高い

状態が入り込むとプログラムは...かなり難しくなる。

Once you introduce state, your life gets significantly harder.

初心者のプログラマはコード量にのみ注目する

初心者のプログラマがコード量を削減するのは、4つの基準の中で最も見つけやすいからだと思う。

他の3つはコード量に比べてはるかに微妙で主観的であるため、発見するにはより多くの経験が必要になる。*2


このコメントに対するツッコミと、curun1rによる回答。

「ステートレスかどうかと結合度は直行する概念では?」

対立するシーンはいくつもあり、小さいプログラムよりもシステムアーキテクチャでよく見られる。たとえばシステムコンポーネントがジョブキューをインメモリで管理する場合、その状態を別のキュー管理コンポーネントに任せる(状態--; 結合++)ほうがうまく状態を管理でき、明晰になる。

「複雑性は結合が生み出すものでは?」

複雑性にはたくさんの種類があり、結合はその一形態に過ぎない。たとえば循環的複雑度も1つの複雑性を示す指標である。複数の複雑性の組み合わせこそが私の考える複雑性であり、プログラマの認知負荷を高めるものだ。


元のコメントで述べられてるコード最適化ガイドラインについてはここまで。以下は感想。

「状態 > 結合 > 複雑性 > コード量」の優れている点

様々なシーンに適用できるエレガントなガイドラインだと感じたので、優れていると感じた点を述べてみる。

基準の普遍性

古今東西様々なプログラミング原則が存在するが、"state > coupling > complexity > code" のガイドラインはシンプルでわかりやすく、パラダイムによらない普遍性があると感じる。

重なるところの大きいCode Smellを端的に言い表しているようにも見える。

順序付き

特定の原則を遵守するときにトレードオフが発生することがあるが、優先度が付けられているので「要はバランス」とならずに判断しやすい

もちろん画一的な判断はできないがコンフリクトした際の指標があるというのが良い。

(最近はあまり聞かないかもしれないが)DRY原則を持ち出してコード量を減らすパッチを受け取ったときに「変更によって状態・結合・複雑性が増しているから受け入れない」と言うことができる。

適用範囲の広さ

ソースコードだけでなくシステムアーキテクチャについても同様の最適化が可能であるという一貫性も良い。マイクロサービスアーキテクチャにSOLIDの一部は適用できる、といった半端さがない。

たとえば、HTTPやイベントソーシング*3が状態を削減しつつ結合や複雑性を増やしていることも、同じ基準によって評価できる。

覚えやすさ

そして、何より覚えやすい。4という数が覚えるのにちょうどよい。

欲を言えば、より端的に表す名前が欲しい。acronymでSCCCとかS3Cみたいな名前が付けられそう*4

関連する言説

思いついたものを2つ。

同主張への関連を見出だせる設計原則やソフトウェアに対する考察は無数にありそう。

Out of the Tar Pit

複雑性といえば、Rich HickeyClojureを作る時に影響を受けたという"Out of the Tar Pit"。同論文では複雑性について以下の主張をする。

  • 複雑であるとは、推論が効きにくいか、不可能であること
  • プログラムの複雑性は、状態・制御・コード量・言語のパワフルさに由来する
  • 複雑性はコントロールできる(とりわけ、"偶発的な複雑性"を指して)

ここでは複雑性がより広い概念として扱われ、状態 (state)・コード量 (code) は複雑性を生む要因の1つに含まれている。

ただし、状態は「コンピューターシステムにおいて絶対になくならない"本質的な複雑性"」とラベリングされているので、コントロールの難易度・アプローチが異なる"偶発的な複雑性"とは切り分けて扱うのが妥当に思う。

「複雑性にはたくさんの種類がある」という補足を先述したが、4つの基準が原則たるためには複雑性という言葉に明晰な定義が必要かもしれない

Clean Architecture

クリーンアーキテクチャ』本における「プログラミングパラダイムは制約を課すものであり、何をすべきでないかを伝えている」という一節を思い出した。

パラダイムの制約が促すプログラム設計により、4つの基準での最適化が推進されるのではないだろうか、と。具体的には以下のようになる。

構造化プログラミングは、直接的な制御の移行に規律を課すものである

モジュール分割によって、結合・複雑性が削減される。

オブジェクト指向プログラミングは、間接的な制御の移行に規律を課すものである

コードの依存関係の制御によって、結合が削減される。

関数型プログラミングは、代入に規律を課すものである

タスクの関数化によって、状態が削減される。

最近の業務であった設計

卑近な例との関連。

これまで1つの責務しかなかったモデルに新たな役割(ユースケース)が追加されそうになる、というありがちなシーンに同僚が直面していた。一緒に設計の相談をした末に2つの実装パターンに思い至ったので、どちらが良いかを評価するために両方のパターンのコードを同僚に書いてもらった。

  1. 同モデルで複数のユースケースの振る舞いを扱えるようにする。変更にかかるコード量は最も少なくても済むが、同一モデル内で気にすべき状態が増える。*5
  2. ユースケースごとに対応するモデルを追加し、元のモデルは抽象にする。変更するコード量はかなり多いが各モデルで気にする状態は少なくなる。

最終的には2を選び、「コード量と複雑性は高いけれども状態と結合は少なくてすむことを優先した」と説明できるようなプログラムになった。

この選択が正解かどうかは時の洗礼を受ける必要があるが、試し書きしたコードをレビューする時点で認知負荷の低さを感じたし、テストもユースケースごとに分割できていて見通しが良かった。

AI Programmer

オフトピック。

最近tabnineCopilotを使っていてコード補完や生成におけるすさまじい技術の進歩を感じるが、この4つの基準で最適化されたコードがsuggestされると面白そう。

現時点でもコードの重複の削減は簡単に提案されるし、状態・結合・複雑性の削減も、循環的複雑度の高さやローカル変数の個数など特定の指標ごとに指摘するツールは多くあるが、さらに一歩先で「インスタンス変数への依存をなくし関数にしませんか」「このクラスは2つの状態に依存していますが取り除くことができます」的な。

まぁ、機械が書いたコードを機械が読むような、真にプログラマが不要な世界に到達したら人間にとってのコードの認知負荷は問題ではなくなるのだが。

*1:優れた開発者を自称できるのすごい

*2:プロとアマチュアプログラマはコードベースを成長させる際の優先順位を確認することでわかる、という別のユーザーからのコメントもあった。

*3:状態ではなく取引(トランザクション)を保存し、状態はトランザクションから導出するという戦略

*4:そして順番を間違える輩が現れそう

*5:ユースケースが複数ある=アクターが複数いる」と考えればそもそも単一責任原則に反しているのだが、コード量の変更は数十行で済むので一見シンプルな変更に見える魅力があった