0

TÁCH BẠCH TRÁCH NHIỆM: KHI CONTROLLER CHỈ DỰNG RESPONSE VÀ SERVICE CHỈ TRẢ VỀ DỮ LIỆU THÔ

trong các hệ thống Laravel quy mô lớn, một trong những cạm bẫy thiết kế tồi tệ nhất và phổ biến nhất chính là sự "nhập nhèm" trách nhiệm: Controller đi viết câu lệnh truy vấn database, còn Service lại đi gọi hàm response()->json() hoặc redirect().

Để xây dựng một codebase sạch sẽ, dễ bảo trì và dễ viết Unit Test, quy tắc vàng mà mọi kỹ sư backend phải nằm lòng là: Controller chỉ nên dựng Response, còn Service chỉ nên trả về dữ liệu thô.

1. Phân Định Rạch Ròi Ranh Giới Trách Nhịem (Separation of Concerns)

Trước khi xét đến code, chúng ta cần hiểu rõ định nghĩa công việc của từng tầng trong một kiến trúc Clean Architecture thu nhỏ:

  • Service Layer (Tầng nghiệp vụ): Là "trái tim" xử lý logic của ứng dụng. Nó thực thi các quy tắc nghiệp vụ (ví dụ: tính tiền giảm giá, tạo giao dịch, gọi API thanh toán, tương tác với Database). Service hoàn toàn mù tịt về HTTP. Nó không biết request đang đến từ Web, từ Mobile App qua API, từ câu lệnh Artisan Console, hay từ một hàng đợi (Queue Worker). Nhiêm vụ duy nhất của nó là nhận đầu vào, xử lý và trả về dữ liệu thô (Raw Data) như mảng PHP (array), các đối tượng Model, DTOs, hoặc các kiểu dữ liệu nguyên thủy.

  • Controller Layer (Tầng điều phối HTTP): Là "giao diện ngoại biên". Nó chịu trách nhiệm tiếp nhận HTTP Request từ người dùng, trích xuất dữ liệu, chuyển giao cho Service xử lý, và sau đó lấy kết quả thô nhận được để đóng gói thành HTTP Response (như trả về JSON, redirect trang, tải file PDF, hoặc ném về View Blade).

2. Phản Biện: Điều Gì Xảy Ra Nếu Trộn Lẫn Trách Nhiệm?

Hãy tưởng tượng bạn viết một Service ôm đồm cả việc dựng Response:

PHP

// ❌ CÁCH VIẾT TỒI: Service tự ý trả về HTTP Response
class UserService 
{
    public function register(array $data) 
    {
        $user = User::create($data);
        
        // Service đang bị phụ thuộc cứng vào HTTP Context!
        return response()->json([
            'status' => 'success',
            'user' => $user
        ], 201);
    }
}

Hệ lụy thảm khốc:

  1. Không thể tái sử dụng (Non-reusable): Ngày mai, khi bạn muốn viết một câu lệnh chạy ngầm (Artisan Command) hoặc một Worker lắng nghe hàng đợi Kafka để tự động tạo tài khoản, gọi vào cái Service này, nó sẽ lập tức trả về một cục JSON response vô nghĩa đối với terminal hoặc queue worker.

  2. Khó viết Unit Test: Khi viết test cho Service, bạn buộc phải giả lập môi trường HTTP Request (Illuminate\Http\Response), làm cho việc kiểm tra logic nghiệp vụ thuần túy trở nên nặng nề và phức tạp một cách vô lý.

3. Chuẩn Mực Thiết Kế Sạch: Clean & Decoupled Code

Hãy xem cách cấu trúc lại theo đúng chuẩn nguyên tắc: Service chỉ trả về dữ liệu thô, còn Controller làm nhiệm vụ đóng gói.

Tầng Service (Tinh khiết, độc lập tuyệt đối):

PHP

namespace App\Services;

use App\Models\User;

class UserService 
{
    public function registerUser(array $data): User 
    {
        // Chỉ làm đúng nghiệp vụ: Tạo user và trả về Model thuần túy
        $user = User::create($data);
        
        event(new UserRegistered($user));

        return $user; // Trả về dữ liệu thô / Object, không dính dáng đến Response
    }
}

Tầng Controller (Linh hoạt dựng Response tùy biến):

PHP

namespace App\Http\Controllers;

use App\Http\Requests\RegisterRequest;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;

class AuthController extends Controller
{
    // Inject Service qua Constructor Injection
    public function __construct(
        protected UserService $userService
    ) {}

    public function register(RegisterRequest $request): JsonResponse
    {
        // 1. Nhận dữ liệu thô từ Service trả về
        $user = $this->userService->registerUser($request->validated());

        // 2. Controller tự quyết định cách đóng gói thành Response tùy theo ngữ cảnh
        return response()->json([
            'success' => true,
            'message' => 'Đăng ký tài khoản thành công.',
            'data' => $user
        ], 201);
    }
}

4. Những Lợi Ích Tuyệt Vời Khi Tuân Thủ Nguyên Tắc Này

  • Linh hoạt đa nền tảng (Multi-Channel Support): Cùng một hàm registerUser trong Service, bạn có thể dùng nó cho Controller trả về JSON (cho Mobile App), Controller trả về Inertia/Blade (cho Web UI), hay cho một Webhook bên thứ ba gọi vào mà không phải sửa một dòng logic nghiệp vụ nào bên trong Service.

  • Unit Test dễ như ăn kẹo: Khi viết test cho Service, bạn chỉ cần assertions trực tiếp trên dữ liệu thô trả về (Assert::assertInstanceOf(User::class, $result)) mà không cần bận tâm đến mã trạng thái HTTP 200 hay định dạng JSON.

  • Codebase trường tồn theo thời gian: Phân định rõ ràng ranh giới giúp các lập trình viên khác khi đọc code không bao giờ phải bối rối tự hỏi "Không biết hàm này đang xử lý logic tính toán hay đang render giao diện".

💡 Lời Kết

Quy tắc "Service trả về dữ liệu thô, Controller dựng Response" không chỉ là một thói quen lập trình, mà là biểu hiện của tư duy thiết kế hệ thống hướng đối tượng chuyên nghiệp. Khi bạn giữ cho tầng nghiệp vụ tinh khiết và cô lập hoàn toàn khỏi tầng giao tiếp HTTP, mã nguồn của bạn sẽ đủ sức mạnh để mở rộng quy mô lên hàng chục microservices mà không sợ sập đổ cấu trúc.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí