読みやすいコードは「読ませない」

2024/10/11に公開
1

経験の浅い人にちょくちょくするアドバイスとして、「コードリーディングのときにはあんまコードを読まないほうがいいよ」がある。コード全体を詳細に読むのではなく、名前やインターフェイスからコードの意図を把握することで効率的にコードリーディングできる。完全に下記の受け売り。

「実装は極力見ないようにして、インターフェイスと構造を理解するようにするんです。ダイヤグラムや、関係のグラフを書いたりして。実装はちゃんと出来ていると信じて、読んでいるメソッドやクラスのインターフェイスの役割やパラメータをしっかり理解するようにするんです。そっちの方が、実装を見るよりずっと楽ですよね。」

牛尾 剛「コードリーディングのコツは極力読まないこと

自分なんかは、エディタの畳み込み機能と変数名ホバーを使って、名前とインターフェイスしか見えない状態で読む。中身を読みたいなーと思ったところは畳み込みを解除して徐々に読んでいく。ちゃんと書かれてたらそれで大体理解できる。

逆に言うと、名前とインターフェイスを見てピンとこないものは中身の実装を読まなきゃいけない。言ってしまえば「読まされる」感じ。読まされるときは、「読みにくいなー」とか「ここ難しいとこなんやなー」とか「もうちょい別の書き方あるんじゃないのー」とか思いながら読んでる。

何が言いたいかというと、これはコードを書くときにも使える知識なのではないか、ということ。「中身を読まなくていい」が読みやすいコードを書けてるかどうかの指針の1つになるのではないだろうか。

つまり、読みやすいコードは読み手に極力「読ませない」コードなのではないか。

余計にコードを読ませないために気をつけるポイント

読み手に「読ませない」コードを書けるようになれば、読みやすいコードが書けることになる。そうだとして、「読ませない」コードを書くためのポイントを考えてみる。

名前

まず、名前の威力は大きい。適切な名前がつけられていれば、その名前と「=」で結ばれた先にあるn行のコードの内容が一撃で掌握できることになる。「読ませない」ために重要なポイントだ。

逆に言うと、名前を見ても中身が伝わらなかったり、「なんかズレてそうな命名だな」と思ったりしたら、そこは「臭う」。設計についてレビューすべきところは大体そういうところだ。

これはつまりこういうことなのではないかと思います。適切な名前をつけられると言うことは、その機能が正しく理解されて、設計されているということで、逆にふさわしい名前がつけられないということは、その機能が果たすべき役割を設計者自身も十分理解できていないということなのではないでしょうか。個人的には適切な名前をつけることができた機能については、その設計の8割が完成したと考えても言い過ぎでないことが多いように思います。

Matz「名前重要

命名の重要性については下記が詳しくておすすめ。

https://r-west.hatenablog.com/entry/20090510/1241962864

「正しい名前」とは、それが何なのかを、そのスコープにおいて的確に表現している名前のこと。「それは何?」を一言で言うには、その名前以外に言い様がないような名前。

インターフェイス

関数のインターフェイスは名前とセットで見ている。インターフェイスが名前と噛み合ってたら書き手を信頼して中身を追わない。例えばuploadFile関数のインターフェイスが (file: File) ⇒ { url: string } だったら、「あー、ファイルをアップロードしてそのURLが返ってくるんだろうなー」として一旦は中身を読むのをスキップする。

また、処理の流れもインターフェイス(というか、なんのデータが入ってなんのデータが出てくるか)でイメージすることが多い。

例えば下記のような処理があったとしたら、

  1. idから従業員の詳細情報を取得
  2. 従業員情報からプロフィール情報作成
  3. プロフィールからメッセージ生成

「大体こんな感じかなー」と考える

  1. (id: string) ⇒ Employee
  2. (employee: Employee) ⇒ Profile
  3. (profile: Profile) ⇒ string

この感じで眺めて、関数の区切り・組み立てが納得の行く感じだったら心の中で書き手と握手する。そうじゃなかったら、書き手とのさらなる対話を求めて中身を読みに行く(?)。

テストケース

