self-documenting-code について
関数とデータの設計を通じて、意図が伝わりテストや保守がしやすいコードを目指すコーディングスキルです。再利用可能なセマンティック関数と、複雑な処理を担うプラグマティック関数を区別します。
主な機能
- 01入出力が明確なセマンティック関数
- 02複雑な処理をまとめるプラグマティック関数
- 03不正な状態を防ぐモデル設計
- 04ドメイン識別子を分けるブランド型
- 05関数とモデルの境界に関する指針
活用シーン
- 01複雑な処理を名前付きの手順に分割
- 02一貫性のあるフィールドを持つモデルの設計
- 03識別子の誤った取り違えの防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ theswerd/ aicode --skill self-documenting-codeインストール後、Claude Code で/self-documenting-codeを入力して読み込まれたか確認します。
詳細
コードは自己説明的であるべきです
ロジックを関数に分割し、関数間で受け渡すデータをどのような形にするかによって、コードベースが長期にわたってどれだけ維持しやすいかが決まります。
セマンティック関数
セマンティック関数はあらゆるコードベースの構成要素です。優れたセマンティック関数は、正確性を優先できるよう、可能な限り最小限にするべきです。セマンティック関数は、目的を達成するために必要なすべての入力を受け取り、必要な出力をすべて直接返すべきです。セマンティック関数は、望ましいフローや使用方法を表すために、ほかのセマンティック関数をラップできます。コードベースの構成要素として、どこでも使われる複雑なフローが明確に定義されている場合は、セマンティック関数を使ってそれをコード化してください。
副作用は、明示的な目的でない限り、セマンティック関数では一般に望ましくありません。セマンティック関数は、内部の仕組みを理解しなくても、説明されているとおりの動作をするものとして安全に再利用できるべきだからです。ロジックが複雑で、大きなフローの中で何をするのか明確でない場合は、そのフローを、一連の自己説明的なセマンティック関数に分割するのがよい方法です。それぞれが必要な情報を受け取り、次のステップに必要なデータを返し、それ以外のことは何もしないようにします。優れたセマンティック関数の例としては、quadratic_formula()からretry_with_exponential_backoff_and_run_y_in_between<Y: func, X: Func>(x: X, y: Y)まで幅広くあります。これらの関数が二度と使われないとしても、コードを確認する未来の人間やエージェントは、情報が整理されていることに感謝するでしょう。
セマンティック関数の周囲にコメントは不要です。コード自体が、その関数の動作を説明する定義になっているべきです。優れたセマンティック関数は明確に定義されているため、理想的には単体テストを非常に簡単に書けるはずです。
実用関数
実用関数は、一連のセマンティック関数と固有のロジックをラップするために使用します。これらはコードベースの複雑な処理です。本番システムを作る際にロジックが複雑になるのは自然なことであり、実用関数はその整理に役立ちます。一般に、実用関数はごく限られた箇所でのみ使うべきです。複数の箇所で使われている場合は、明示的なロジックを分解し、セマンティック関数に移すことを検討してください。たとえば、provision_new_workspace_for_github_repo(repo, user)やhandle_user_signup_webhook()などです。実用関数のテストは統合テストの領域に属し、多くの場合、アプリ全体の機能をテストする中で行われます。実用関数は、内部の処理から機能そのものまで、時間の経過とともに完全に変わることが想定されています。そのため、関数の上にドキュメントコメントを付けるとよいでしょう。関数名や明らかな特性を繰り返すのは避け、たとえば「残高が10未満の場合は早期に失敗する」といった予想外の挙動や、関数名から生じるほかの誤解を解消する情報を記載してください。ドキュメントコメントを読む際は、内容を鵜呑みにしないでください。関数内で作業するコーダーがコメントの更新を忘れている可能性があるため、誤りかもしれないと思った場合は、事実確認をするとよいでしょう。
モデル データの形は、誤った状態が起こり得ないようにするべきです。実際には決して同時に存在しないはずのフィールドの組み合わせをモデルが許しているなら、そのモデルは役割を果たしていません。オプショナルなフィールドがあるたびに、コードベースのほかの部分はそのデータに触れるたびに答えを出さなければならない問いが増えます。また、型付けが緩いフィールドは、一見正しそうでも実際には違う値を呼び出し側が渡すことを招きます。モデルが正しさを保証すれば、前提が崩れる無関係な処理の深いところではなく、構築時点でバグが表面化します。モデルの名前は、どのフィールドを見てもそれが含まれるべきかどうか判断できるほど正確であるべきです。名前から判断できないなら、そのモデルはあまりに多くの役割を担おうとしています。よく一緒に必要とされるものの、独立した概念である場合は、統合するのではなく組み合わせてください。たとえば、UserAndWorkspace { user: User, workspace: Workspace } とすれば、ワークスペースのフィールドをユーザーに平坦化せず、両方のモデルをそのまま保てます。UnverifiedEmail、PendingInvite、BillingAddress のような適切な名前なら、どのフィールドが属するのかが明確です。BillingAddress に phone_number フィールドがあれば、何か問題が起きたとわかります。
形が同じ値でも、まったく異なるドメイン概念を表すことがあります。{ id: "123" } は、ある場所では DocumentReference、別の場所では MessagePointer かもしれません。関数が単に { id: String } を受け取るなら、どちらを渡してもエラーになりません。ブランド型は、プリミティブ値を別個の型でラップすることでこの問題を解決し、コンパイラーがそれらを別の型として扱うようにします。たとえば、裸の UUID ではなく DocumentId(UUID) を使います。ブランド型を使えば、2つの ID を誤って入れ替えた場合、数階層下で表面化する見えにくいバグではなく、構文エラーになります。
問題が起きる場所
問題は、意味に基づく関数が利便性のために実用優先の関数へと変わり、それに依存するコードベース内のほかの箇所が意図しない処理をするようになることでよく起こります。これを防ぐには、関数を作成するとき、その関数が何をするかではなく、どこで使われるかに基づいて明示的に命名してください。名前から、関数の振る舞いが厳密に定義されておらず、内部処理で特定のタスクを正確に実行することを当てにすべきではないと、ほかのプログラマーにも伝わるようにします。そうすれば、その関数によるデグレードのデバッグも容易になります。
モデルも同じように、ただしよりゆっくりと崩れていきます。最初は用途が絞られていても、新しいモデルを作るより簡単だからと、誰かが「もう一つだけ」オプショナルなフィールドを追加します。さらに別の誰かが同じことをし、最終的には、互いにほとんど関係のないデータが寄せ集められたモデルになります。利用側は、どのフィールドに実際に値が設定されているのか、なぜ設定されているのかを推測しなければなりません。名前はデータの内容を表さなくなり、フィールドは単一の概念を中心にまとまらなくなります。そして、モデルを使う新機能を追加するたびに、そのモデルが表現するよう設計されていなかった状態への対処が必要になります。モデルのフィールドがその名前に沿ってまとまらなくなったら、モデルが結び付けている個別の概念に分割するべきという合図です。