フロントエンドエンジニアの安齋です。
名著『リーダブルコード』には、「コードは他の人が最短時間で理解できるように書かなければいけない」という根本的な原則が書かれています。
この本を読んで私なりに解釈したことをいくつかのTipsに分けて書きたいと思います。
名前に情報を詰め込む
コードの読みやすさは、変数や関数の「命名」で8割決まると言っても過言ではありません。
明確な単語を選ぶ
何気なくよく使ってしまうgetですが、場合によっては「何をして取得しているのか」が曖昧です。検索で得た値ならsearch、リクエストを送信した結果ならfetchなど、より適切なワードを常に選ぶ必要があります。
Before
// 何をして取得しているのか曖昧
const data = getUserId();
After
// 検索して取得することが一目でわかる
const userId = searchUserId();
// API等のリクエストで取得することが一目でわかる
const userProfile = fetchUserProfile();
値の単位まで名前に入れ込む
例えば時間を表す変数なら、単にstartTimeとするのではなくstartTimeMsとすれば「ミリ秒(ms)だな」と一目で伝わります。
Before
// 秒なのか、ミリ秒なのかわからない
const timeout = 3000;
After
// ミリ秒(ms)であることが一目でわかる
const timeoutMs = 3000;
よく見るlength
使ってしまいがちですが、明確に文字数であれば、maxLengthよりもmaxCharactersなどのほうが誤解がありません。長過ぎる名前はNGですが「伝わらないより長いほうがマシ」です。不要な単語を削りつつ、誤解されない名前を目指します。
Before
// 文字数(Characters)制限なのか、配列の長さなのか曖昧
const maxLength = 140;
After
// 「最大文字数」であることが誤解なく伝わる
const maxCharacters = 140;
コメントは「コードで表現できないこと」を書く
「コードを見ればわかること」をわざわざコメントに書く必要はありません。
命名をしっかりしていればコメントは減る
「名前に情報を詰め込む」が実践できていれば、説明用のコメントは減らせます。
残すべきは「意図」や「注意点」
TODO:やFIXME:はもちろん、「後日改修するときにハマりそうな罠」を先回りして書いておくのが優しいコメントです。
正確に簡潔に書く
「これ」「ここ」などの曖昧な代名詞は避けます。また、「優先度を変える」ではなく、「◯◯の場合優先度を上げる/下げる」のように、具体的なアクションで記述するのがポイントです。
積極的に書く
あまりに「いらないコメントは書かない」等を意識しすぎると何も書けなくなったりします。書いてないよりは書いてあったほうがマシ、ということも多いので積極的に書くことを意識します。
Before
// カウントを1増やす(見ればわかる)
count++;
// これが20以上ならここの優先度を変える(「これ」「ここ」が曖昧)
if (score >= 20) {
status = 'high';
}
After
// ユーザーのクリック回数を計測
count++;
// プレミアムユーザーの基準(スコア20以上)を満たした場合は、優先度を「高」に上げる
if (score >= 20) {
status = 'high';
}
ループとロジックを単純化する
コードのネストが深くなると、人間の脳のメモリを圧迫します。極力シンプルに保つ工夫が必要です。
条件式の左側は「変化する値」、右側は「比較対象」
if (currentValue === MAX_VALUE)のように配置すると直観的に読めます。また、elseがある場合は、非定型(!)の条件式は避けたほうが認知負荷が下がります。
Before
// 直感的に読みにくく、!(否定)のelseで脳のメモリを使う
if (MAX_COUNT === currentValue) { ... }
if (!isLoggedIn) {
// 未ログインの処理
} else {
// ログイン済みの処理
}
After
// 左側に「変化する値」、右側に「比較対象」で直感的に読める
if (currentValue === MAX_COUNT) { ... }
// elseがあるなら肯定形を優先して認知負荷を下げる
if (isLoggedIn) {
// ログイン済みの処理
} else {
// 未ログインの処理
}
早期リターン(ガード節)
関数はif (count === 0) return;のようなガード節をつかってサクッと速く返します。そうすることでネストが減り単純化します。
Before
// 正常系がネスト(深層)に入り込んでしまう
function login(user) {
if (user) {
if (user.isActive) {
// 実際のログイン処理
console.log("ログイン成功");
}
}
}
After
// ガード節を使ってサクッと先に返すことでネストを減らす
function login(user) {
if (!user) return;
if (!user.isActive) return;
// 正常系をネストなしでスッキリ書ける
console.log("ログイン成功");
}
非同期処理はawait
非同期処理もawaitなどを積極的に使ってネストを浅く保つのが鉄則です。
Before
// .then() のチェインでネストが深くなる
function getUserData() {
fetch('/api/user')
.then((response) => response.json())
.then((user) => {
return fetch(`/api/posts/${user.id}`)
.then((response) => response.json())
.then((posts) => {
console.log(posts);
});
});
}
After
// await を使って上から下にまっすぐ読めるように保つ
async function getUserData() {
const resUser = await fetch('/api/user');
const user = await resUser.json();
const resPosts = await fetch(`/api/posts/${user.id}`);
const posts = await resPosts.json();
console.log(posts);
}
巨大な式は一時変数を使って分割する
ロジックが複雑になりそうなときは、定数をオブジェクトや配列にまとめるだけでも、見違えるほど綺麗に整理できます。
Before
// 条件式が長すぎて一目で理解できない
if (user.age >= 20 && user.hasLicense && !user.isSuspended) {
// 運転可能
}
After
// 一時変数(定数)に代入して、式の意味をラベル化する
const isAdult = user.age >= 20;
const isQualified = user.hasLicense && !user.isSuspended;
if (isAdult && isQualified) {
// 運転可能
}
変数の「寿命」と「変更回数」を減らす
無駄な変数は置かないのが大前提です。
変数が変更する場所(スコープ)が多いと、バグの温床になります。「書き込み回数は少なくとも1回にする(イミュータブルに扱う)」や「変数を置かずに出来るメソッドはないか」を意識することで、コードの追いやすさが劇的に変わります。
Before
// let で何度も再代入され、どこで値が変わったか追いかけにくい
let price = basePrice;
if (isSale) {
price = basePrice * 0.8;
}
if (hasCoupon) {
price = price - 500;
}
After
// const を使い、イミュータブル(不変)に扱うことでバグを防ぐ
const salePrice = isSale ? basePrice * 0.8 : basePrice;
const finalPrice = hasCoupon ? salePrice - 500 : salePrice;
無関係な下位問題を「ユーティリティ」として抽出する
1つの関数に複数のタスクを詰め込まないように意識します。
ユーティリティ関数への分割
コードを書いていると、1つの関数に大量のタスクを詰め込んでしまいがちです。後からでも問題ないので、細かい関数(ユーティリティ関数)に分割して再利用できるようにしましょう。1つのタスクが1つの結果をかえすように分割することで、結果的にテストコードも書きやすくなり、バグの抑制につながります。
Before
// 1つの関数の中で「配列のクレンジング」と「フォーマット」を両方やっている
function displayActiveUserNames(users) {
// アクティブなユーザーだけに絞り込む(下位問題)
const activeUsers = users.filter(user => user.isActive);
// 名前をカンマ区切りの文字列にする
return activeUsers.map(user => user.name).join(', ');
}
After
// 下位問題をユーティリティ関数として切り出す(単体テストも書きやすくなる)
const getActiveUsers = (users) => users.filter(user => user.isActive);
function displayActiveUserNames(users) {
const activeUsers = getActiveUsers(users);
return activeUsers.map(user => user.name).join(', ');
}
コードに思いを込める
物理的に関数を分割するだけでなく、「コードの流れが人間の思考と同じになるように意識する」だけで、読みやすさは格段にアップします。
車輪の再発明はしない
ブラウザの進化は速く便利な標準メソッドは常に追加され続けています。また、外部のライブラリを使うことで、面倒なロジックを書かずともすぐ使える機能も多いです。
「まず書いてみるか」より「なんか便利なものはないか」を意識することでより見やすいコードになります。意外と知られていない便利な標準機能も多いです。
Before
// 配列から重複を消すロジックを自前で頑張ってループを回して書く
const uniqueItems = [];
for (let i = 0; i < items.length; i++) {
if (!uniqueItems.includes(items[i])) {
uniqueItems.push(items[i]);
}
}
After
// 標準の便利な機能(Set)を使えば1行で安全に書ける
const uniqueItems = [...new Set(items)];
まとめ
結局「理解しやすい」とは
「理解しやすい」の定義はやはり人によって多少異なるかなと思っています。私は「誤解されないこと」と「短い(シンプルである)こと」と「丁寧であること」が最大のキーワードだと感じました。
常に意識することは難しいポイントもあるとは思っています。要所要所で自分の書いたコードを振り返ることが、少しでも「理解しやすいコード」へ近づけるための方法なのかなと思います。
Blog