🥁

【Laravel実例で学ぶ】リーダブルコードの要点まとめ

に公開

はじめに

「リーダブルコード」はエンジニアなら必読の名著として知られています。しかし、サンプルコードはC++、Python、JavaScript、Javaで記述されているため、Laravel開発者にはイメージしにくい部分があります。

この記事では、Laravelの具体的なコードサンプルを使ってリーダブルコードの要点をまとめていきます。

リーダブルコードの基本理念

優れたコードとは何か?

優れたコードとは「他の人が読んだ時に、コードの意味を理解するまでの時間を短くできるコード」です。

重要なポイント:

  • コードは短く書いたほうがよいが「理解するまでの時間が短くなる」ほうが優先度は高い
  • 「他の人」とは未来の自分かもしれません

第1部:表面上の改善

名前に情報を詰め込む

変数名や関数名は、コードを読む人に正確な情報を伝える重要な手段です。曖昧な名前は理解を阻害し、明確な名前は理解を促進します。

明確な単語を選ぶ

悪い例:

// UserController.php
public function getData($id)
{
    return User::get($id);
}

良い例:

// UserController.php  
public function fetchUserProfile($userId)
{
    return User::findOrFail($userId);
}

例えばインターネットからデータを取ってくる場合にはFetchPage(url)やDownloadPage(url)の方が明確なように、getDataよりfetchUserProfileの方が何をしているかが明確です。

汎用的すぎる名前を避ける

tmpdatainfoなどの汎用的な名前は、その変数が何を表しているのかを読み手に伝えません。変数の具体的な役割や内容を表現する名前を選びましょう。

悪い例:

// 汎用的すぎる変数名
$tmp = $user->orders()->where('status', 'pending')->get();
$data = $tmp->map(function($order) {
    return $order->total;
});

良い例:

// 意図が明確な変数名
$pendingOrders = $user->orders()->where('status', 'pending')->get();
$pendingOrderTotals = $pendingOrders->map(function($order) {
    return $order->total;
});

tmp → 生存期間が短く、一時的な保管がもっとも大切な変数につける場合がある程度に留めるべきです。

Laravelらしい命名規則

Laravelには確立された命名規則があります。これに従うことで、他のLaravel開発者が理解しやすいコードになります。

// モデル - 単数形、パスカルケース
class OrderItem extends Model

// コントローラー - 複数形、パスカルケース  
class OrderItemsController extends Controller

// マイグレーション - スネークケース
public function up()
{
    Schema::create('order_items', function (Blueprint $table) {
        $table->id();
        $table->foreignId('order_id')->constrained();
        $table->string('product_name');
        $table->integer('quantity');
        $table->decimal('unit_price', 8, 2);
    });
}

誤解されない名前

名前は正確でなければなりません。読み手が誤解する可能性のある名前は避け、境界や条件を明確に表現しましょう。

悪い例:

// startDateやendDateの前日・当日・翌日のどの日付が含まれるのか不明
public function getOrdersBetweenDates($startDate, $endDate)
{
    return Order::whereBetween('created_at', [$startDate, $endDate])->get();
}

良い例:

// 境界が明確(開始日・終了日の当日が含まれることがわかる)
public function getOrdersFromDateToDateInclusive($startDate, $endDate)
{
    return Order::whereBetween('created_at', [$startDate, $endDate])->get();
}

// または(引数名で当日が含まれることを明確化)
public function getOrdersInDateRange($startDateInclusive, $endDateInclusive)
{
    return Order::whereBetween('created_at', [$startDateInclusive, $endDateInclusive])->get();
}

美しいコードレイアウト

コードの見た目は理解しやすさに大きく影響します。一貫性のあるスタイルと適切なグループ化により、コードの構造を視覚的に表現できます。

一貫性のあるスタイル

// 関連するコードをまとめる
class OrderService
{
    // 注文作成関連
    public function createOrder(array $orderData): Order
    {
        return DB::transaction(function () use ($orderData) {
            $order = Order::create($orderData);
            $this->createOrderItems($order, $orderData['items']);
            $this->updateInventory($orderData['items']);
            
            return $order;
        });
    }
    