「こういう場合にどう振る舞うの」が知りたいときに、テストが書いてあると中身を読まなくて済むことがある。

例えば、isXXX(yyy) みたいな関数で「こういう場合にtrue、こういう場合にfalse」のテストが書かれていてそれが通っていれば、信頼して中身を読まなくて済む。中身の判定ロジックが込み入っている場合には特に威力が大きい。

まあでも、そもそもisXXXの命名とインターフェイスがしっくりきていればもう中身は読まないかも。

コメント

どうしても認知負荷が高いコードを書くときに、自然言語で振る舞いを説明して読み手をアシストすることがある。処理の概要を把握するのに役立つ。

ただ、コメントだけを信頼して読み飛ばすということはあまりない。名前とインターフェイスがしっかりしていてこそ、「ちゃんと設計されているな」と安心して詳細をスキップすることができる。

コメントは、「コード外にある情報を探しに行く」のをスキップするために書けるといいと思う。文脈を伝える資料のURLとか、「MEMO: ここイマイチな気がする…」みたいな書き手の気持ちとか。名前とインターフェイスがしっかりしているうえで、それだけでは伝わりきらない情報を書く。

逆に、コードから自明なことは余計なので極力書かない。自然言語のコメントは読みやすいだけについつい「読ませちゃう」ところがあるので気をつける。

読ませないコードを書くための術

「読ませないコードを書くためのより具体的なノウハウを教えて!」と言われたら、とりあえず次の3つを紹介する。

処理の流れを自然言語で書く

いきなりコードを書き始めない。まずは自然言語で処理の流れを説明してみる。こうすると、「どう動くか」ではなくて「何が起こるか」という観点で整理できる。一行で説明できるくらいの意味のまとまりで処理を考えられる。

// Employee IDをもとに詳細情報をサーバー取得

// バリデーション

// EmployeeからProfileオブジェクトの作成

// Profileからメッセージを生成

// メッセージを出力

これでもう、処理の概形ができる。最近はAIの支援も受けられる。

// Employee IDをもとに詳細情報をサーバー取得
const fetchEmployeeResult = await fetchEmployeeById(...)

if(isErr(fetchEmployeeResult)) {
  // エラー処理
}

// バリデーション
const validateEmployeeResult = validateEmployee(fetchEmployeeResult.val)

if(isErr(validateEmployeeResult)) {
  // エラー処理
}

// EmployeeからProfileオブジェクトの作成
const profile = createEmployeeProfile(validateEmployeeResult.val)

// Profileからメッセージを生成
const message = generateMessageFromProfile(profile)

// メッセージを出力
console.log(message)

あとは個別の関数について中身を書いていくだけ(コメントは余計だと思ったら消す)。

TDD

TDDをやると「中身がどう動くか」ではなくて「振る舞いはどんなか」を中心に考えることになるし、いやでも名前・インターフェイス・テストケースに意識が向く。

処理の流れを自然言語で書いて全体観を得たあとに、個別の関数についてTDDで整理していくといいと思う。

TDDについては、自分はt_wadaさんの資料を色々追いかけて学んだ。自分の周りの人も大体そう。

https://t-wada.hatenablog.jp/entry/canon-tdd-by-kent-beck

関数型プログラミングのスタイル

関数型プログラミングのスタイルについては自分も序の口しか学べてないけど、それでも「式の組み立てでプログラムを構成していく」という世界観に触れられたのは良かったなと思う。名前やインターフェイス、意味のまとまりや処理の流れについて考えるきっかけをもらえた。

自分は最初にこれを半分くらい読んでなんとなく雰囲気を掴んだあと、Result型やカリー化、pipeなど比較的取り入れやすいテクニックから学んでいる。

https://kentutorialbook.github.io/functionalprogramming2022/

おわりに

プログラムを書き始めの頃にこれがわかってたら、コードを俯瞰的に読み書きできていくらか楽だったかもしれない。他にもポイントがあれば教えてください。

株式会社ゆめみ

Discussion

もりたもりた

めっちゃ刺さりました!
ありがとうございます!!!