BÀI 3: Dependency Injection (DI) & Service Container – Chiếc Cầu Nối Giữa Các Tầng
Ở Bài 1 và Bài 2, chúng ta đã hiểu tại sao phải dùng Trừu tượng hóa và cách thiết lập Hợp đồng (Interface). Tuy nhiên, một câu hỏi thực tế sẽ xuất hiện ngay trong đầu bạn:
"Tôi đã viết xong Interface rồi, nhưng làm thế nào để ứng dụng biết lúc nào thì phải gọi Class nào để chạy?"
Câu trả lời chính là Dependency Injection (Tiêm phụ thuộc) và Service Container.
1. Phá vỡ lầm tưởng: "Injection" nghe to tát nhưng là gì?
Hãy bắt đầu bằng một ví dụ rất đời thường. Bạn là một Giám đốc dự án (Controller). Bạn cần một nhân viên lập trình để làm việc.
- Cách làm truyền thống (Tự khởi tạo - Tight Coupling): Bạn tự vào phòng nhân sự, tự gọi cậu nhân viên tên Nguyễn Văn A vào làm. Nếu ngày mai Nguyễn Văn A nghỉ việc và thay bằng Trần Văn B, bạn sẽ phải vào tận phòng code để sửa lại tên nhân viên. Code của bạn bị phụ thuộc cứng (
hardcode). - Cách làm theo kiểu Dependency Injection (Nhận từ bên ngoài): Bạn ngồi ghế giám đốc và tuyên bố: "Tôi không quan tâm cậu là ai, miễn là cậu cầm trên tay cái thẻ 'Lập trình viên' (Interface)". Bộ phận nhân sự (Service Container) sẽ tự động tìm người phù hợp, nhét (
inject) người đó vào phòng làm việc của bạn. Bạn chỉ việc dùng.
Trong lập trình, Dependency Injection nghĩa là: Đừng tự khởi tạo (new ClassName) các đối tượng bên trong class của bạn. Hãy truyền (inject) chúng vào từ bên ngoài (thường thông qua Constructor).
2. So sánh Code "Cứng Nhắc" và Code "Linh Hoạt"
❌ Cách viết cũ (Vi phạm Dependency Inversion):
Controller tự tay new một class cụ thể bên trong nó:
public function search(Request $request){
// Vi phạm nghiêm trọng: Controller tự khởi tạo object cứng
$repo = new ElasticSearchRepository();
$results = $repo->newEsBuilder()->where('name', $request->keyword)->get();
return response()->json($results);
}
Hậu quả: Controller đang ôm đồm việc quản lý vòng đời của đối tượng. Bạn không thể thay thế ElasticSearchRepository bằng một repository khác để viết Unit Test trừ khi sửa trực tiếp code trong Controller.
✅ Cách viết chuẩn mực (Sử dụng Dependency Injection qua Constructor):
Controller không thèm quan tâm object được tạo ra kiểu gì, nó chỉ nhận Interface qua Constructor hoặc Method Injection:
class ProductController extends Controller{
// Tiêm (Inject) Interface thông qua Constructor
public function __construct(
protected EsRepositoryInterface $productRepo
) {}
public function search(Request $request)
{
// Cực kỳ sạch sẽ và linh hoạt
$results = $this->productRepo->newEsBuilder()
->where('name', $request->keyword)
->get();
return response()->json($results);
}
}
3. Phù thủy phía sau hậu trường: Service Container
Nếu viết như trên, ai sẽ là người quyết định truyền ElasticSearchRepository vào EsRepositoryInterface? Đó là nhiệm vụ của Service Container (trong Laravel hay các framework hiện đại đều tích hợp sẵn).
Bạn chỉ cần đăng ký ánh xạ (Binding) trong cấu hình của Framework (ví dụ file AppServiceProvider):
// Nói cho Framework biết: "Hễ cứ ai gọi EsRepositoryInterface, hãy trả về bản thể của ElasticSearchRepository"
$this->app->bind(EsRepositoryInterface::class, ElasticSearchRepository::class);
Khi Framework nhận một HTTP Request gọi đến ProductController:
- Nó nhìn vào Constructor của Controller và thấy tham số yêu cầu
EsRepositoryInterface. - Nó tra cứu trong Service Container và biết phải tạo ra
ElasticSearchRepository. - Nó tự động
newobject đó ra rồi nhét vào Controller một cách trong suốt.
4. Sức mạnh hủy diệt khi viết Unit Test
Tại sao toàn bộ tư duy từ Bài 1 đến Bài 3 này lại quan trọng đến vậy? Đó là vì Unit Test.
Khi bạn muốn test ProductController nhưng không muốn kết nối thật vào cụm Elasticsearch (vì làm thế sẽ chậm và dễ lỗi), nhờ có Interface và Dependency Injection, bạn có thể dễ dàng Mock nó:
- Bạn tạo một class giả lập
MockEsRepository implements EsRepositoryInterface. - Bạn bảo Service Container: "Khi chạy test, hãy inject
MockEsRepositorythay vì bản thật". - Controller của bạn chạy mượt mà, trả về dữ liệu giả lập ngay lập tức mà không cần chạm vào hạ tầng bên dưới.
Tổng kết Series Trừu tượng hóa & Kiến trúc
- Bài 1: Dùng Trừu tượng hóa để giấu đi sự phức tạp, tách biệt "Cái gì" và "Như thế nào".
- Bài 2: Dùng Interface (Contract) để tạo ranh giới giao tiếp sạch sẽ giữa các tầng.
- Bài 3: Dùng Dependency Injection & Service Container để lắp ráp các mảnh ghép lại với nhau một cách lỏng lẻo (Loose Coupling).
Khi kết hợp cả 3 yếu tố này, bạn sẽ sở hữu tư duy thiết kế của một Senior Engineer thực thụ: Code sạch, dễ bảo trì, dễ thay thế công nghệ và dễ viết test.
All Rights Reserved