    private function createOrderItems(Order $order, array $items): void
    {
        foreach ($items as $item) {
            $order->items()->create($item);
        }
    }
    
    private function updateInventory(array $items): void
    {
        foreach ($items as $item) {
            Product::find($item['product_id'])
                ->decrement('stock_quantity', $item['quantity']);
        }
    }
    
    // 注文検索関連
    public function searchOrders(array $criteria): Collection
    {
        $query = Order::query();
        
        if (isset($criteria['status'])) {
            $query->where('status', $criteria['status']);
        }
        
        if (isset($criteria['date_from'])) {
            $query->where('created_at', '>=', $criteria['date_from']);
        }
        
        return $query->get();
    }
}

第2部:ループとロジックの単純化

制御フローを読みやすくする

制御フローはプログラムの流れを決定する重要な部分です。複雑なネストを避け、自然に読める流れを作ることで、コードの理解が容易になります。

ガード節の活用

悪い例:

public function updateUserProfile(Request $request, $userId)
{
    $user = User::find($userId);
    if ($user) {
        if ($request->has('name')) {
            if (strlen($request->name) > 0) {
                $user->name = $request->name;
                $user->save();
                return response()->json(['message' => 'Profile updated']);
            } else {
                return response()->json(['error' => 'Name cannot be empty'], 400);
            }
        } else {
            return response()->json(['error' => 'Name is required'], 400);
        }
    } else {
        return response()->json(['error' => 'User not found'], 404);
    }
}

良い例:

public function updateUserProfile(Request $request, $userId)
{
    // ガード節で早期リターン
    $user = User::find($userId);
    if (!$user) {
        return response()->json(['error' => 'User not found'], 404);
    }
    
    if (!$request->has('name')) {
        return response()->json(['error' => 'Name is required'], 400);
    }
    
    if (empty($request->name)) {
        return response()->json(['error' => 'Name cannot be empty'], 400);
    }
    
    // 正常系の処理
    $user->update(['name' => $request->name]);
    return response()->json(['message' => 'Profile updated']);
}

ガード節:処理の対象外とする条件を、関数やループの先頭に集めて return や continue/break で抜ける方法。ネストを減らし正常系の処理が分かりやすい

巨大な式を分割する

複雑で長い式は理解が困難です。式を意味のある単位に分割し、中間結果に意味のある名前を付けることで、コードの意図を明確にできます。

悪い例:

public function calculateOrderDiscount($order)
{
    return $order->total * (($order->user->membership_level === 'premium' && $order->total > 10000) ? 0.15 : (($order->user->membership_level === 'gold' && $order->total > 5000) ? 0.10 : (($order->user->membership_level === 'silver' && $order->total > 3000) ? 0.05 : 0)));
}

良い例:

public function calculateOrderDiscount($order)
{
    $user = $order->user;
    $total = $order->total;
    
    $isPremiumMemberWithLargeOrder = $user->membership_level === 'premium' && $total > 10000;
    $isGoldMemberWithMediumOrder = $user->membership_level === 'gold' && $total > 5000;
    $isSilverMemberWithSmallOrder = $user->membership_level === 'silver' && $total > 3000;
    
    if ($isPremiumMemberWithLargeOrder) {
        return $total * 0.15;
    }
    
    if ($isGoldMemberWithMediumOrder) {
        return $total * 0.10;
    }
    
    if ($isSilverMemberWithSmallOrder) {
        return $total * 0.05;
    }
    
    return 0;
}

式を変数に格納することで読みやすくする(注意:意味がないならやらない)

変数のスコープを縮める

変数のスコープは可能な限り小さくしましょう。グローバルな状態やクラス変数の乱用は、コードの追跡を困難にし、予期しない副作用を生む可能性があります。

悪い例:

class OrderController extends Controller
{
    private $temporaryData = []; // グローバルな状態
    
    public function processOrders()
    {
        $orders = Order::pending()->get();
        
        foreach ($orders as $order) {
            $this->temporaryData = $order->toArray(); // グローバルな変数を使用
            $this->processPayment();
            $this->sendConfirmation();
        }
    }
    
