Lập trình PHP

Mô hình lập trình DI - Dependency Injection

Dependency Injection là một mô hình lập trình, cách tổ chức code sao cho các đoạn code khác nhau, các module khác nhau, các lớp khác nhau không phụ thuộc nhau một cách cứng nhắc, mà cần có một cơ chế thay đổi các thành phần phụ thuộc cả ở thời điểm chạy và thời điểm biên dịch. Ví dụ dưới đây trình bày với lập trình PHP.

Bằng cách sử dụng mô hình Dependency Injection ta dễ dàng bảo trì code, test và module hóa ứng dụng. Tất cả các project đều có các thành phần phụ thuộc vào nhau, dự án càng lớn thì càng nhiều thành phần phụ thuộc, thì cơ chế DI giúp cho quản lý các thành phần phụ thuộc này tốt nhất.

Giờ bạn tạo ra 2 lớp mà chúng không sử dụng cơ chế DI, sau đó viết lại có sử dụng DI để xem sự khác biệt:

Lớp thứ nhất là StockItem biểu diễn mặt hàng trong kho (số lượng, tình trạng). Lớp thứ 2 biểu diễn mặt hàng bán trên trang web, liên quan đến mặt hàng lưu trữ trong kho.

<?php

class StockItem {

    private $quantity;
    private $status;

    public function __construct($quantity, $status){
        $this->quantity = $quantity;
        $this->status   = $status;
    }

    public function getQuantity(){
        return $this->quantity;
    }

    public function getStatus(){
        return $this->status;
    }

}

Lớp product trình bày như sau:

<?php
class Product {
    private $stockItem;
    private $code;

    public function __construct($code, $stockQuantity, $stockStatus){
        $this->stockItem  = new StockItem($stockQuantity, $stockStatus);
        $this->code        = $code;
    }

    public function getStockItem(){
        return $this->stockItem;
    }

    public function getCode(){
        return $this->code;
    }
}

$product = new Product("101010", 50, "Áo Dài");
var_dump($product->getStockItem());

Code trên tạo ra lớp Product, khi khởi tạo Product thì cũng khởi tạo đối tượng StockItem trong hàm tạo của lớp Product bằng cách truyền các tham số $stockQuantity, $stockStatus

Với cách sử dụng code như trên, đó là đoạn code bình thường không có vấn đề gì về logic, nhiều khi đánh giá là đoạn code tốt. Tuy nhiên khi vận hành, bảo trì, mở rộng có thể phát sinh một số vấn đề:

  • StockItemProduct là một cặp cố định, vậy thì một lúc nào đó StockItem thay đổi tham số khởi tạo (thêm, bớt) thì sao? Vậy bạn cần phải viết lại hàm khởi tạo trong Product cũng như tất cả các lớp có sử dụng phiên bản cũ của StrockItem
  • Product biết quá nhiều, ở đây là số lượng và trạng thái của sản phẩm trong kho. Điều này giảm đi tính độc lập, đóng kín của lập trình hướng đối tượng.
  • Khi viết unit test cho code trên, vì Product khởi tạo stockItem trong hàm tạo, nên không thể tạo Unit test cho Product mà không tạo Unit test cho stockItem

Viết lại với Dependency Injection

Tiêm vào đối tượng các thành phần phụ thuộc cần thiết:

<?php
class Product {
    private $stockItem;
    private $code;

    public function __construct($code, StockItem $stockItem){
        $this->stockItem   = $stockItem;
        $this->code        = $code;
    }

    public function getStockItem(){
        return $this->stockItem;
    }

    public function getCode(){
        return $this->code;
    }
}

$stockItem = new StockItem(50, "Áo Dài");
$product = new Product("101010", $stockItem);
var_dump($product->getStockItem());

Với cách viết thứ 2 này, đối tượng StockItem không còn khởi tạo bên trong hàm tạo Product nữa, mà nó được truyền vào (tiêm) Product thông qua chính đối tượng StockItem, như vậy khi thay đổi cách khởi tạo StockItem thì lớp Product không phải thay đổi gì. Đó chính là khái niệm Dependency Injection

Các kiểu Dependency Injection

Việc cài cắm đối tượng phụ thuộc vào một đối tượng khác được thực hiện qua mấy cách sau:

Constructor Injection - Cài cắm thông qua hàm tạo

Ở ví dụ trên chính là sử dụng kiểu Constructor Injection, với cách này có một số đặc điểm

  • Khi một lớp phụ thuộc cần một lớp triển khai để hoạt động thì sẽ sử dụng cách này để đảm bảo lớp đó được cung cấp cho đối tượng phụ thuộc.
  • Do truyền đối tượng qua hàm tạo, nên cần đảm bảo thành phần phụ thuộc này không bị thay thế trong suốt quá trình tồn tại của đối tượng
  • Sử dụng cách này khi kế thừa các lớp việc xử lý hàm tạo khá phức tạp

Setter Injection - Cài cắm thông qua hàm setter

Thành phần phụ thuộc được tiêm (truyền) vào đối tượng thông qua hàm setter. Ví dụ:

<?php

class Product {
    private $stockItem;
    private $code;

    public function __construct($code){
        $this->code        = $code;
    }

    public function getStockItem(){
        return $this->stockItem;
    }

    public function getCode(){
        return $this->code;
    }

    public function setStockItem(StockItem $stockItem){
        $this->stockItem = $stockItem;
    }
}

$stockItem = new StockItem(50, "Áo Dài");
$product = new Product("101010");
$product->setStockItem($stockItem);
var_dump($product->getStockItem());

Như vậy đối tượng phụ thuộc vào $stockItem được cài vào Product thông qua một hàm setter: setStockItem($stockItem). Với cách này:

  • Cho phép tùy chọn các phụ thuộc và lớp có thể tạo ra với các giá trị mặc định
  • Thêm các thành phần phụ thuộc một cách đơn giản thông qua hàm setter mà không làm hỏng logic của code.

Interface Injection - Cài cắm thông qua giao diện lớp

Với cách này định nghĩa một giao diện sao cho các thành phần phụ thuộc được tích hợp vào mã triển khai giao diện:

<?php

interface ProductInterface {
    public function getStockItem();
    public function setStockItem(StockItem $stockItem);
} 

Khi triển khai giao diện ProductInterface cần cung cấp StockItem thông qua định nghĩa hàm của giao diện

Dependency Injection Container - Trình chứa DI

Trên đây là toàn bộ cách cài cắm một đối tượng này vào đối tượng khác sao cho chúng không phụ thuộc vào nhau, với một đối tượng có một vài đối tượng phụ thuộc vấn đề sẽ không có gì, tuy nhiên khi lượng đối tượng phụ thuộc là lớn thì lại trở lên rất phức tạp để quản lý nó.

Lúc này giải pháp đưa ra cần tạo một trình chứa chuyên quản lý tất cả các đối tượng độc lập, từ autoload, khởi tạo, thiết lập và cài cắm vào đối tượng khác. Dependency Injection Container lại là một mô hình lập trình khác, tìm hiểu thêm qua PSR-11 Container Interface

Thư viện http://php-di.org/ là một thư viện chuyên về Dependency Injection Container, bạn có thể sử dụng trong dự án