[Quán cơm bình dân] Thực đơn số 1: Cơm bình dân Dependency Injection - Phần 2
Bài này là bài tiếp theo phần 1 có nội dung về việc cắt nghĩa Dependency Injection dưới cách giải thích của cá nhân, và cũng nằm trong series Quán cơm bình dân mà chủ bút là kiendt :D Phần 1 của bài ở link này: http://kipalog.com/posts/Quan-com-binh-dan--Thuc-don-so-1--Com-binh-dan-Dependency-I ...
Bài này là bài tiếp theo phần 1 có nội dung về việc cắt nghĩa Dependency Injection dưới cách giải thích của cá nhân, và cũng nằm trong series Quán cơm bình dân mà chủ bút là kiendt :D
Phần 1 của bài ở link này:
http://kipalog.com/posts/Quan-com-binh-dan--Thuc-don-so-1--Com-binh-dan-Dependency-Injection---Phan-1
Tóm tắt phần 1:
- Định nghĩa dependency injection(DI) như là 1 sự khai báo thư viện cần sử dụng
- Tìm hiểu đường đi của request trong các framework có hỗ trợ DI.
- Đưa ra vấn đề ứng dụng trong thực tế và tại sao, khi nào thì áp dụng DI
- Giải thích cách làm của các framework: Làm thế nào họ có thể tự động cấp phát dependency trong các class của họ mà không cần khai báo khi khởi tạo
Phần 2 này sẽ có nội dung
Phần 2 sẽ là viết một DI_container cụ thể, dùng để duyệt các controller-file từ đó khởi tạo dependency và tiến đến là khởi tạo controller rồi trả về view, cùng với đó là phần triển khai cụ thể ví dụ áp dụng kỹ thuật DI và nguyên lý số 5 trong SOLID để tối ưu lại ví dụ "tính toán chi phí công tác của nhân viên" bên trên.
Viết một DI_container để tự động cấp phát thư viện cho các lời gọi controller
Nói lại qua về cấu trúc DI_container của bài trước
class IoC_container { public function get($controller_name) { //lấy ra danh sách các dependency $list_dependency = $this->get_dependency_list($controller_name); //lặp qua list này rồi khởi tạo foreach($list_dependency as $dependency) { //... } //truyền vào constructor của controller $object = new $controller_name($list_dependency); return $object; } protected function get_dependency_list($controller_name) { //đây là bí quyết của các framework - làm cách nào họ có thể tự động cấp phát thư viện, service cho các class, controller của họ. Chính là anh ReflectionClass này. $reflection = new ReflectionClass($controller_name); //đọc file controller dùng biến reflection, tìm các chỗ khai báo injection và khởi tạo thôi //...rất nhiều xử lý đọc file. Document : http://php.net/manual/en/class.reflectionclass.php return []; } }
Bây giờ ta sẽ sửa lại class này một chút để sử dụng.
class IoC_container { public function get($className) { $class = new ReflectionClass($className); $constructor = $class->getConstructor(); //lấy ra các tham số được dependency rồi khởi tạo class $args = $this->get_parameters($constructor); return $class->newInstanceArgs($args); } //hàm này dùng để lấy ra các tham số trong constructor của class cần khởi tạo private function get_parameters(ReflectionFunctionAbstract $constructor) { $parameters = []; foreach ($constructor->getParameters() as $index => $parameter) { $parameterClass = $parameter->getClass(); if ($parameterClass) { //nếu tham số là class => đây là thư viện được dependency, cần phải khởi tạo $dependencyClass = $parameterClass->getName(); $parameters[$index] = new $dependencyClass(); } } return $parameters; } }
Full đoạn code + test class IoC_container phía trên
class PlaneCost { private $brand_name; private $distance; public function __construct() { //... } public function setBrand($brand_name) { $this->brand_name = $brand_name; } public function setDistance($distance) { $this->distance = $distance; } public function cost() { //... tính toán tiền vé dựa trên hãng và khoảng cách return 20000; } } class RailwayCost { private $brand_name; private $distance; public function __construct() { //... } public function setBrand($brand_name) { $this->brand_name = $brand_name; } public function setDistance($distance) { $this->distance = $distance; } public function cost() { //... tính toán tiền vé dựa trên hãng và khoảng cách return 19000; } } class BusinessCost { //chi phí đi lại private $trip_cost; //chi phí ăn ở và các chi phí cá nhân khác private $personal_cost; public function __construct(RailwayCost $trip_cost) { //... $this->trip_cost = $trip_cost; $this->trip_cost->setBrand('Vietnam Airline'); $this->trip_cost->setDistance(2000); } public function count_trip_cost() { return $this->trip_cost->cost(); } } class IoC_container { public function get($className) { $class = new ReflectionClass($className); $constructor = $class->getConstructor(); //lấy ra các tham số được dependency rồi khởi tạo class $args = $this->get_parameters($constructor); return $class->newInstanceArgs($args); } //hàm này dùng để lấy ra các tham số trong constructor của class cần khởi tạo private function get_parameters(ReflectionFunctionAbstract $constructor) { $parameters = []; foreach ($constructor->getParameters() as $index => $parameter) { $parameterClass = $parameter->getClass(); if ($parameterClass) { //nếu tham số là class => đây là thư viện được dependency, cần phải khởi tạo $dependencyClass = $parameterClass->getName(); $parameters[$index] = new $dependencyClass(); } } return $parameters; } } //test $container = new IoC_container(); $businessCost = $container->get('BusinessCost'); echo $businessCost->count_trip_cost();
Thực hành DI và nguyên lý số 5 trong SOLID
Với ví dụ "tính toán chi phí công tác của nhân viên" phía trên, ta cần sửa đổi một chút cho đúng với nguyên lý thứ 5:
- Các module cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả 2 nên phụ thuộc vào abstraction.
- Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại. (Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)
Trong ví dụ trên, ta đã truyền vào phụ thuộc hàm là RailwayCost/PlaneCost. Cách truyền này nói chung rất stupid. Khi có sự thay đổi về logic bên trong ta lại phải vào class chính là BusinessCost để thay đổi cho phù hợp với dependency truyền vào. Ví dụ như trường hợp trên là thay đổi giữa RailwayCost và PlaneCost.
Để hạn chế việc sửa đổi code class chính BusinessCost - mà việc sửa đổi lại chả liên quan gì đến logic hay nhiệm vụ của bản thân class này - ta áp dụng nguyên lý bao gồm cả 1 và 2. Trong trường hợp cụ thể bài toán của chúng ta thì module cấp cao là BusinessCost, module cấp thấp là PlaneCost và RailwayCost. Theo như mô tả thì BusinessCost không nên phụ thuộc trực tiếp vào PlaneCost và RailwayCost mà nên phụ thuộc vào abstraction của 2 class này.
Vậy triển khai cụ thể như thế nào? Ta tạo một interface tên là TripCost sau đó truyền phụ thuộc vào BusinessCost.
interface TripCost { //chỉ định hãng cung cấp dịch vụ public function setBrand($brand_name); //khai báo quãng đường di chuyển public function setDistance($distance); //tính toán chi phí di chuyển public function cost(); }
và sau đó thì để PlaneCost và RailwayCost cùng implements inteface này
class PlaneCost implements TripCost { //... } class RailwayCost implements TripCost { //... }
cuối cùng là khai báo dependency này vào class chính BusinessCost
class BusinessCost { //... public function __construct(TripCost $trip_cost) { //... } }
Như vậy khi có logic thay đổi - nhân viên không dùng tàu hỏa hay máy bay nữa mà bị bắt phải ...đi bộ thì chúng ta cũng không cần phải sửa đổi class BusinessCost bởi vì nó chả liên quan đến logic của class này, chả có lý do gì ta phải sửa nó. Ta chỉ cần thêm 1 class WalkingCost implements TripCost là được.
Lời kết
Trên đây mình đã trình bày lý do tại sao lại sử dụng DI và cách sử dụng. Tuy nhiên cách sử dụng là linh hoạt, và việc chia module thực hiện từng chức năng cũng là do tư duy của từng cá nhân. Có trường hợp chia ra nhiều module, service xử lý từng chức năng là tốt, nhưng cũng có trường hợp làm rối chương trình, gây khó hiểu cho người bảo trì sau này. Nói chung đó là cả một nghệ thuật mà chỉ có kinh nghiệm mới mang đến câu trả lời chính xác nhất.
Dù sao nguyên lý vẫn luôn là nguyên lý, luôn đúng trong mọi trường hợp mọi hoàn cảnh, còn việc kế thừa tư tưởng đó đến đâu thì tùy vào khả năng của từng người, hoàn cảnh của từng dự án (nói nhỏ: như kiểu áp dụng CNXH và CNCS vào đất nước VN mình ấy - đẻ ra cái xây dựng cơ chế thị trường định hướng XHCN ấy ) Tuân thủ các nguyên lý SOLID sẽ giúp bạn rất nhiều trong việc mở rộng ứng dụng cũng như thay đổi và sửa lỗi ứng dụng sau này.
Ghi chú
Một cơ chế nữa trong DI mà mình chưa nói ở đây là cơ chế register/bind interface. Đó là lý do vì sao khi thay đổi code trên theo nguyên lý số 5 của SOLID thì không chạy được. Bởi vì mình chưa có hàm chỉ định interface TripCost nào được thi hành trong ứng dụng của mình. Nói chung phải có 1 hàm nữa trong IoC_container để khai báo việc sử dụng class nào cho interface TripCost
public function register($interface_name, $class_name);
Hi vọng các bạn có thể tự viết được function này để hoàn thiện bài test cũng như hiểu biết thêm về DI và nguyên lý Dependency Inversion.
Bài log tiếp theo mình sẽ giới thiệu đến các bạn một thư viện DI Container được sử dụng rất phổ biến là PHP-DI và mình sẽ nhúng vào trong CodeIgniter để có thể đưa CodeIgniter thành framework hỗ trợ code DI và SOLID - và có thể - đi sâu hơn vài ví dụ triển khai SOLID trong CodeIgniter
Rất mong nhận được sự ủng hộ của các bạn :)