    private function processPayment()
    {
        // $this->temporaryDataを使用
        // どのデータが入っているか分からない
    }
}

良い例:

class OrderController extends Controller
{
    public function processOrders()
    {
        $orders = Order::pending()->get();
        
        foreach ($orders as $order) {
            $this->processOrder($order); // 必要なデータを明示的に渡す
        }
    }
    
    private function processOrder(Order $order)
    {
        $this->processPayment($order);
        $this->sendConfirmation($order);
    }
    
    private function processPayment(Order $order)
    {
        // 引数として明示的に受け取る
        $paymentResult = PaymentService::charge($order->total, $order->payment_method);
        // ...
    }
}

グローバル変数に限らず、全ての変数の「スコープを縮める」のはいい考えである


第3部:コードの再構成

無関係の下位問題を抽出する

一つの関数やメソッドが複数の責任を持つと、理解が困難になります。無関係な処理は別のメソッドやクラスに抽出し、単一責任の原則に従いましょう。

悪い例:

public function generateInvoice($orderId)
{
    $order = Order::with('items', 'user')->findOrFail($orderId);
    
    // PDF生成ロジックが混在
    $pdf = new TCPDF();
    $pdf->AddPage();
    $pdf->SetFont('Arial', 'B', 16);
    $pdf->Cell(0, 10, 'Invoice', 0, 1, 'C');
    
    // 税計算ロジックが混在
    $subtotal = $order->items->sum(function($item) {
        return $item->quantity * $item->unit_price;
    });
    $taxRate = 0.08;
    $tax = $subtotal * $taxRate;
    $total = $subtotal + $tax;
    
    // メール送信ロジックが混在
    Mail::to($order->user->email)->send(new InvoiceMail($pdf));
    
    return $pdf;
}

良い例:

public function generateInvoice($orderId)
{
    $order = Order::with('items', 'user')->findOrFail($orderId);
    
    // 各責任を専用のサービスに分離
    $invoiceData = $this->calculateInvoiceTotals($order);
    $pdf = $this->pdfService->generateInvoicePdf($order, $invoiceData);
    $this->mailService->sendInvoice($order->user, $pdf);
    
    return $pdf;
}

private function calculateInvoiceTotals(Order $order): array
{
    $subtotal = $order->items->sum(function($item) {
        return $item->quantity * $item->unit_price;
    });
    
    $tax = $this->taxCalculator->calculate($subtotal);
    
    return [
        'subtotal' => $subtotal,
        'tax' => $tax,
        'total' => $subtotal + $tax
    ];
}

一度に1つのことを

一つのメソッドは一つのことに集中すべきです。複数の処理を一つのメソッドに詰め込むと、理解が困難になり、テストも難しくなります。

悪い例:

public function getUserDashboardData($userId)
{
    $user = User::findOrFail($userId);
    $orders = $user->orders()->latest()->take(5)->get();
    $totalSpent = $user->orders()->sum('total');
    $favoriteProducts = DB::select("
        SELECT p.*, COUNT(oi.id) as order_count 
        FROM products p 
        JOIN order_items oi ON p.id = oi.product_id 
        JOIN orders o ON oi.order_id = o.id 
        WHERE o.user_id = ? 
        GROUP BY p.id 
        ORDER BY order_count DESC 
        LIMIT 3
    ", [$userId]);
    
    return [
        'user' => $user,
        'recent_orders' => $orders,
        'total_spent' => $totalSpent,
        'favorite_products' => $favoriteProducts
    ];
}

良い例:

public function getUserDashboardData($userId)
{
    $user = User::findOrFail($userId);
    
    return [
        'user' => $user,
        'recent_orders' => $this->getRecentOrders($user),
        'total_spent' => $this->getTotalSpent($user),
        'favorite_products' => $this->getFavoriteProducts($user)
    ];
}

private function getRecentOrders(User $user): Collection
{
    return $user->orders()->latest()->take(5)->get();
}

private function getTotalSpent(User $user): float
{
    return $user->orders()->sum('total');
}

private function getFavoriteProducts(User $user): Collection
{
    return Product::select('products.*', DB::raw('COUNT(order_items.id) as order_count'))
        ->join('order_items', 'products.id', '=', 'order_items.product_id')
        ->join('orders', 'order_items.order_id', '=', 'orders.id')
        ->where('orders.user_id', $user->id)
        ->groupBy('products.id')
        ->orderByDesc('order_count')
        ->take(3)
        ->get();
}

第4部:コメントについて

コメントすべきことを知る

コメントは「なぜそうするのか」を説明するものです。コード自体が「何をするのか」を表現できない場合や、複雑なビジネスロジックの背景を伝える必要がある場合に有効です。

良いコメントの例

class OrderService
{
    /**
     * 注文の自動キャンセル処理
     * 
     * 支払い待ちの注文を24時間後に自動キャンセルする
     * 在庫の確保期間を超えた場合の処理
     */
    public function cancelExpiredOrders(): int
    {
        $cutoffTime = now()->subHours(24);
        
        // TODO: 将来的にはキャンセル前にリマインダーメールを送信する
        $expiredOrders = Order::where('status', 'pending_payment')
            ->where('created_at', '<', $cutoffTime)
            ->get();
        
        $cancelledCount = 0;
        foreach ($expiredOrders as $order) {
            // 在庫を解放してから注文をキャンセル
            $this->releaseInventory($order);
            $order->update(['status' => 'cancelled']);
            $cancelledCount++;
        }
        
        return $cancelledCount;
    }
    
    /**
     * 複雑な税率計算
     * 
     * 商品カテゴリと配送先によって税率が変わる
     * - 食品: 8%
     * - 書籍: 0%  
     * - その他: 10%
     * - 海外配送: 0%
     */
    private function calculateTax(Order $order): float
    {
        if ($order->shipping_country !== 'JP') {
            return 0; // 海外配送は免税
        }
        
        $tax = 0;
        foreach ($order->items as $item) {
            $rate = match($item->product->category) {
                'food' => 0.08,
                'book' => 0.00,
                default => 0.10
            };
            
            $tax += $item->total * $rate;
        }
        
        return $tax;
    }
}

コメントの目的はコードの意図を読み手に伝えることです。コードについての考えをはっきり文章化することが大切です

正確で簡潔なコメント

コメントは正確性と簡潔性のバランスが重要です。明らかなことをコメントで説明するのは避け、本当に必要な情報のみを記述しましょう。

悪い例:

// コード自体が何をしているかが明らかなのに、わざわざコメントで説明している
// ユーザーのIDを取得する
$userId = $request->user()->id;

// データベースからユーザーを取得する  
$user = User::find($userId);

// ユーザーの名前を更新する
$user->name = $request->name;

// データベースに保存する
$user->save();

良い例:

/**
 * ユーザープロフィールの更新
 * 
 * バリデーション済みのデータでユーザー情報を更新
 * 更新履歴はuser_update_logsテーブルに記録される
 */
public function updateProfile(UpdateProfileRequest $request)
{
    $user = $request->user();
    
    // 更新前の状態を履歴として保存
    $this->logUserUpdate($user, $request->validated());
    
    $user->update($request->validated());
    
    return response()->json(['message' => 'Profile updated successfully']);
}

Laravelでの実践的なテクニック

Eloquentモデルでの可読性向上

Eloquentモデルでは、スコープやアクセサを活用することで、ビジネスロジックを適切な場所に配置し、可読性を向上できます。

class Order extends Model
{
    // 意図が明確なスコープ
    public function scopePending($query)
    {
        return $query->where('status', 'pending');
    }
    
    public function scopeCompletedInPeriod($query, $startDate, $endDate)
    {
        return $query->where('status', 'completed')
                    ->whereBetween('completed_at', [$startDate, $endDate]);
    }
    
    // 計算ロジックをメソッドに抽出
    public function getTotalWithTaxAttribute(): float
    {
        return $this->subtotal + $this->tax_amount;
    }
    
    public function getIsExpiredAttribute(): bool
    {
        return $this->created_at->addHours(24)->isPast() 
               && $this->status === 'pending_payment';
    }
}

// 使用例:scopeCompletedInPeriodメソッドを使用
$recentCompletedOrders = Order::completedInPeriod(
    now()->subMonth(),
    now()
)->latest()->get();

サービスクラスでの責務分離

サービスクラスを適切に分割することで、各クラスが単一の責任を持ち、テストしやすく保守しやすいコードを実現できます。

// 単一責任の原則に従ったサービス分割
class PaymentService
{
    public function processPayment(Order $order, array $paymentData): PaymentResult
    {
        // 支払い処理のみに集中
    }
}

class InventoryService  
{
    public function reserveItems(array $items): bool
    {
        // 在庫管理のみに集中
    }
    
    public function releaseReservation(Order $order): void
    {
        // 在庫解放のみに集中
    }
}

class OrderService
{
    public function __construct(
        private PaymentService $paymentService,
        private InventoryService $inventoryService,
        private NotificationService $notificationService
    ) {}
    
    public function createOrder(array $orderData): Order
    {
        // 各サービスを組み合わせて注文処理を実現
        return DB::transaction(function () use ($orderData) {
            $order = Order::create($orderData);
            
            $this->inventoryService->reserveItems($orderData['items']);
            $paymentResult = $this->paymentService->processPayment($order, $orderData['payment']);
            $this->notificationService->sendOrderConfirmation($order);
            
            return $order;
        });
    }
}

バリデーションでの可読性

Laravelのフォームリクエストを活用することで、バリデーションルールとエラーメッセージを整理し、理解しやすい形で表現できます。

悪い例:

// コントローラー内でバリデーションが散らかっている
public function store(Request $request)
{
    // バリデーションルールが複雑で理解しにくい
    $request->validate([
        'items' => 'required|array|min:1',
        'items.*.product_id' => 'required|exists:products,id',
        'items.*.quantity' => 'required|integer|min:1|max:99',
        'payment_method' => 'required|in:credit_card,bank_transfer',
        'shipping_address' => 'required|string|max:255',
    ], [
        'items.min' => '注文には最低1つの商品が必要です',
        'items.*.quantity.max' => '1商品あたりの注文数量は99個までです',
        'payment_method.in' => '選択された支払い方法は無効です',
    ]);
    
    // 実際の処理...
}

良い例:

// フォームリクエストで整理され、ルールが明確
class CreateOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'items' => ['required', 'array', 'min:1'],
            'items.*.product_id' => ['required', 'exists:products,id'],
            'items.*.quantity' => ['required', 'integer', 'min:1', 'max:99'],
            'payment_method' => ['required', 'in:credit_card,bank_transfer'],
            'shipping_address' => ['required', 'string', 'max:255'],
        ];
    }
    
    public function messages(): array
    {
        return [
            'items.min' => '注文には最低1つの商品が必要です',
            'items.*.quantity.max' => '1商品あたりの注文数量は99個までです',
            'payment_method.in' => '選択された支払い方法は無効です',
        ];
    }
}

// コントローラーはシンプルに
public function store(CreateOrderRequest $request)
{
    // バリデーション済みのデータを使用
    $order = $this->orderService->createOrder($request->validated());
    return response()->json($order);
}

まとめ

この本をある程度実践体現できるだけで、大分可読性の良いコードを書けるようになります

Laravelにおけるリーダブルコードの要点:

  1. 明確な命名 - Laravel規約に従いつつ、意図が伝わる名前を
  2. 単一責任 - コントローラー、サービス、モデルの役割を明確に
  3. 早期リターン - ガード節でネストを減らす
  4. 適切な抽出 - 複雑な処理は小さなメソッドに分割
  5. 意図を伝えるコメント - なぜそうするかを説明

一度読んで終わりではなく、日々エンジニアとしてコードを書いていく中で、何度も読み返して自分の中に定着していくようにすることをオススメします

リーダブルコードの原則を意識して、保守しやすく理解しやすいLaravelアプリケーションを作っていきましょう!

Discussion