楽しいコーディングのための CUPID - SOLID 原則に対するアンチテーゼ


はじめに

BDD(Behavior-Driven Development) で著名な Dan North により提唱された、CUPID についての概要です。

Thoughtworks Technology Radar Vol.26(2022年3月版) の Techniques 部門にも Assess(採用を検討) として入っています。

元ネタは以下となります。

dannorth.net

少し長いので、ここで概要を紹介します。


SOLID 原則

SOLID 原則は、2000年代に Robert C. Martin により取りまとめられたソフトウェア設計の原則集です。

  • 単一責任の原則 (Single-responsibility principle)
  • 開放閉鎖の原則(Open/closed principle)
  • リスコフの置換原則(Liskov substitution principle)
  • インターフェース分離の原則 (Interface segregation principle)
  • 依存性逆転の原則(Dependency inversion principle)

Dan North は、SOLID原則は、あるコンテキストにのみ特化していたり、誤って適用されやすいものであるため、文脈にとらわれないアドバイスとして提供できるものではないとしています。

例えば、単一責任の原則は、「変更理由が一つのクラスに対して一つ以上あってはならない」となりますが、Dan North はこれを「無意味に漠然とした原則」と呼びます。


CUPID とは

役に立たない SOLID 原則に変わるものは何か?

この問いに対して Dan North が提唱する、優れたソフトウェア設計・実装の特性(properties)が CUPID です。

CUPID の5つの特性は、以下の通りです。

  • Composable(組み立て易い)
    • plays well with others
  • Unix philosophy(Unix哲学)
    • does one thing well
  • Predictable(予測通り)
    • does what you expect
  • Idiomatic(慣用的な)
    • feels natural
  • Domain-based(ドメインに基づく)
    • the solution domain models the problem domain in language and structure

これらは、properties(特性)として提示されています。 SOLID が「原則(principles)」であるのに対し、CUPIDは「特性(properties)」として記述されます。

「特性(properties)」は、従うべきルールではなく、関わることが楽しみに繋がるようなコードの品質や特徴をプロパティとして定義します。 向かうべき目標や中心を定義し、コードの内部そのものを語るのではなく、外から人間の視点で語られるものになります。

ある種のコードは、作業するのが楽しいものであり、この楽しみのための特性と捉えることができます。


Composable(組み立て易い)

固く訳せば「構成可能性」となりますが、組み立て易い・組み合わせて構成し易いといったことになるでしょう。

高凝集 疎結合(High Cohesion and Low Coupling)の言い替えとも読めますし、SOLID原則にも通じるものですが、人間の視点で語られているのがポイントになるでしょう。

Small surface area(小さな表面積)

API が小さければ、簡単に使え、他のコードと衝突や矛盾が発生する可能性が少なくなります。 API が小さすぎると、複数のAPIを一緒に使うことになり、正しい組み合わせを学習するコストが発生します。 適切な粒度の API を定義することは、ユースケースの粒度を定義するのと同じように、見た目以上に難しいものです。 断片的と肥大化の間に「ちょうどいい」まとまりというスイートスポットがあるのです。

Intention-revealing(意図を明確に)

意図が明らかなコードは、発見しやすく、評価しやすいものです。 コンポーネントを簡単に見つけることができ、それが必要なものかどうか素早く判断することができます。 意図的に明確な名前を付けてクラスを書き始めれば、大抵の場合、他の誰かが同じ考えを持っていて、セレンディピティにコードを見つけることが容易になります。

Minimal dependencies(依存は最小限に)

依存関係が最小限のコードは、心配事が少なく、バージョンやライブラリの非互換性の可能性も低くなります。


Unix philosophy(Unix哲学)

KISS原則と捉えても良いでしょう。

A simple, consistent model(シンプルで一貫性のあるモデル)

Unixの技術的魅力は、そのシンプルで一貫した設計思想にあります。 一言で要約すれば「一つのことを、うまくやれ」となり全てのプログラムの出力が、まだ見ぬ別のプログラムの入力になることを期待します。

catコマンドは1つまたは複数のファイルの内容を表示し、grepは与えられたパターンに一致するテキストを選択し、sedはテキストパターンを置き換える。これらをパイプによりコマンドの出力を次のコマンドの入力として接続し、選択、変換、フィルタリング、ソートなどのパイプラインを作成することができます。このパイプは、選択、変換、フィルタリング、ソートなどのパイプラインを形成するものである。

Single purpose vs single responsibility(単一目的 vs 単一責任)

Unix philosophy は、単一責任の原則(Single-responsibility principle)と同じように見えるし、ある種の解釈には重なる部分もあります。

しかし、「一つのことをうまくやる」というのはアウトサイド・インの視点であり、具体的で明確に定義された包括的な目的を持つという性質です。 SRPは、インサイド・アウトの視点であり、コードの組織化に関するものです。


Predictable(予測通り)

驚き最小の原則(Principle of least astonishment / Rule of least surprise) と捉えてもいいでしょう。

コードは、見た目通りのことを、一貫して、確実に、不愉快な驚きなしに実行する必要があります。 また、それを確認することは可能であるばかりでなく、容易でなければなりません。

Behaves as expected(期待通りに動作する)

