Dependency Injection
Nếu các bạn đã biết về nguyên lý SOLID thì biết tới nguyên tắc cuối cùng được nhắc đến là Đảo ngược phụ thuộc – Dependency Inversion Principle (DIP). Nhắc lại nguyên tắc này, chúng ta có định nghĩa:
+ Các module cấp cao không nên phụ thuộc vào module cấp thấp.
+ Các class giao tiếp với nhau nên thông qua interface, không nên thông qua các implemention.
Chi tiết có thể xem lại trong bài viết ” Nguyên lý solid PHP “.
Dựa vào đó có 1 khái niệm chúng ta sẽ tìm hiểu trong bài viết này là Dependency Injection. Dependency Injection là 1 design pattern cho phép xóa bỏ sự phụ thuộc hard code, làm cho ứng dụng dễ dàng mở rộng cũng như maintain về sau.
Có 3 khái niệm cần phân biệt:
Dependency Inversion: Đây là nguyên lý để thiết kế và viết code.
Inversion of Control: Đây là 1 design pattern được tạo ra để code tuân thủ nguyên lý Dependency Inversion. Có nhiều cách để thực hiện pattern này: ServiceLocator, Event, Delegate… và Dependency Injection chỉ là 1 trong các cách đó.
Inversion of Control (IoC)
Dependency Injection giúp chúng ta mở rộng code, và giảm sự phụ thuộc giữa các Dependency với nhau. Tuy nhiên lúc này code của chúng ta phải kiêm thêm nhiệm vụ đựa các phụ thuộc vào ( thông qua, constructor, setter, ….).
Ví dụ:
Chúng ta thực hiện hành động checkout giỏ hàng khi đó cần thực hiện. Chú ý đây chỉ là ví dụ, trong thực tế mọi thứ sẽ khác đi rất nhiều.
+ Lưu dữ liệu vào database.
+ Ghi log nội dung.
+ Thông báo cho người dùng qua email.
Đoạn mã ví dụ sẽ như sau:
<?php $db = new MysqlDatabase; $notify = new EmailNotify; $logger = new FileLogger; $cart = new Cart($db,$logger,$notify); $cart->checkout();
Câu hỏi đặt ra từ ví dụ trên là, nếu như chúng ta có rất nhiều phụ thuộc thì sẽ như thế nào? Không phải là lúc đó sẽ phải hàm khởi tạo sẽ rất dài và rối sao. Chưa kể đến là chúng ta cũng phải khởi tạo hàng tá đối tượng đưa vào.
Để giảm thiểu độ rắc rối của đoạn mã có thể set phụ thuộc qua các setter, tuy vậy công sức bỏ ra cho chương trình như vậy không hề nhỏ.
Và chúng ta ước rằng giá có công cụ hoặc ai đó làm hộ phần này thì tốt biết mấy. Và Inversion of Control chính là giải pháp để chúng ta thực hiện việc này.
Theo wiki: Inversion of Control là một nguyên tắc lập trình, luồng điều khiển của ứng dụng thì không được quản lý bởi ứng dụng đó mà bởi 1 khung định nghĩa cơ bản khác.
Phân tích về Dependency Injection.
Bài toán ví dụ:
Chúng ta xây dựng ứng dụng sử dụng cơ sở dữ liệu với Mysql. Đương nhiên khi đó, các lớp của chúng ta sẽ dạng như là Mysqlconnection… để thực hiện thao tác với Mysql. Nhưng một ngày đẹp trời vì yêu cầu khách hàng,hay đơn giản là chúng ta thích đổi sang SQL Server thì sẽ phải làm như thế nào? Chúng ta phải lần từng file để sửa, điều này vô cùng tốn công sức, thời gian mà còn chứa nhiều rủi ro…
Khi đó Dependency Injection chính là giải pháp chúng ta giải quyết bài toàn này.
Các phương pháp thực hiện Dependency Injection
Tham khảo ở https://en.wikipedia.org/wiki/Dependency_injection#Interface_injection.
+ Constructor Injection: Các dependency sẽ được truyền vào 1 class thông qua hàm khoải tạo, Đây là cách thông dụng nhất được sử dụng nhiều ngôn ngữ lập trình và nhiều framework. Với PHP thì có thể thấy ở Laravel hay Magento…
+ Setter Injection: Các dependency sẽ được truyền vào 1 class thông qua các hàm Setter/Getter.
+ Interface Injection.
Ưu và nhược điểm của Dependency Injection
Ưu điểm
+ Giảm hard code, tạo linh động giữa các module và cả linh hoạt trong việc mở rộng hành vi của 1 class.
+ Code dễ dạng bảo trì, thay thế, nâng cấp.
+ Dễ test, unit test.
+ Cho phép phát triển đồng thời nhiều module tính năng do sự phụ thuộc gắn kết giữa các module chỉ còn phụ thuộc vào các interface mà thôi.
+ Đây là một phương pháp thường sử dụng trong việc refactoring (tái cấu trúc) code.
Nhược điểm
+ Khó hiểu với người mới, cũng như khó xác định được đối tượng nào đang được Injection vào class hiện tại.
+ Khó debug, và chúng ta phải xem nhiều file, class hơn để thực hiểu được chương trình.
+ Cũng có 1 số hạn chế khi sử dụng để tìm code nhanh trên IDE, tìm tài liệu, hoặc xử lý các tham chiếu…
+ Các đối tượng được tạo từ ban đầu làm giảm performance
Áp dụng Dependency Injection với PHP
Không chỉ trong Asp, java (spring)… mà ngay trong PHP chúng ta cũng có thể áp dụng các kỹ thuật này. Mà điển hình nhất trong PHP chính là cách vận hành của Laravel.
Chúng ta cùng xây dựng 1 ví dụ cho việc này.
Vẫn là ví dụ bên trên với việc checkout giỏ hàng.
Khai báo các interface cho hành động cần xử lý.
<?php // Thực hiện việc ghi log dữ liệu interface Logger{ public function write($text):bool; } // Xử lý kết nối database interface Database{ public function save($table ,$data = [],$where= []):bool; } // Xử lý nhiệm vụ thông báo interface Notify{ public function send($message) :bool; }
Tiếp đến là các lớp dẫn xuất của các interface trên:
<?php class MysqlDatabase implements Database{ public function save($table ,$data = [],$where= []):bool{ echo sprintf('%s save data to table %s'.PHP_EOL,static::class,$table); return true; } } class SqlServerDatabase implements Database{ public function save($table ,$data = [],$where= []):bool{ echo sprintf('%s save data to table %s'.PHP_EOL,static::class,$table); return true; } } class FileLogger implements Logger{ public function write($text):bool{ echo sprintf('%s write log %s'.PHP_EOL,static::class,$text); return true; } } class EmailNotify implements Notify{ public function send($message) :bool{ echo sprintf('%s send notify %s'.PHP_EOL,static::class,$message); return true; } }
Cuối cùng là class xử lý các phụ thuộc:
class DI{ private $container = []; public function register($type,$instance){ echo sprintf('%s register %s =>> %s'.PHP_EOL,static::class,$type,get_class($instance)); $this->container[$type] = $instance; } public function hasType($type):bool{ return array_key_exists($type,$this->container); } public function resolve($type){ if($this->hasType($type)){ return $this->container[$type]; } else{ if(interface_exists($type)){ throw new \Exception('Can not create instance of interface '.$type.'!'); } else if(class_exists($type)){ $ref = $this->getReflectionClassByType($type); $ins = $this->createInstanceType($ref); return $ins; } else{ throw new \Exception('Class '.$type.' is not found!'); } throw new \Exception('Instance of '.$type.' is not found!'); } } protected function getReflectionClassByType($type){ $ref = new ReflectionClass($type); if (! $ref->isInstantiable()) { throw new \Exception('Can not create instance of'.$type.'!'); } return $ref; } protected function createInstanceType($ref){ $constructor = $ref->getConstructor(); if(!isset($constructor)){ $ins = $ref->newInstance(); } else{ $params = $constructor->getParameters(); $parameters = []; foreach($params as $param){ $paramClass = $param->getClass()->getName(); $parameters[] = $this->resolve($paramClass); } $ins = $ref->newInstanceArgs($parameters); } return $ins; } }
Trong class này chúng ta sẽ đi chi tiết từng phần trong class.
Class DI sẽ có:
+ 1 mảng container để chứa các phụ thuộc đã được khai báo.
+ hàm register để đăng ký một phụ thuộc mới.
+ Hàm resolve: thực hiện với tham số đầu vào là tên phụ thuộc và kết quả trả về là đối tượng phù hợp.
Trong hàm resolve này có chú ý:
+ Kiểm tra xem kiểu phụ thuộc đó đã tồn tại trong container chưa, nếu có rồi trả về kết quả, nếu chưa có mới thực thi việc tạo đối tượng.
+ Nếu kiểu tham số truyền vào là dạng interface, thì kiểu đối tượng này không thể tự động khởi tạo được mà phải khởi tạo trước xong sau đó register vào hệ thống. Trong trường hợp đối tượng tương ứng chưa tồn tại sẽ ném ra ngoại lệ.
+ Nếu kiểu đối tượng truyền vào là dạng class và cũng chưa tồn tại trong hệ thống sẽ tiếp tục resolve liên tục để khởi tạo đối tượng tương ứng.
Kết quả trả về của hàm resolve này chỉ có 2 kết quả:
+ Một là sẽ có đối tượng trả về đúng theo kiểu dữ liệu đưa vào.
+ Ném ra ngoại lệ do tham số truyền vào không hợp lệ, hoặc class không tồn tại, hoặc không thể khởi tạo đối tượng.
Để tiện lợi cho việc gọi và sử dụng ta có thể viết thêm class App.php như sau:
class App{ private static $di; private static function getDI(){ if(!isset(static::$di)){ static::$di = new DI; } return static::$di; } public static function make($type){ return static::getDI()->resolve($type); } public static function register($type,$instance){ return static::getDI()->register($type,$instance); } }
Khi đó class Cart của chúng ta như dưới đây.
Chú ý bước này tôi bổ sung thêm 1 class Reward để minh họa cho việc hệ thống tự động khởi tạo đối tượng cho 1 class mới mà không cần phải truyển vào qua hàm register.
class Reward { public function add(){ echo sprintf('%s add reward points'.PHP_EOL,static::class); } } class Cart { private Database $db; private Logger $log; private Notify $notify; private Reward $reward; public function __construct(Database $db,Logger $log,Notify $notify,Reward $reward){ $this->db= $db; $this->log= $log; $this->notify= $notify; $this->reward= $reward; echo sprintf('%s __construct '.PHP_EOL,static::class); } public function checkout(){ $this->db->save('order',[],[]); $this->log->write('checkout'); $this->notify->send('send email notify checkout'); $this->reward->add(); echo "------------------------------------------------------".PHP_EOL; } }
Các tham số được truyền vào thông qua Constructor
Đây là đoạn mã chính của chương trình:
$db = new MysqlDatabase(); App::register(Database::class,$db); $log = new FileLogger(); App::register(Logger::class,$log); $notify = new EmailNotify(); App::register(Notify::class,$notify); $cart = App::make(Cart::class); $cart->checkout();
Kết quả hiển thị màn hình là
DI register Database =>> MysqlDatabase DI register Logger =>> FileLogger DI register Notify =>> EmailNotify Cart __construct MysqlDatabase save data to table order FileLogger write log checkout EmailNotify send notify send email notify checkout Reward add reward points ------------------------------------------------------
Như chúng ta thấy, mặc dù đưa vào là Interface Database nhưng đối tượng được áp dụng xử lý là MysqlDatabase. Tương tự với FileLogger và EmailNotify.
Một chú ý nho nhỏ nữa là Reward mặc dù chúng ta không register nhưng hệ thống đã tự động khởi tạo và tiêm vào trong hàm khởi tạo của Cart.
Giải sử, giờ chúng ta muốn thay đổi 1 chút, thay vì sử dụng Mysql chúng ta sử dụng SQL Server thì cần chỉnh sửa như nào?
Sẽ không có bất kì đoạn mã nào ở Cart.php phải thay đổi cả, đơn giản là chúng ta sẽ khai báo lại (Register) lại mà thôi
$db = new MysqlDatabase(); App::register(Database::class,$db); $log = new FileLogger(); App::register(Logger::class,$log); $notify = new EmailNotify(); App::register(Notify::class,$notify); $cart = App::make(Cart::class); $cart->checkout(); $db = new SqlServerDatabase(); App::register(Database::class,$db); $cart = App::make(Cart::class); $cart->checkout();
Kết quả sẽ là:
DI register Database =>> MysqlDatabase DI register Logger =>> FileLogger DI register Notify =>> EmailNotify Cart __construct MysqlDatabase save data to table order FileLogger write log checkoutEmailNotify send notify send email notify checkoutReward add reward points------------------------------------------------------ DI register Database =>> SqlServerDatabase Cart __construct SqlServerDatabase save data to table order FileLogger write log checkoutEmailNotify send notify send email notify checkoutReward add reward points------------------------------------------------------
Các bạn thấy đó, trong lần checkout 1, kết quả vẫn như cũ là sử dụng MysqlDatabase nhưng sau khi thay đổi đối tượng thực thi sang Sqlserver mà không cần thay đổi bất kì dòng code nào trong cart cả. Hành động thực thi tương ứng đã được thực hiện.
Tổng kết
Tóm lại Inversion of Control hay Dependency Injection là kỹ thuật lập trình phổ biến bởi tính cơ động và hiệu quả nó mang lại. Chúng được áp dụng trong nhiều framework mã nguồn với nhiều ngôn ngữ lập trình khác nhau. Không nói đâu xa, ngay với framework Laravel, nếu nắm được sự hiểu biết về Dependency Injection là bạn đã hiểu cốt lõi cơ bản phần vận hành của framework này. Hi vọng với bài viết này các bạn sẽ hiểu hơn về tiêm phụ thuộc cũng như ứng dụng chúng vào trong thực tế