ソフトウェア設計

最小驚きの原則:getUserがメールを送ってはいけない理由

コードレビュー中に、こんな関数に出会ったことはありませんか?

読了 4 分
最小驚きの原則:getUserがメールを送ってはいけない理由のカバー画像

コードレビュー中に、こんな関数に出会ったことはありませんか?

名前はgetUser()なのに、中ではユーザーを取得し、最終ログイン時刻を更新し、キャッシュを更新し、さらに休眠アカウントなら通知メールまで送っていました。

名前だけを見て「取得するだけだから安全だろう」とループ内で呼び出していたら、ユーザーにメール爆弾を送ることになりますよね。

今日取り上げるのは、こうしたコードを見抜くための最小驚きの原則です。KISS・DRY・YAGNI・SOLIDへと続いてきた開発原則シリーズですが、今回は名前からして面白い原則です。

最小驚きの原則(Principle of Least Astonishment、略してPOLA)が示すのは、次のことです。

コードは、読む人が予想したとおりに動作すべきです。利用者を驚かせる設計があるなら、その設計を見直しましょう。

1960〜70年代のシステム設計から生まれた古い原則ですが、UIデザインやAPI設計、日常的なコードまで幅広く適用できます。


なぜ驚きがコストになるのか?

プログラミングにおける「驚き」は感情の問題ではなく、コストの問題です。

開発者はコードを読むとき、名前や慣例をもとに仮説を立てます。getUserは取得だけを行い、isValidはBoolを返すだろう、といった具合です。コードが仮説どおりに動けば、読み進めるのは簡単です。

しかし仮説が崩れた瞬間、読む流れを止めて関数の中に入り、実際の動作を確認しなければなりません。こうした関数がコードベースに10個あるだけでも、「このコードでは名前を信用できない」という不信感が生まれます。そこからは、すべての関数を開きながら読むことになります。コードを読む速度は半分になります。

最悪なのは、確認せずに仮説どおり使って障害を起こすことです。先ほどのメール爆弾のように。


コードで驚きが現れる場面

ここでは、私が実際に経験したパターンを中心に整理します。

1. 名前と動作が異なる関数

// 名前は取得なのに、内部で状態を変更する
func getUser(id: Int) -> User {
    let user = db.find(id)
    user.lastSeenAt = Date()  // 驚き 1: 副作用
    db.save(user)
    if user.isDormant {
        mailer.send(to: user.email, message: "休眠解除のお知らせ")  // 驚き 2: 外部呼び出し
    }
    return user
}

取得関数に隠れた書き込み処理は、驚きの代表例です。getは読み取りだけにし、状態を変更するならupdateやtouchのような名前を使うべきです。

2. 慣例を裏切る戻り値

同じコードベースで、見つからないとnilを返す関数、例外を投げる関数、空のオブジェクトを返す関数が混在していると、呼び出し側は毎回賭けをすることになります。どれか一つに統一することが重要です。

3. 何も知らせない失敗

func parseConfig(_ json: Data) -> Config {
    guard let config = try? JSONDecoder().decode(Config.self, from: json) else {
        return Config()  // 驚き:設定が壊れているのに、何事もなかったかのように空の設定を返す
    }
    return config
}

設定ファイルが壊れているのに、黙ってデフォルト値に戻ると、ユーザーはずっと後になって「なぜ設定が反映されないのか」と苦しむことになります。失敗は大きく知らせたほうが、驚きは少なくなります。

名前と動作が違うと、読む人が苦労します
名前と動作が違うと、読む人が苦労します

驚きの少ないコードを書くコツ

私が守るようにしているのは、次のとおりです。

コツ 内容
名前は契約である 名前が約束したことだけを行い、それ以上を行うなら名前を変える
慣例に従う 理由なく、言語・フレームワーク・チームの既存スタイルから外れない
副作用を明示する 状態を変更する関数だと名前から分かるようにする
驚かせるなら文書化する どうしても特殊な動作が必要なら、コメントとドキュメントで目立つように知らせる

この中で最も強力なのは2つ目です。自分だけの賢いやり方より、退屈な標準方式のほうが、チーム全体で見ると常に勝ちます。前回のKISS編で、「目立つ一行」より普通のループのほうがよいと述べたのと、まったく同じ文脈です。

退屈で予測可能なコードが、結局は勝ちます
退屈で予測可能なコードが、結局は勝ちます

最後に

最小驚きの原則について、覚えておきたいことは3つです。

  • コードは、名前と慣例が作る期待どおりに動作すべきです。期待を裏切るコードは、バグの温床です。
  • 取得関数に副作用を隠さず、失敗を黙って握りつぶさないでください。
  • チームでは、奇抜なコードより退屈で予測可能なコードが勝ちます。

良いコードの基準を一つだけ挙げるなら、私はこれを挙げます。読む人を驚かせないコードです。

最近コードを読んでいて「えっ、なぜこんな動きをするの?」と思った瞬間があったなら、それこそ、この原則が破られた現場です。