私はかつて、複雑なアルゴリズム取引アプリケーションに携わったことがありますが、そのアプリケーションの「テストカバレッジ」は7%程度でした。 これらのテストは均等に配置されていませんでした。 コードの多くには自動化されたテストがまったくなく、あるコードには非常に多くの高度なテストがあり、微妙なバグやエッジケースをチェックしていました。 なぜなら、それぞれのコンポーネントが1つのことを行い、その動作は単純で予測可能であるため、大抵の場合、変更は自明だからです。

Deterministic(決定論的)

ソフトウェアは常に同じことをするべきで、予測可能であるべきです。 予測可能なコードは、期待通りに動作し、決定論的で観察可能であるべきです。

決定論的コードは以下の特徴を持ちます。

  • Robustness(堅牢性):カバーする状況の幅や完全性のことで、限界やエッジケースは明らかであるべき
  • Reliability(信頼性):カバーする状況において期待通りに動作することで、毎回同じ結果が得られる
  • Resilience(回復力):カバーできない状況、つまり入力や動作環境における予期せぬ摂動にどれだけうまく対処できるかということ

Observable(観察可能)

複数のコンポーネントが相互作用すると、特に非同期では、すぐに創発的な振る舞いと非線形な結果が発生します。 すなわち、コードは、制御理論的(control theory sense)な意味で観測可能でなければなりません。 これは、設計時にのみ可能で、コードを最初からインストルメント化することは、その実行時の特性を理解するための貴重なデータを得ることを意味します。


Idiomatic(慣用的な)

「人間が理解できるコード」を書くということは、誰かのためにコードを書くということです。これがイディオムコードの意味です。 慣れないコードを扱う上で、余計な認知的負担を増やすことは避ける必要があります。

Language idioms(言語イディオム)

コードは、その言語のイディオムに従わなければなりません。 イディオムに従わない場合、認知的負荷を与え、目の前の問題について考える能力に影響を与え、不確実性を増大させ、喜びを減少させます。

コードイディオムは、関数、型、パラメータ、モジュールの命名、コードのレイアウト、モジュールの構造、ツールの選択、依存関係の選択、依存関係の管理方法など、あらゆる粒度のレベルで発生します。 テクノロジースタックがどのような意見を持っていようとも、その言語のイディオム、エコシステム、コミュニティ、好みのスタイルなどを時間をかけて学べば、あなたの書くコードはより共感されやすく楽しいものになるはずです。

Local idioms(ローカルなイディオム)

慣用句のスタイルについてコンセンサスがない、あるいはいくつかの選択肢がある言語の場合、「良い」とはどのようなものかを決定し、一貫性を保つための制約やガイドラインを導入するのは、あなたとあなたのチームにかかっています。

IDEにおけるコードフォーマットのルールの共有、リントツール、標準ツールチェーンの合意など、単純なもので構いません。 ADR(Architecture Decision Records:ADR)は、スタイルやイディオムに関するあなたの選択を文書化する素晴らしい方法です。


Domain-based(ドメインに基づく)

私たちは、あるニーズを満たすためにソフトウェアを書きます。 それは、特定の状況に対応するためかもしれませんし、一般的で広範囲に及ぶものかもしれません。 どのような目的であれ、コードは、あなたが書いたものとそれが実行することの間の認知的距離を最小にするために、問題領域の言語でそれが何をしているかを伝える必要があります。これは、「正しい言葉を使う」以上のことです。

Domain-based language(ドメインに基づく言語)

基本的な型は整数、文字、ブール値で構成されている。誰かの姓を文字列[30]として宣言することができ、それがそのまま保存されるかもしれませんが、Surname型を定義することで、より意図が明らかになることでしょう。姓に関連する操作やプロパティ、あるいは制約を持つこともできます。金融ソフトのプログラマーは、通貨と金額を組み合わせた複合型であるMoney型を定義します。

型や操作の名前をうまくつけることは、単にバグを発見したり防止したりするだけでなく、コードの中で解決策を明確にし、ナビゲートすることを容易にすることでもあります。

Domain-based structure(ドメインに基づく構成)

ドメインベースの言語を使用することは重要ですが、コードをどのように構造化するかも同様に重要です。

多くのフレームワークでは、ディレクトリレイアウトとスタブファイルからなる「スケルトンプロジェクト」が提供されており、すぐに始められるように設計されています。これは、あなたが解決しようとしている問題とは全く関係のない、先験的な構造をコードに押し付けるものです。

その代わりに、ディレクトリ名、子フォルダと兄弟フォルダの関係、関連ファイルのグループ化と命名など、コードのレイアウトは、問題領域をできるだけ忠実に反映させる必要があります。

例えば Rails などのフレームワークでは、assets channels controllers mailers javascript models views といったプロジェクトレイアウトを提供します。これは認知負荷を高め、結束力を弱め、製品の変更を行う際の労力を増やします。先に述べたように、このイデオロギー的な制約は、仕事を難しくし、コードベースを楽しくなくしてしまうことがあります。

そうではなくて、例えば患者記録管理のアプリケーションを考えた場合、コードベースのトップレベルでは、病院管理の主なユースケースを示すべきです。例えば、patient_historyappointmentsstaffingcompliance