手紙のようにコードを書く
社内のエンジニア研修や1on1で、「手紙のようにコードを書きましょう」というフィードバックをすることが割とあるので、整理のためにまとめてみます。
新人のエンジニアの方やエンジニア教育にあたる方の一助になれば幸いです。
「手紙のようにコードを書く」とは
手紙のように書く、というのは周りの人が読んでいて自然と頭に入ってくるコードを書く、という意図をこめており、以下の3点が重要だと考えています。
- 自然言語(英語や日本語)のようにコードを読めること
- やりたいこと(要件)が伝わってくること
- 読み手の気持ちを考えながらメッセージを伝えること
逆にこれが満たされていないと以下のような問題が発生してきます。
- 1行1行丁寧に読み込まないとやりたいこと(要件)が見えてこない
- コードを読みながら大量の疑問が浮かんでくる
個人開発であればこのようなことが起きていても全て自分の頭の中に入っているので問題になりづらいですが、特にチーム開発で他人のコードを読む時間が多くなるので、これらのことを意識するのが大事になってきます。
「手紙のようにコードを書く」ために大事なプロセス
ではどうすれば「自然言語のようにわかりやすく」「要件が伝わり」「読み手が納得できる」コードを書けるのでしょうか。
まず重要なのは、コーディングする前に要件を日本語で整理することです。
要件をブレイクダウンしていくとツリー上に整理できるはず。
例えば要件を3ステップぐらいに分けたとします。さらにそれぞれのステップを3ステップに分けたとすると、3x3の要件ツリーができますね。
このツリー上の要件を関数として切り分けながら書き下していくことで、要件が伝わるようになるし、関数の命名が悪くなければ自然言語のように読みやすいコードになるはずです。
逆に、そもそも要件を整理できていない、もしくは要件を書き出していてもツリー上に分解できておらず抽象度が異なる要件がフラットに並んでしまっているようなときは、コードを書いていても雑然としたコードになってしまいますし、当然結果として要件も伝わってきません。
要件をロジカルに整理できてないままコードを書いてしまい、なんとなく動いたからPRを提出したら先輩エンジニアに怒られてしまった、みたいな経験がある人もいるのではないでしょうか。
要件をブレイクダウンしてツリー上にする作業の段階でつまったらこの時点で周りの人に相談しましょうね。
具体例で考えてみる
説明だけだとわかりづらいと思うので具体例を一緒に考えてみましょう。
「ユーザーログインAPIの作成」というタスクを例に挙げてみます。
チケットの要件整理
まずはタスクの要件を整理していきましょう。
例えばツリー上に分解すると以下のようになるかと思います。
- メールアドレスとパスワードが正しいことを確認する
- 入力されたメールアドレスが存在することを確認する
- データソース(DBなど)と照合
- アカウントが凍結されていないかチェック
- 凍結されていないかデータソース(DBなど)と照合
- パスワードが正しいことを確認する
- パスワードを暗号化
- 暗号化したパスワードを使ってDBなどのデータソースと照合
- 入力されたメールアドレスが存在することを確認する
- レスポンスを返す
- 正しい場合、ユーザーにセッションを発行する
- トークンを発行
- 200ステータスでトークンを返す
- 間違っている場合、エラーを返す
- …省略
- 正しい場合、ユーザーにセッションを発行する
悪い例
まずは要件がぱっと伝わってこない例を見てみましょう。(実装はダミーです。)
function authenticateUserAPI(email, password) {
// ダミーのユーザーリスト
const users = ["user@example.com", "test@example.com"];
let userExists = false;
for (const user of users) {
if (user === email) {
userExists = true;
break;
}
}
if (!userExists) {
console.error("User does not exist");
}
// このあとも様々な逐次処理がつらつらと記述されていく...
}
全体のコードは大変なので割愛しますが、逐次的に処理が何十行にも並んでいくイメージです。
このような処理は1行ずつ読んでいかないと頭に入っていかず、保守性も低いです。
良い例
反対にいい例を見てみましょう。
要件をコードに起こすと例えば以下のようになります。(実装はダミーです。)
function authenticateUserAPI(email, password) {
const user = _authenticateUser(email, password)
const session = _createUserSession(user);
return session
}
function _authenticateUser(email) {
const user = _findUserByEmail(email);
if (user == null) {
_raiseError("User does not exist.");
}
if (!_verifyPassword(password, user.password)) {
_raiseError("Incorrect password.");
}
return user;
}
function _findUserByEmail(email) {
...
}
function _verifyPassword(rawPassword, encryptedPassword) {
...
}
function _encryptPassword(password) {
...
}
function _createUserSession(email) {
...
}
function _raiseError(message) {
...
}
コードを読んでみると、ほとんど要件で書かれたことがそのままfunctionの定義、呼び出しとして記述されていることがわかります。
このようにするとfunction名や引数を含めたシグネチャを見るだけで大体要件を満たしたような設計となっているかが一目でわかり、コードレビューも捗るし将来的なコード改変も取り組みやすくなります。
この際関数や引数の命名がわかりづらいと読みづらくなってしまい台無しなので注意しましょう。
セルフレビューで読み手の気持ちを考える
コードが書けたらセルフレビューをして、要件がそのまま伝わるような構造にできたか客観的に見直してみましょう。
また、実装の都合上敢えて変則的なコードを書いているのであればコメントするなりして読み手の疑問を先回りして伝えてあげましょう。
まとめ
以上のように要件を正しくツリー上に分解し、正しい命名を行い、セルフレビューで読み手の疑問を潰せれば手に取るように読みやすいコードになるはずです。
ぜひ意識してみてください。
補足: SLAP(抽象度統一の原則)
SLAP(Single Level of Abstraction Principle、抽象度統一の原則)と呼ばれる原則もあるみたいで、これと言いたいことは近い気もします。
要件ツリーを組み立てていくときに階層ごとに抽象度をそろえるようにすると、これをコードに落とせば自然にSLAPにを満たせるはずです。
SLAPについてはここでは解説しませんので、気になる方は調べてみてください。