0

Series Kiến trúc MVC Cơ bản | Bài 3: "Fat Model, Skinny Controller" - Bí quyết giữ code không bị "béo phì"

Chào anh em! Ở Bài 2, chúng ta đã biết Controller đóng vai trò như anh bồi bàn, nhận order từ khách và báo cho đầu bếp (Model). Nhưng trong thực tế đi làm, rất nhiều anh em developer ép anh bồi bàn này làm luôn cả việc... nhặt rau, thái thịt, tính tiền và rửa bát.

Đó là căn bệnh "Bloated Controller" (Controller béo phì) – nơi mà mọi logic tính toán, xác thực dữ liệu, gọi API bên ngoài đều bị nhét hết vào một file Controller dài hàng ngàn dòng. Hôm nay, chúng ta sẽ học cách chữa căn bệnh này bằng một câu thần chú kinh điển trong giới lập trình: "Fat Model, Skinny Controller" (Model béo, Controller gầy).

1. Dấu hiệu nhận biết một Controller "Béo phì"

Hãy nhìn vào một đoạn code Controller xử lý việc tạo đơn hàng (Checkout) dưới đây. Trông quen không anh em?

class OrderController extends Controller 
{
    public function checkout(Request $request) 
    {
        // 1. Validation (Xác thực dữ liệu) lộn xộn
        if (!$request->has('product_id') || $request->qty < 1) {
            return response()->json(['error' => 'Dữ liệu không hợp lệ'], 400);
        }

        // 2. Logic nghiệp vụ (Business logic) viết thẳng vào Controller
        $product = Product::find($request->product_id);
        if ($product->stock < $request->qty) {
            return response()->json(['error' => 'Hết hàng'], 400);
        }

        $totalPrice = $product->price * $request->qty;
        if ($request->has('discount_code') && $request->discount_code == 'SALE20') {
            $totalPrice = $totalPrice * 0.8; // Giảm 20%
        }

        // 3. Tương tác Database
        $order = new Order();
        $order->product_id = $product->id;
        $order->qty = $request->qty;
        $order->total = $totalPrice;
        $order->save();

        // 4. Trừ tồn kho
        $product->stock = $product->stock - $request->qty;
        $product->save();

        return response()->json(['message' => 'Mua thành công', 'order' => $order]);
    }
}

Hậu quả của đoạn code trên:

  • Controller đang ôm đồm quá nhiều việc.
  • Nếu ngày mai công ty làm thêm tính năng "Tạo đơn hàng qua API cho Mobile", bạn sẽ phải copy-paste toàn bộ đoạn tính toán giảm giá, trừ tồn kho này sang một Controller khác.
  • Cực kỳ khó viết Unit Test vì mọi thứ dính chặt vào Request HTTP.

2. Refactor: Giảm cân cho Controller, Vỗ béo cho Model

Theo triết lý Skinny Controller, anh bồi bàn chỉ nên làm đúng 3 việc: Nhận Request -> Gọi logic -> Trả Response. Toàn bộ logic tính toán tiền, trừ tồn kho phải đẩy sâu xuống Model.

Hãy cấu trúc lại mã nguồn:

Bước 1: Chuyển logic xuống Model

class Order extends Model 
{
    // Model chứa logic tính tiền và tạo đơn
    public static function createOrder($product, $qty, $discountCode) 
    {
        $totalPrice = $product->price * $qty;
        if ($discountCode == 'SALE20') {
            $totalPrice *= 0.8;
        }

        $order = self::create([
            'product_id' => $product->id,
            'qty' => $qty,
            'total' => $totalPrice
        ]);

        return $order;
    }
}

class Product extends Model 
{
    // Model chứa logic xử lý tồn kho
    public function deductStock($qty) 
    {
        if ($this->stock < $qty) {
            throw new \Exception('Hết hàng');
        }
        $this->stock -= $qty;
        $this->save();
    }
}

Bước 2: Controller giờ đây cực kỳ "thanh mảnh"

class OrderController extends Controller 
{
    public function checkout(Request $request) 
    {
        try {
            // Giả sử đã có lớp Request Form Validation riêng xử lý đầu vào
            
            $product = Product::findOrFail($request->product_id);
            
            // Controller chỉ làm nhiệm vụ điều phối (gọi hàm)
            $product->deductStock($request->qty);
            $order = Order::createOrder($product, $request->qty, $request->discount_code);

            return response()->json(['message' => 'Mua thành công', 'order' => $order]);
            
        } catch (\Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }
}

3. Khi nào thì Model cũng không nên "quá béo"?

Triết lý "Fat Model" rất tốt cho các dự án vừa và nhỏ. Nhưng ở những hệ thống Enterprise đồ sộ, nếu bạn nhét cả logic gửi Email, logic gọi cổng thanh toán vào Model thì file Model đó cũng sẽ phình to thành hàng ngàn dòng code.

Đó là lúc chúng ta cần đến những khái niệm nâng cao hơn như Service Pattern, Repository Pattern hay Kiến trúc Modules (chia nhỏ Model theo từng vùng nghiệp vụ) mà anh em có thể tìm đọc ở các series nâng cao khác trên blog này.

Tổng kết Series

Kiến trúc MVC là cánh cửa đầu tiên đưa anh em vào thế giới thiết kế phần mềm bài bản. Bằng cách hiểu rõ luồng đi của dữ liệu (Request/Response) và tuân thủ chặt chẽ việc phân chia trách nhiệm (Skinny Controller), code của anh em sẽ luôn giữ được sự trong trẻo, dễ đọc và dễ nâng cấp trong tương lai.

Hãy luôn nhớ: Viết code cho máy tính đọc là chuyện nhỏ, viết code cho con người (đồng nghiệp và chính bạn sau 6 tháng nữa) đọc hiểu được mới là đẳng cấp!

Vậy là chúng ta đã hoàn thành trọn vẹn series MVC cơ bản để làm nền móng. Sắp tới, để tiếp tục giữ nhiệt cho chuỗi bài chia sẻ kiến thức hệ thống trên Viblo, bạn muốn chúng ta triển khai chủ đề về C++ (như series 30 bài về Operator Overloading/System Design) hay chuyển sang một Design Pattern cụ thể nào khác trong PHP?


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.