0

Tư duy Senior: Sự thật "ngã ngửa" đằng sau chiếc Interface vạn năng getFirstBy()

Chào anh em Viblo, lại là mình đây!

Nếu anh em đã từng tìm hiểu về Repository Pattern trong PHP (đặc biệt là các dự án Laravel), chắc chắn 99% anh em từng copy/paste một cái Base Repository từ trên mạng về, và trong đó chễm chệ một chiếc method (phương thức) trông cực kỳ nguy hiểm và quyền lực thế này:

public function getFirstBy(array $condition = [], array $select = [], array $with = []);

Hồi mới vào nghề, mình nhìn cái method này với ánh mắt đầy ngưỡng mộ. "Wow! Chàng trai này thật đa năng!". Bất kể lúc nào mình cần lấy dữ liệu: tìm theo ID, tìm user đang active, lấy kèm quan hệ (relations), chọn cột để tối ưu query... chỉ cần gọi đúng một hàm này là xong. Quá tiện!

Nhưng theo thời gian, khi dự án phình to ra, số lượng dòng code lên tới hàng trăm ngàn, và mình bắt đầu phải đứng ở góc độ của một Senior để review và thiết kế kiến trúc, mình mới nhận ra: Chiếc method vạn năng này thực chất là một "Anti-pattern" (Mẫu thiết kế tồi) tàn phá code base khủng khiếp. Hôm nay, hãy cùng mình mổ xẻ tư duy tầm nhìn của một Senior khi nhìn vào bản thiết kế Interface này nhé.

1. Sự ảo tưởng về tính đa năng (Góc nhìn của Junior/Mid)

Tại sao chúng ta lại thích thiết kế hàm truyền vào mảng (array)? Bởi vì nó "lười" và nó "dễ".

Thay vì phải định nghĩa 10 hàm khác nhau (getUserById, getActiveUser, getUserWithRoles), bạn gom tất cả vào một hàm duy nhất. Lúc gọi, bạn chỉ cần ném cho nó một cục mảng:

$user = $userRepository->getFirstBy(
    ['status' => 'active', 'age' => 18], // $condition
    ['id', 'name', 'email'],             // $select
    ['roles', 'profile']                 // $with
);

Nhìn thì có vẻ sạch sẽ, tái sử dụng cao. Nhưng thực ra, bạn đang tự đào một cái hố rất sâu cho chính team của mình.

2. Sự thật trần trụi dưới góc nhìn của Senior

Một Senior không đánh giá code qua việc nó chạy được hay không (đó là điều hiển nhiên), mà đánh giá qua khả năng bảo trì, tính dễ đọc và sự an toàn khi thay đổi. Và cái hàm getFirstBy trên vi phạm toàn bộ những tiêu chuẩn đó.

Thứ nhất: Lập trình hướng... Mảng (Array-driven programming)

Trong các ngôn ngữ hiện đại, chúng ta luôn hướng tới Type-safety (An toàn kiểu dữ liệu). Mảng (array) là một hộp đen đáng sợ nhất. Khi bạn đọc đoạn code trên, làm sao bạn biết key status hay age có tồn tại trong database? Làm sao IDE (như PhpStorm) có thể auto-complete cho bạn? Làm sao các công cụ phân tích code tĩnh (như PHPStan) có thể bắt lỗi nếu một anh dev mới vào team lỡ gõ nhầm chữ statsu thay vì status? Câu trả lời là: Không thể! Hệ thống sẽ chết vào lúc Runtime (lúc code đang chạy thật) thay vì báo lỗi ngay lúc bạn gõ code.

Thứ hai: Abstraction rò rỉ (Leaky Abstraction)

Mục đích của Interface/Repository là để giấu đi sự phức tạp của tầng Database (ORM). Business Logic (tầng Controller hoặc Service) không cần biết dưới Database đang dùng MySQL, MongoDB hay ElasticSearch. Nhưng khi bạn thiết kế cái hàm có tham số $with (chuyên dùng cho Eager Loading của Eloquent), hoặc mảng $condition tuân theo cú pháp query của Database, bạn đã đem "ruột gan" của tầng DB phơi bày thẳng lên tầng Service. Tầng Service lúc này bị dính chặt (tightly-coupled) vào ORM. Nếu một ngày bạn muốn đổi ORM, bạn phải viết lại toàn bộ code ở tầng Service.

Thứ ba: Phá vỡ Ngôn ngữ nghiệp vụ (Ubiquitous Language)

Code phải phản ánh được nghiệp vụ. Khi sếp nói: "Lấy cho anh thông tin khách hàng đang bị khóa", code của bạn nên là $repo->getBannedUser(). Nó mang tính con người, dễ đọc, dễ hiểu. Còn $repo->getFirstBy(['status' => -1]) là ngôn ngữ của cái máy. Ba tháng sau đọc lại, bạn sẽ ngồi gãi đầu tự hỏi: "Ủa cái -1 này là trạng thái gì vậy ta?".

3. Senior thiết kế Interface như thế nào?

Thay vì cố gắng nhét mọi thứ vào một cái "thùng rác" tên là mảng, Senior sẽ thiết kế Interface dựa trên nguyên tắc Rõ ràng và Rành mạch (Explicit).

Hướng 1: Trực diện và dễ hiểu (Explicit Methods)

Nếu nghiệp vụ yêu cầu, hãy tạo một hàm mới với tên gọi rõ ràng.

interface UserRepositoryInterface 
{
    public function findById(int $id): ?User;
    public function getActiveUserByEmail(string $email): ?User;
}

IDE sẽ nhắc code mượt mà, type-hinting rõ ràng (bắt buộc truyền string, bắt buộc trả về User hoặc null).

Hướng 2: Áp dụng Criteria Pattern nếu cần filter động Nếu bài toán thực sự yêu cầu phải filter dữ liệu theo hàng chục điều kiện khác nhau (ví dụ: màn hình tìm kiếm nâng cao), Senior sẽ không dùng mảng, mà sẽ dùng Criteria Pattern hoặc các Data Transfer Object (DTO).

// Tạo một Class để chứa các điều kiện tìm kiếm
class UserSearchCriteria {
    public ?string $status = null;
    public ?int $minAge = null;
    public array $includes = []; // Giới hạn sẵn các relation được phép lấy
}

// Interface lúc này sẽ nhận vào một Object thay vì mảng
interface UserRepositoryInterface 
{
    public function findFirstMatching(UserSearchCriteria $criteria): ?User;
}

Với cách thiết kế này, bạn vẫn giữ được sự linh hoạt, nhưng lại kiểm soát được 100% dữ liệu truyền vào. Dữ liệu là một Object có thuộc tính rõ ràng, IDE gợi ý được, Validate được, và giấu kín được logic query SQL ở bên dưới Repository.

Lời kết

Từ bỏ những "chiếc mảng vạn năng" là một trong những bước chuyển mình quan trọng nhất từ một Junior/Mid lên Senior. Giai đoạn đầu, bạn có thể cảm thấy khó chịu vì phải gõ code nhiều hơn, định nghĩa nhiều hàm hơn, tạo nhiều Class (DTO/Criteria) hơn. Nhưng tin mình đi, vài tháng sau khi dự án có biến cố hoặc có thêm người mới vào bảo trì, bạn sẽ thấy quyết định thiết kế Interface rõ ràng ngày hôm nay là một khoản đầu tư vô giá.

Anh em team mình hiện tại đang dùng pattern nào để filter dữ liệu qua Repository? Vẫn dùng array hay đã chuyển sang các pattern an toàn hơn rồi? Cùng chém gió ở dưới nhé!


All Rights Reserved

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