2025.10.31
- 技術情報
- テックブログ
技術投稿│読みやすいコードの作法 — 未来の自分とチームを助ける実践テクニック
はじめに
お久しぶりです。開発グループのGMごーくんです。
皆さんはコードを書くとき、どんな点に気をつけていますか?最近、若手メンバーのコードレビューをする機会が増えたので、「読みやすいコード」についてまとめてみます。
この記事が、日々のコーディングやレビューの参考になれば幸いです。
なぜ読みやすいコードを書く必要があるのか
読みやすいコードは単なる見た目の良さではなく、未来の自分やチームの時間を節約してくれる投資です。
数ヶ月後に自分でコードを見返して「なんでこう書いたっけ?」と悩む時間を減らせますし、他のメンバーが読んだときにも処理の理解が早くなります。
また、読みやすさは感覚だけの話ではありません。識別子の名前付けや空白・インデントといったフォーマット要素、関数の分割などの構造的な要素が、プログラマのソースコード理解に影響することは研究でも示されています。
可読性の高いコードは読解や修正タスクに要する時間が短く、バグを見つける成功率が上がる、目線トラッキング解析では視線が集中しやすく余計な探索が減る――といった報告があります。
ですから、命名規約や自動整形、リンティング、単一責任の徹底といったプラクティスは、単に見た目を整えるための作業ではなく、生産性と品質に直結する有効な手段です。
実務で見た「良くない」コードの問題
実務でよく見かける問題点の例を挙げます。
- Controllerの1つの関数に責務が集中(バリデーション+ビジネスロジック+DBアクセス
- 使われてない変数が残っている
- 変数名と役割が一致していない
- 深いネストと否定条件
- マジックナンバーとハードコード文字列
- 全く同じ処理が複数箇所に点在
さて、これらは具体的にどんな問題を引き起こすのでしょうか。1つずつ見ていきます。
Controllerの1つの関数に責務が集中
Controllerの1つの関数に責務が集中 1つの関数にバリデーション、ビジネスロジック、DBアクセスなどが混ざっていると、その関数の目的が分かりづらくなります。
テストやモックが作りにくく、変更の影響範囲も広くなるため、改修コストが上がります。
コントローラは「受け取って、整えて、サービスを呼ぶ」程度に留め、実際の処理はサービス層やリポジトリに委譲するのが望ましいです。
使われてない変数が残っている
未使用の変数は、コードを読む人に「何か抜けているのでは?」という不安を与えます。
静的解析ツールで検出されることが多いので、警告を放置せず削除するか、意図があるならコメントで理由を書いておきましょう。
変数名と役割が一致していない
$data、$tmp、$result のような曖昧な名前や、実体と名前がずれている変数は理解コストを上げます。
名前は「何を表しているか」「期待する型」を反映するようにしましょう。IDEの型情報や PHPDoc、型宣言を併用するとさらに安全です。
深いネストと否定条件
入れ子や否定条件が多いコードは、どれが主処理かが見えにくく、認知負荷が高くなります。
ガード節(早期リターン)を使ってネストを浅くし、複雑な条件は意味のある名前で変数化するか小さな関数に切り出すと読みやすくなります。
マジックナンバーとハードコード文字列
数値や文字列がコード内に直接埋め込まれていると、意味が不明瞭で変更時のリスクも高くなります。
意味のある定数や設定に切り出し、名前で意図を伝えるようにしましょう。
全く同じ処理が複数箇所に点在
同じ処理がコピペで散らばっていると、仕様変更時に修正漏れが発生しやすくなります。
共通処理はユーティリティやサービスにまとめ、テストを一箇所に集約して管理しましょう。
コード例・解説
さて、この章では1つ前で挙げた「良くない」コードの問題点も踏まえて、具体的なコード例を通じて解説していきます。
単一責任(SRP)違反の関数を分割する
❌悪い例
function processUser(array $input)
{
// バリデーションチェック
if (empty($input['id']) || empty($input['email'])) {
throw new Exception('invalid');
}
// データ変換
$userData = [
'id' => $input['id'],
'email' => strtolower($input['email']),
];
// 保存
$user = new User();
$user->fill($userData);
$user->save();
// メール送信
if (!empty($input['welcome'])) {
(new Mailer())->send($user->email, 'Welcome!');
}
return $user->id;
}
✅改善例
function validate(array $input): void
{
if (empty($input['id'])) {
throw new InvalidArgumentException('id is required');
}
if (empty($input['email'])) {
throw new InvalidArgumentException('email is required');
}
}
function buildUserFromInput(array $input): array
{
return [
'id' => $input['id'],
'email' => strtolower($input['email']),
];
}
function saveUser(array $user): void
{
$user = new User();
$user->fill($userData);
$user->save();
}
function maybeSendWelcomeEmail(array $user, bool $send): void
{
if (!$send) {
return;
}
(new Mailer())->send($user['email'], 'Welcome!');
}
function createUser(array $input): string
{
validate($input);
$user = buildUserFromInput($input);
saveUser($user);
maybeSendWelcomeEmail($user, !empty($input['welcome']));
return $user['id'];
}
悪い例は1つの関数に「入力検証」「データ変換」「保存」「メール送信」といった複数の責務が詰め込まれていて、何をテストすれば良いのか、どこを修正すれば良いのかが分かりにくくなっています。
改善例では、それぞれを小さな関数に切り出すことで責務を明確にしています。こうすると
- 各処理ごとに単体テストを書ける(テストの粒度が上がる)
- 変更時に影響範囲を把握しやすくなる
といったメリットが得られます。コントローラは「入力を受け取り、整え、サービスを呼ぶ」程度に留め、具体的な処理はサービス/リポジトリ/ユーティリティに実装するのが望ましいです。
命名の重要性
❌悪い例
function handle($d) {
// ... たくさんの処理
}
✅改善例
function calculateOrderTotal(array $orderItems, float $taxRate): float
{
// 関数名、変数名から処理内容が明確
$subtotal = array_reduce($orderItems, fn($sum, $it) => $sum + $it['price'] * $it['qty'], 0.0);
return $subtotal * (1 + $taxRate);
}
短く意味のない名前(handle、$d など)は、関数が何をするかを読む側に推測させてしまいます。一方、意味のある関数名・変数名は「意図のドキュメント」になり、レビューやデバッグの時間を短縮します。命名の際は「誰が」「何を」「どんな型で返すか」を意識してください。
IDEや型宣言と合わせると安全性がさらに高まります。命名はリファクタのコストを下げる最も費用対効果の高い投資の一つです。
ネストを浅くする(ガード節)
❌悪い例
function getActiveProfile($user)
{
if ($user) {
if (!$user->isDisabled()) {
if ($user->hasProfile()) {
if ($user->profile->isValid()) {
return $user->profile;
}
}
}
}
return null;
}
✅改善例
function getActiveProfile(?User $user): ?Profile
{
if (is_null($user)) {
return null;
}
if (!$user->hasProfile()) {
return null;
}
if (!$user->profile->isValid()) {
return null;
}
return $user->profile;
}
深い入れ子や否定条件は、主要な処理がどこにあるか分かりにくくし、認知負荷を上げます。
ガード節(早期リターン)を使えば、前提チェックを先に終わらせて主処理をフラットに書けます。読みやすくなるだけでなく、テストもしやすくなり、条件分岐の漏れも検出しやすくなります。
複雑な条件は意味のある名前の変数に置き換えるか、小さな判定関数に切り出すとさらに読みやすくなります。
マジックナンバーと設定の抽出
❌悪い例
function
{
if ($attempts >= 3) {
lockAccount($userId);
}
if ($status === 3) {
throw new Exception('invalid');
}
}
✅改善例
class AuthConfig
{
public const MAX_LOGIN_ATTEMPTS = 3;
public const USER_STATUS_PROVISIONAL = 1;
public const USER_STATUS_ACTIVE = 2;
public const USER_STATUS_INVALID = 3;
}
function processLogin($userId, $attempts, $status)
{
if ($attempts >= AuthConfig::MAX_LOGIN_ATTEMPTS) {
lockAccount($userId);
}
if ($status === AuthConfig::USER_STATUS_INVALID) {
throw new Exception('invalid');
}
}
コード中の生の数値や文字列は「なぜその値か」を伝えられません。定数や設定に置き換えると、値の意味が明確になり、変更があったときに探し回る必要がなくなります。
定数名は読み手に意図が伝わるように付け、環境やプロジェクトで変わりうる値は設定ファイルや環境変数に移すと良いでしょう。こうすることで可読性だけでなく運用の柔軟性も向上します。
重複コード(DRY)の解消とユーティリティの活用
❌悪い例
function registerUser(array $in, UserRepo $repo): int
{
$email = strtolower(trim($in['email'] ?? ''));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('invalid email');
}
// ユーザ情報登録
return $repo->save($email);
}
function updateContact(int $id, array $in, ContactRepo $repo): void
{
$email = trim($in['email'] ?? '');
$email = strtolower($email);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('invalid email');
}
// ユーザ情報更新
$repo->update($id, $email);
}
✅改善例
final class EmailUtil
{
public static function normalize(string $email): string
{
return strtolower(trim($email));
}
public static function assertValid(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('invalid email');
}
}
}
function registerUser(array $in, UserRepo $repo): int
{
$email = EmailUtil::normalize($in['email'] ?? '');
EmailUtil::assertValid($email);
return $repo->save($email);
}
function updateContact(int $id, array $in, ContactRepo $repo): void
{
$email = EmailUtil::normalize($in['email'] ?? '');
EmailUtil::assertValid($email);
$repo->update($id, $email);
}
同じ処理が複数箇所にコピーされていると、仕様変更やバグ修正の際に修正漏れが発生しやすくなります。
正規化や検証のような共通処理はユーティリティやサービスに一箇所でまとめ、呼び出す形にすると管理が楽になります。さらに、そのユーティリティに対して集中してユニットテストを書けば振る舞いの保証も一本化できます。
レビュワー向けチェックリスト
プルリクを出す前、またはレビューする際にチェックしてほしい項目です。
- 単一責任(SRP):関数/クラスは1つの目的か?
- KISS:よりシンプルに書けないか?
- DRY:同じロジックの重複はないか?
- 命名:識別子は意図を正確に伝えているか?
- ガード節:早期リターンでネストを浅くしているか?
- エラーハンドリング:例外・メッセージは具体的か?
- 型宣言:引数と戻り値の型は宣言されているか?
- SQLのN+1問題:ループの中で都度クエリを発行していないか?
各プロジェクトでコーディング規約は違うと思いますが、上記を意識することでより良いコードになります。
おわりに
読みやすさは一朝一夕で身につくものではありませんが、小さな習慣を積み重ねることで確実に改善します。今日からできることは意外とシンプルです。まずは「命名を意識する」「1つの関数は1つの責務にする」「ネストを見つけたらガード節に置き換える」といった、PR ごとに1つだけ取り入れられるルールを自分に課してみてください。毎回全部を完璧にする必要はなく、少しずつ直していくことが結果的に大きな差になります。
チームとして取り組むときは、個人の努力を仕組みに落とし込むと効果が出やすいです。たとえば、PR テンプレートに「命名/関数の責務/未使用変数」のチェック項目を加えたり、CI に PHPStan/Psalm、php-cs-fixer を組み込んで自動的に警告を出すようにすると、凡ミスが減ります。そうした小さなプロセス改善が、コードベース全体の健全性を高めます。
また、可読性の改善は測定とフィードバックを通じて定着します。レビュー時間の変化や、バグ件数、PR の差し戻し回数など簡単な指標を追ってみてください。「この1か月でレビュー時間が短くなった」「バグの再発が減った」といった正の変化が見えれば、取り組みのモチベーションになりますし、より改善しやすくなります。
最後に、読みやすいコードを書くことは「未来の自分へのやさしさ」であると同時に、チーム全体に対する思いやりでもあります。自分が書いたコードで誰かが助かる、という経験は開発の喜びにもつながります。まずは今日の PR で一つだけルールを守る――その積み重ねが、やがてはチーム全体の文化になります。

