1. Lịch sử ra đời IoC
IoC là viết tắt của chữ Inversion of Control, dịch sang Tiếng Việt là Đảo Ngược Quyền Điều Khiển.
Trước kia, anh em lập trình viên hay viết code cho các class dính vào nhau, chẳng hạn mình cần làm tính năng send OTP khi user đăng nhập bằng Email.
<?php
class AuthService
{
private GmailService $gmailService;
public function __construct()
{
$this->gmailService = new GmailService();
}
public function sendOtp(string $otp): boolean
{
return $this->gmailService->send($otp);
}
}
Trong code mình viết ở trên, Class AuthService quyết định việc tạo Object GmailService.
Điều này dẫn đến khi ở môi trường dev hoặc staging, nếu muốn ghi otp vào file .txt để dễ code và debug thì rất khó khăn, phải trộn code để làm.
Thậm chí nếu đổi dịch vụ send mail khác, không xài Gmail nữa, thì có thể phải sửa hàng loạt file.
Do vậy, một nhu cầu mới được phát sinh, trao quyền tạo Object GmailService cho Hệ thống.
Từ đó triết lý IoC ra đời.
2. Công cụ
Để thực hiện được triết lý IoC (Đảo Ngược Quyền Điều Khiển), các lập trình viên đã nghĩ ra cách làm sau:
a, Dependency Injection (DI)
DI là kỹ thuật tiêm dependency từ bên ngoài vào class thay vì để class tự khởi tạo. Điều này giúp class chỉ tập trung vào nghiệp vụ, không quan tâm dịch vụ mà nó đang dùng được tạo ra như thế nào.
Tiếp tục ví dụ ở trên, thay vì AuthService tự tạo GmailService, ta inject nó từ bên ngoài:
<?php
interface MailServiceInterface
{
public function send(string $otp): bool;
}
class GmailService implements MailServiceInterface
{
public function send(string $otp): bool
{
// Gửi OTP bằng Gmail
return true;
}
}
class FileMailService implements MailServiceInterface
{
public function send(string $otp): bool
{
// Ghi OTP vào file .txt để debug
file_put_contents('otp.txt', $otp, PHP_EOL);
return true;
}
}
class AuthService
{
private MailServiceInterface $mailService;
// Dependency Injection happening here!
public function __construct(MailServiceInterface $mailService)
{
$this->mailService = $mailService;
}
public function sendOtp(string $otp): bool
{
return $this->mailService->send($otp);
}
}
Với DI, chúng ta có thể dễ dàng đổi dịch vụ gửi OTP mà không phải sửa code nghiệp vụ:
$auth = new AuthService(new GmailService());
// hoặc
$auth = new AuthService(new FileMailService());
b, IoC Container
Nếu dự án có nhiều class và các class phụ thuộc lẫn nhau, thì việc DI thủ công rất mất thời gian.
Chẳng hạn:
<?php
class GmailService
{
public function send(string $mssage)
{
//
}
}
class MailService
{
public function __construct(private GmailService $gmailService)
{
}
}
class AuthService
{
public function __construct(private MailService $mailService)
{
}
}
$authService = new MailService(new GmailService());
Nếu app của chúng ta có hàng trăm class thì DI thủ công là một cơn ác mộng.
Do vậy, các lập trình viên đã nghĩ ra IoC Container để tự động hoá việc DI.
IoC Container sẽ đọc type-hint và tự resolve (tạo instance phù hợp), rồi inject vào class bạn cần.
Một minh hoạ tối giản cho IoC Container
<?php
class Container
{
protected array $bindings = [];
/**
* Đăng ký binding: interface -> class thực thi
*/
public function bind(string $abstract, string $concrete): void
{
$this->bindings[$abstract] = $concrete;
}
/**
* Resolve / tạo object
*/
public function make(string $abstract)
{
// Nếu có binding -> lấy concrete
$concrete = $this->bindings[$abstract] ?? $abstract;
return new $concrete();
}
}
interface MailServiceInterface
{
public function send(string $otp): bool;
}
class GmailService implements MailServiceInterface
{
public function send(string $otp): bool
{
echo "Gửi OTP qua Gmail: {$otp}";
return true;
}
}
class AuthService
{
public function __construct(private MailServiceInterface $mail)
{
$this->mail = $mail;
}
public function sendOtp(string $otp): bool
{
return $this->mail->send($otp);
}
}
$container = new Container();
// Đăng ký: khi thấy MailServiceInterface -> tạo GmailService
$container->bind(MailServiceInterface::class, GmailService::class);
// Tạo mail service từ container
$mailService = $container->make(MailServiceInterface::class);
// Inject vào AuthService
$auth = new AuthService($mailService);
// Gửi OTP
$auth->sendOtp('123456');
Trong thực chiến, nếu các xài các framework như Laravel, thì load AuthService từ IoC Container ra xài luôn, chứ không cần “new AuthService($mailService)”.