Series Dependency Injection | Bài 3: Làm chủ Vòng đời (Lifecycle) - Trận chiến giữa Singleton, Transient và Scoped
Chào anh em! Ở Bài 2, chúng ta đã tung hô IoC Container như một "vị quản gia" thần thánh, biết tự động tìm và lắp ráp (new) các object lại với nhau để tiêm vào Class của chúng ta.
Nhưng, hãy dừng lại một giây và tự hỏi: Khi có 10 Controller cùng yêu cầu tiêm DatabaseConnection, vị quản gia này sẽ chạy đi new 10 cái kết nối mới tinh, hay chỉ new 1 lần rồi đem cái cũ phân phát cho cả 10 người?
Câu trả lời là: Tùy thuộc vào cách bạn cấu hình Vòng đời (Lifecycle) cho nó. Nếu cấu hình sai, nhẹ thì làm chậm hệ thống, nặng thì sập database vì quá tải connection, hoặc tệ hơn là lộ dữ liệu chéo giữa các user. Hôm nay, chúng ta sẽ bóc tách 3 kiểu vòng đời kinh điển nhất trong thế giới Dependency Injection: Transient, Singleton và Scoped.
1. Transient (Tạo mới mọi lúc, mọi nơi)
Bản chất: Gắn liền với từ khóa new. Mỗi khi một Class hoặc một Interface được yêu cầu (resolve) từ Container, nó sẽ tạo ra một bản sao (instance) hoàn toàn mới.
Khi nào nên dùng? Dành cho những Service chỉ chứa logic xử lý tính toán, không lưu trữ trạng thái bên trong (Stateless).
Ví dụ thực tế: Bạn có một HashPasswordService chuyên dùng để băm mật khẩu. Hàm này nhận vào chuỗi string và trả ra chuỗi đã băm, không lưu lại data gì. Việc tạo mới nó liên tục không tốn bao nhiêu tài nguyên và đảm bảo an toàn tuyệt đối, không sợ bị dính dữ liệu từ các request trước.
Trong Laravel, đây là cách Binding mặc định khi bạn dùng lệnh $this->app->bind(...).
2. Singleton (Chỉ một và duy nhất)
Bản chất: Container sẽ khởi tạo object này đúng MỘT LẦN duy nhất trong suốt vòng đời chạy ứng dụng. Bất cứ ai yêu cầu sau đó, Container đều trả về đúng cái instance đầu tiên ấy.
Khi nào nên dùng? Dành cho những thao tác cực kỳ tốn kém tài nguyên để khởi tạo, hoặc bắt buộc phải chia sẻ chung một trạng thái (Stateful).
Ví dụ thực tế - Bài toán thiết bị ngoại vi: Giả sử chúng ta đang build hệ thống bán vé tự động AFC (Automatic Fare Collection) cho tuyến Metro. Backend của chúng ta cần một HardwareConnectionService để mở cổng TCP/IP giao tiếp với máy bán vé tự động (TVM) tại nhà ga.
Nếu cấu hình service này là Transient, mỗi lần có khách quẹt thẻ, hệ thống lại mở một kết nối TCP mới. Chỉ cần 100 khách quẹt thẻ cùng lúc vào giờ cao điểm, hàng trăm kết nối mạng sẽ được tạo ra, thiết bị ngoại vi tại trạm chắc chắn sẽ "treo" ngay lập tức vì kiệt sức.
Giải pháp bắt buộc ở đây là Singleton:
// Cấu hình trong ServiceProvider của Laravel
$this->app->singleton(HardwareConnectionService::class, function ($app) {
return new HardwareConnectionService('192.168.1.100', 8080);
});
Lúc này, kết nối mạng chỉ được mở đúng 1 lần khi hệ thống khởi động. 100 giao dịch quẹt thẻ sau đó sẽ dùng chung cái "ống nước" kết nối này để truyền tín hiệu. Vừa nhanh, vừa tối ưu!
3. Scoped (Vòng đời theo Request)
Lưu ý: Môi trường PHP truyền thống (PHP-FPM) mỗi HTTP Request vốn đã là một vòng đời độc lập, nên Scoped và Singleton trong Laravel (PHP-FPM) gần như giống nhau. Tuy nhiên, nếu anh em chạy PHP với Swoole, Octane hoặc dùng các ngôn ngữ như C# (.NET), Node.js (NestJS), Scoped đóng vai trò sinh tử.
Bản chất: Object được tạo ra 1 lần duy nhất trên MỖI HTTP REQUEST. Khi Request A kết thúc, object đó bị tiêu hủy. Sang Request B, một object mới được tạo ra.
Khi nào nên dùng? Khi bạn muốn chia sẻ một trạng thái chung giữa nhiều class, nhưng CHỈ TRONG PHẠM VI của một người dùng đang gọi API hiện tại.
Ví dụ thực tế: Bạn có một class CurrentUserContext lưu thông tin (User ID, Role, Permissions) của người đang gọi API.
- Nếu để Transient: Cứ đi qua Middleware, Controller, Repository... hệ thống lại phải chọc xuống Database để lấy lại thông tin User -> Sập DB vì quá nhiều query thừa.
- Nếu để Singleton (trên môi trường Node.js/Swoole): Thông tin của ông Nguyễn Văn A (Request 1) sẽ bị lưu lại và hiển thị cho bà Trần Thị B (Request 2) -> Lỗi bảo mật nghiêm trọng (Memory Leak/Data Bleeding).
- Scoped là chân ái: Lấy data User 1 lần ở Middleware, lưu vào CurrentUserContext. Các layer bên dưới (Controller, Service) cứ thế lấy ra dùng mà không cần query lại. Xong Request là xóa sạch!
4. Bùa chú nâng cao: Contextual Binding (Gắn kết theo ngữ cảnh)
Đôi khi, hệ thống của bạn có một Interface StorageInterface, nhưng bạn lại có 2 Class implement nó là LocalStorage và S3Storage.
Làm sao để chỉ thị cho Container: "Nếu là Module Báo cáo thì tiêm LocalStorage, còn nếu là Module Media thì tiêm S3Storage"?
Laravel Container cung cấp một tính năng cực mạnh tên là Contextual Binding:
// Khi ReportController yêu cầu StorageInterface, hãy đưa cho nó LocalStorage
$this->app->when(ReportController::class)
->needs(StorageInterface::class)
->give(LocalStorage::class);
// Khi MediaController yêu cầu StorageInterface, hãy đưa cho nó S3Storage
$this->app->when(MediaController::class)
->needs(StorageInterface::class)
->give(S3Storage::class);
Không cần sửa một dòng code logic nào trong Controller, hệ thống tự động bẻ lái chính xác dựa trên ngữ cảnh!
Tổng kết Bài 3
Không có vòng đời (Lifecycle) nào là hoàn hảo, chỉ có vòng đời phù hợp với bài toán của bạn:
- Transient: Xài xong bỏ, an toàn, không lo rò rỉ dữ liệu, dùng cho các hàm xử lý logic thuần.
- Singleton: Xài chung 1 cái, tiết kiệm tài nguyên mạng/RAM, dùng cho Database, Logger, kết nối phần cứng.
- Scoped: Share data trong cùng 1 request, cực kỳ quan trọng cho các ứng dụng chạy nền (Daemon/Swoole/Node.js).
Nắm vững được các khái niệm này, anh em đã chính thức thoát khỏi mác "Thợ gõ code" để bước chân vào con đường thiết kế hệ thống thực thụ.
All rights reserved