良いコードとは?【第3回】シンプルなロジックについて
1. なぜシンプルなロジックが重要なのか
同じ機能を実現するコードでも、複雑に書くことも、シンプルに書くこともできます。良いコードとは「動くこと」に加えて、他の人(未来の自分も含む)が読んですぐ理解できることが求められます。
複雑なロジックは次のような問題を引き起こします。
- バグが混入しやすい(分岐が多いほどテストすべきパターンが増える)
- レビューに時間がかかる
- 修正時に影響範囲を把握しづらい
これは「KISS原則(Keep It Simple, Stupid)」と呼ばれる考え方で、「賢く見える一行」より「誰でも理解できる数行」の方が価値が高いとされています。
以下、PHPでのビフォーアフター例を3つ紹介します。
2. 例1:深いネストを「早期リターン(ガード節)」で解消する
Before(ネストが深く、読みにくい)
// 値引き閾値
define ('DISCOUNT_THRESHOLD',1000);
// 値引き率
define ('DISCOUNT_RATE1',0.9); // 閾値以上の価格用
define ('DISCOUNT_RATE2',0.8); // 閾値未満の価格用
//-----------------------------------------------------------------------
// 値引き価格の取得
// $user : ユーザー情報
// $price : 購入価格
// 返り値 : 値引き後の価格
//-----------------------------------------------------------------------
function getDiscountPrice($user, $price) {
if ($user !== null) {
// ユーザ情報がある場合
if ($user->isActive()) {
// ユーザがアクティブな場合
if ($user->hasCoupon()) {
// クーポンを持っている場合
if ($price >= DISCOUNT_THRESHOLD) {
// 購入価格が閾値以上の場合
return($price * DISCOUNT_RATE2);
} else {
// 購入価格が閾値未満の場合
return($price * DISCOUNT_RATE1);
}
} else {
// クーポンがない場合
return($price);
}
} else {
// ユーザがアクティブでない場合
return($price);
}
} else {
// ユーザ情報がない場合
return($price);
}
}
問題点:ifが4段ネストしており、「本来やりたいこと(クーポン所持者への割引計算)」がどこにあるのか読み取りにくい。分岐の組み合わせが多く、テストケースも追いにくい。
After(ガード節で早期リターンし、本質のロジックだけ残す)
// 値引き閾値
define ('DISCOUNT_THRESHOLD',1000);
// 値引き率
define ('DISCOUNT_RATE1',0.9); // 閾値以上の価格用
define ('DISCOUNT_RATE2',0.8); // 閾値未満の価格用
//-----------------------------------------------------------------------
// 値引き価格の取得
// $user : ユーザー情報
// $price : 購入価格
// 返り値 : 値引き後の価格
//-----------------------------------------------------------------------
function getDiscountPrice($user, $price) {
if ($user === null) {
// ユーザ情報がない場合
return($price);
}
if (!$user->isActive()) {
// アクティブではない場合
return($price);
}
if ($user->hasCoupon()) {
// クーポンがない場合
return($price);
}
if ($price >= DISCOUNT_THRESHOLD) {
// 購入価格が閾値以上の場合
return($price * DISCOUNT_RATE2);
} else {
// 購入価格が閾値未満の場合
return($price * DISCOUNT_RATE1);
}
}
}
改善点:「割引の対象外になる条件」を先に弾いてしまうことで、残ったコードは「割引対象者の金額計算」だけになり、本来のロジックが一目で分かる。
3. 例2:複雑な条件式は分割する
Before(条件式がそのまま埋め込まれていて意味が読み取りにくい)
//-----------------------------------------------------------------------
// 公開できるか?
// $article : 記事情報
// 返り値 : true できる false できない
//-----------------------------------------------------------------------
function canPublishArticle($article) {
if ($article->status === 'draft'
&& $article->author->isVerified()
&& count($article->body) > 0
&& $article->scheduledAt <= new DateTime()
// ドラフト状態かつ、確認済みかつ、文章があるかつ、公開予定日時を超えている
) {
return true;
}
return false;
}
問題点:ifの条件が4つも連結されており、「結局どういう状態なら公開できるのか」を読み解くのに時間がかかる。同じ条件が他の場所にコピペされると、修正漏れも起きやすい。
After(条件に意味のある名前を付けて分解する)
//-----------------------------------------------------------------------
// 公開できるか?
// $article : 記事情報
// 返り値 : true できる false できない
//-----------------------------------------------------------------------
function canPublishArticle($article) {
if ($article->status === 'publish') {
// 公開の場合
return (false);
}
if (!$article->author->isVerified()) {
// 未確認の場合
return (false);
}
if (count($article->body) == 0) {
// 文章がない場合
return (false);
}
if ($article->scheduledAt > new DateTime()) {
// 公開予定日時を超えていない場合
return (false);
}
return(true);
}
改善点:行数は増えるが条件分を分割することで見やすくなる。今後条件が増えたときも改修しやすい。
4. 例3:似たコードの重複を「共通関数」にまとめる(DRY原則)
Before(似た処理がコピペで繰り返されている)
//-----------------------------------------------------------------------
// ユーザーフォーム内容の検証
// $data : フォームデータ
// 返り値 : なし
//-----------------------------------------------------------------------
function validateUserForm($data) {
if (empty($data['name'])) {
// 名前がない場合
throw new Exception('名前は必須です');
}
if (mb_strlen($data['name']) > 50) {
// 名前が50文字より多い場合
throw new Exception('名前は50文字以内で入力してください');
}
if (empty($data['email'])) {
// メールアドレスがない場合
throw new Exception('メールアドレスは必須です');
}
if (mb_strlen($data['email']) > 500) {
// メールアドレスが500文字より多い場合
throw new Exception('メールアドレスは50文字以内で入力してください');
}
if (empty($data['address'])) {
// 住所がない場合
throw new Exception('住所は必須です');
}
if (mb_strlen($data['address']) > 100) {
// 住所が100文字より多い場合
throw new Exception('住所は50文字以内で入力してください');
}
}
問題点:「必須チェック」と「文字数チェック」という同じロジックが3項目分コピペされている。文字数の上限を変更したくなった場合、3箇所すべてを修正する必要があり、修正漏れの温床になる。
After(共通のバリデーション関数にまとめる)
//-----------------------------------------------------------------------
// フォーム項目の検証
// $label : ラベル
// $value : 項目値
// $maxLength : 最大文字数
// 返り値 : なし
//-----------------------------------------------------------------------
function validateRequiredField($label,$value,$maxLength)
{
if (empty($value)) {
// 項目値がない場合
throw new Exception("{$label}は必須です");
}
if (mb_strlen($value) > $maxLength) {
// 最大文字数を超えている場合
throw new Exception("{$label}は{$maxLength}文字以内で入力してください");
}
}
//-----------------------------------------------------------------------
// ユーザーフォーム内容の検証
// $data : フォームデータ
// 返り値 : なし
//-----------------------------------------------------------------------
function validateUserForm($data)
{
validateRequiredField('名前', $data['name'],50);
validateRequiredField('メールアドレス', $data['email'],500);
validateRequiredField('住所', $data['address'],100);
}
改善点:ロジックを1箇所に集約したことで、文字数上限の変更やエラーメッセージの修正が1箇所で完結する。項目を追加したい場合も1行足すだけで済む。
5. まとめ:シンプルなロジックにするための3つの視点
| 視点 | 手法 | 効果 |
|---|---|---|
| ネストを浅くする | ガード節・早期リターン | 本質的なロジックが目立つようになる |
| 条件式を単純にする | 条件分の分解 | 「何を判定しているか」が一目で分かる |
| 重複をなくす | 共通関数への抽出(DRY) | 修正箇所が1つになり、修正漏れを防げる |
ポイント:シンプルにする、とは「行数を減らす」ことではなく、「読んだ人が意図をすぐに理解できる形にする」ことです。時にはコードが数行増えても、分割・命名によって読みやすくなるなら、それは良いリファクタリングです。

