11/08/2018, 20:55

[Quán cơm bình dân] Thực đơn số 1: Cơm bình dân Dependency Injection - Phần 1

Cuối tuần mình có thư giãn bằng cách đọc hiểu và ứng dụng một chút về dependency injection . Cảm thấy khá thấm nên muốn chia sẻ cho các bạn về những gì mình nắm được, chúng ta có thể trao đổi và thảo luận. Mình cũng nảy ra ý tưởng sẽ viết các log có tính chất bình dân hóa về tin học, các kỹ ...

Cuối tuần mình có thư giãn bằng cách đọc hiểu và ứng dụng một chút về dependency injection. Cảm thấy khá thấm nên muốn chia sẻ cho các bạn về những gì mình nắm được, chúng ta có thể trao đổi và thảo luận.
Mình cũng nảy ra ý tưởng sẽ viết các log có tính chất bình dân hóa về tin học, các kỹ thuật lập trình, các kiến thức hay ý tưởng mới, các log này mang tính chất ứng dụng thực tế nhiều hơn trong sự nghiệp đi cày code kiếm cơm hay trên con đường mưu cầu code đạo của anh em.

Các bài log này sẽ có ngôn ngữ rất chi là gần gũi và dễ hiểu dành cho mọi lứa tuổi, mọi cấp độ cũng như mọi kinh nghiệm. Trong bài viết có thể có một số thuật ngữ, có thể mình sẽ không giải thích hết được thì các bạn vui lòng search google nhé.

Ý tưởng viết Quán cơm bình dân này nảy ra khi mình tìm hiểu về dependency injection - mấy bài viết trên mạng chỉ cho mình cách thực hiện nhưng hầu hết là dịch từ nước ngoài, và cách hiểu cũng khá nôm na không rõ ràng, chủ yếu sử dụng trong các framework hỗ trợ sẵn dependency injection, trong khi CodeIgniter thì không có. Vậy là mình quyết định tìm hiểu sâu hơn với mục đích làm sao để đưa CodeIgniter về framework hỗ trợ khai báo dependency trong controller.

Mình sẽ có một bài log ngay sau bài này về việc đưa 1 thư viện hỗ trợ code dependency injection vào trong CodeIgniter 3.0.

Và đây sẽ là bài đầu tiên trong series Quán cơm bình dân - chủ bút kiendt. :trollface: Bài khá dài nên hi vọng các bạn theo dõi được hết.

Nội dung chính

Nội dung chính của thực đơn ngày hôm nay là giới thiệu về dependency injection, cách chế biến dependency injection trong code thực tế, giải thích về cơ chế bên trong framework "Làm thế nào để framework có thể cho phép bạn dependency injection" từ đó tiến tới tự xây dựng một tool hỗ trợ chúng ta code theo phong cách dependency injection.

Về dependency injection

Đầu tiên là viết tắt. Người ta thích viết tắt dependency injection là DI. Vì thế trong bài viết này mình cũng dùng DI để chỉ dependency injection cho ngắn gọn.
Vậy DI dịch sang nghĩa tiếng việt là gì: Dependency = sự phụ thuộc, Injection = tiêm nhiễm. Dịch ra tiếng Việt là khai báo sự phụ thuộc, khai báo phụ thuộc hàm. MÌnh thì bình dân hóa nó đơn giản là khai báo thư viện (library) sử dụng trong hàm - mặc dù cách hiểu này hoàn toàn không chính xác nhưng nghe đỡ hàn lâm hơn chữ phụ thuộc
Các bạn có thể xem ví dụ này để thấy rõ hơn vì sao mình lại định nghĩa DI là khai báo thư viện

mainApp.controller('MyController', function($scope, MyService) {
    $scope.number = 2;
    $scope.result = MyService.function_a($scope.number);

    $scope.my_function = function() {
        $scope.result = MyService.function_b($scope.number);
    }
});

Đoạn code trên là của AngularJS - một framework JS của Google. Cách khai báo $scope, MyService phía trên được gọi là khai báo sự phụ thuộc, tức là anh MyController muốn dùng một hàm nào đó của MyService, hay nói rộng ra là muốn dùng tài nguyên nào đó do MyService điều khiển thì phải đính kèm nó trong lời gọi khởi tạo (constructor). Như vậy tức là MyController bị lệ thuộc, phụ thuộc vào $scope, MyService ở trên. Và đó là yếu tố dependency. Cũng từ ví dụ này mà mình đã định nghĩa DI là khai báo thư viện cần sử dụng trong lời gọi khởi tạo class - mang một ý nghĩa đơn giản là: nếu bạn muốn dùng một tiện ích (utility), một thư viện (library), hay một dịch vụ (service) trong class của bạn thì bạn phải khai báo nó.

Vậy thiếu gì cách khai báo mà phải khai báo trong hàm khởi tạo

Đúng là như thế, có nhiều cách để khai báo thư viện (khai báo phụ thuộc) tuy nhiên để theo chuẩn DI thì nên khai báo ở trong hàm constructor của class. Các bạn hãy xem các ví dụ về khai báo thư viện sử dụng theo các cách khác.

Các ví dụ mình sử dụng là code PHP nhưng nếu bạn có kiến thức về lập trình thì có lẽ đọc cũng dễ hiểu :)

Tình huống: Giả sử chúng ta có một class dùng để tính toán chi phí công tác của nhân viên trong công ty. Chi phí thì bao gồm chi phí đi lại, chi phí ở khách sạn...

Ta có 2 class sau

/* 
class phụ - dùng để tính tiền vé đi lại - class này được coi như 1 library, hay 1 service cũng được. Class chính sẽ "phụ thuộc" vào class này, dựa vào class này để tính chi phí cho 1 nhân viên cụ thể.
*/

class PlaneCost
{
    private $brand_name;
    private $distinct;
    public function __construct($brand_name, $distance) {
        $this->brand_name = $brand_name;
        $this->distinct = $distinct;
    }
    public function cost() {
        //... tính toán tiền vé dựa trên hãng và khoảng cách
        return 20000;
    }
}
//class chính phục vụ việc tính toán chi phí của nhân viên
class BusinessCost
{

    public function __construct() {
        //... 
      $this->count_trip_cost();
    }

    public function count_trip_cost() {
        //giả sử đi máy bay, Vietnam Airline, quãng đường 2000km
        $plan_cost = new PlaneCost('Vietnam Airline',2000);
        return $plan_cost->cost();
    }
}


Thực tế thì anh em có kinh nghiệm sẽ không fix cứng code như trên, tuy nhiên đây là ví dụ mình đưa ra để minh họa. Hi vọng không bị soi quá :D

Triển khai DI trong thực tế và lợi ích khi sử dụng DI.

Trong ví dụ trên ta đã thấy sự phụ thuộc của BusinessCost vào PlaneCost, tuy nhiên sự phụ thuộc chưa thể hiện chặt chẽ. Thực tế thì ta hay làm thế này hơn.

    //Class BusinessCost
    //khai báo biến lưu chi phí đi lại
    private $trip_cost;
    //khai báo biến chi phí ăn ở và các chi phí cá nhân khác
    private $personal_cost;

    public function __construct() {
        //giả sử đi máy bay, hãng Vietnam Airline, quãng đường 2000km
        $this->trip_cost = new PlaneCost('Vietnam Airline',2000);
    }

    public function count_trip_cost() {
        return $this->trip_cost->cost();
    }

Ở đây ta đã kéo sự phụ thuộc của BusinessCost đối với PlaneCost thành 1 thuộc tính là trip_cost. Như vậy code chúng ta có tách các bộ phận ra tính toán riêng rồi khai báo phụ thuộc vào class chính sẽ có độ logic cao hơn và khả năng mở rộng cũng dễ dàng hơn. Tuy nhiên đây là code minh họa về sự phụ thuộc lẫn nhau của 2 class thôi chứ không ai lại đi fix cứng logic trong class thế kia :D. Thực tế ta thường code theo những mẫu dưới này.:thumbsdown:

Và đây là một vài mẫu triển khai thực tế việc khai báo sự phụ thuộc trong một số ứng dụng mà ta hay làm.
Mẫu 1:

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() {
        //...
    }
    public function set_trip($brand_name,$distance) {
        $this->trip_cost = new PlaneCost($brand_name,$distance);
    }
    public function count_trip_cost() {
        return $this->trip_cost->cost();
    }
}
//sử dụng ngoài ứng dụng
$business_cost = new BusinessCost();
$business_cost->set_trip('Vietnam Airline', 2000);
echo $business_cost->count_trip_cost();

Mẫu 2:

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(PlaneCost $trip_cost) {
        //...
        $this->trip_cost = $trip_cost;
    }

    public function count_trip_cost() {
        return $this->trip_cost->cost();
    }
}
//khai báo để sử dụng
$trip_cost = new PlaneCost('Vietnam Airline', 2000);
$business_cost = new BusinessCost($trip_cost);
echo $business_cost->count_trip_cost();

Cả 2 mẫu trên đều thể hiện sự phụ thuộc của class BusinessCost với class PlaneCost.
Với mẫu 1, xem ra không có vấn đề tuy nhiên thực tế khi sử dụng chúng ta phải nhớ tới hàm set_trip mà thực tế hàm này chả liên quan lắm đến logic nhiệm vụ của BusinessCost, và muốn sử dụng được thì phải gọi hàm ra để dùng.
Với mẫu 2, mọi chuyện có vẻ khá hơn khi chúng ta đưa việc set trip vào trong constructor, tuy vậy thì nó chỉ đỡ được khoản gọi hàm khi sử dụng. Thực tế code của tôi hiện nay (và có lẽ không ít trong số các bạn) vẫn còn chứa khá nhiều class viết kiểu khai báo này. Nó sẽ là ổn nếu ứng dụng không có sự thay đổi logic đáng kể nào.
Tuy nhiên điều gì sẽ xảy ra nếu có thay đổi về logic bên trong: khi công ty quyết định cắt giảm chi phí bằng cách cho nhân viên đi tàu hỏa thay vì máy bay. Lúc này ứng dụng của chúng ta sẽ phải code lại kha khá, đầu tiên là ở phần sử dụng. Sau đó là vào class Business để sửa lại thành phần dependency thành RailwayCost

class RailwayCost
{
    private $brand_name;
    private $distinct;
    public function __construct($brand_name, $distance) {
        $this->brand_name = $brand_name;
        $this->distinct = $distance;
    }
    public function cost() {
        //... tính toán tiền vé dựa trên hãng và khoảng cách
        return 19000;   //tiết kiệm được 5% =))
    }
}

class BusinessCost
{
    //...
    public function __construct(RailwayCost $trip_cost) {
        //...
    }
    //...
}

Đó là với class ví dụ, ta sửa có vẻ ít nhưng thực tế các class của chúng ta phức tạp hơn và thường làm cho ta mất thời gian hơn nhiều.
Và đây chính là lúc ta cần áp dụng DI - Dependency Injection kết hợp với 1 nguyên lý trong SOLID tương ứng với chữ D.

Nguyên tắc thứ 5 của SOLID và kỹ thuật DI.

SOLID là nguyên lý code giúp cho việc bảo trì, nâng cấp ứng dụng không trở thành ác mộng với lập trình viên. Tìm hiểu thêm ở link này. SOLID là gì? Áp dụng các nguyên lý SOLID để abc, bla bla....
Chữ D cuối cùng trong 5 nguyên lý SOLID là: Dependency inversion principle (Đảo ngược sự phụ thuộc)

  1. 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.
  2. 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.)

Mình không diễn giải sâu thêm về SOLID, các bạn có thể đọc thêm về nó trong link ở trên nhưng đảm bảo khó hiểu hơn bài này :stuck_out_tongue_closed_eyes:
Đây không đơn giản là một cách code, một khái niệm mà nó còn là một kiến trúc lập trình, một mô hình thiết kế theo mẫu (design pattern). Và kỹ thuật DI cùng nguyên lý số 5 này đã được áp dụng ở rất nhiều framework mới như Yii2, Symfony2, Zend2, Laravel...

Trong các framework này ở phần controller các bạn có thấy họ khai báo các service hay thư viện vào hàm khởi tạo controller không?

Laravel

class BlogController extends Controller {
    /**
     * The BlogRepository instance.
     * @var AppRepositoriesBlogRepository
     */
    protected $blog_gestion;
    /**
     * The UserRepository instance.
     * @var AppRepositoriesUserRepository
     */
    protected $user_gestion;

    public function __construct(BlogRepository $blog_gestion, UserRepository $user_gestion)
    {
        //...
    }
}

Không biết có bạn nào thắc mắc là họ khởi tạo controller thì sao biết mà truyền vào BlogRepository hay UserRepository như mình làm khi cấp phát PlaneCost ở ví dụ trên nhỉ? Mình cũng thắc mắc vì theo tư duy MVC của mình thì có mô hình này

Mô hình MVC truyền thống

alt text

Và khi tìm hiểu kỹ thuật DI thì mình mới phát hiện ra bí mật của các framework khi ứng dụng DI vào trong code base của họ. Đó là họ sử dụng 1 IoC container để khởi tạo các object của toàn bộ ứng dụng, từ đó cấp phát các service hay thư viện tương ứng khi object nào đó khai báo rằng tôi cần service này để cho ra kết quả.

Mô hình MVC hỗ trợ DI

Tức là mô hình MVC mới sẽ được chi tiết hơn như sau:
alt text

Nhìn vào hình trên các bạn đã thấy vị trí của IoC container trong ứng dụng MVC rồi đó. Nó nằm giữa router và controller, dùng để khởi tạo object. Nên thay vì đoạn code trong các ứng dụng MVC cũ mà ta vẫn nghĩ (đại diện có ông CodeIgniter)

$router = Router("/blog/edit/2");
// --- tính toán 1 thôi 1 hồi để rút ra tên `controller`, tên `method` của `controller` và tham số của `method`
$controller = "BlogController";
$method = "edit";
$params = array(2);

//khởi tạo controller
$ctrl = new $controller();
//gọi hàm theo yêu cầu từ router
$ctrl->$method($params);

thì các framework hỗ trợ DI sẽ làm như sau:

//cũng phân tích router các kiểu con đà điểu để ra được tên controller cần gọi và tên method được gọi tới
$ioc_container = new IoC_container();
//tạo 1 instance của controller
$ctrl = $ioc_container->get($controller);
$ctrl->$method($params);

Trong Laravel thì biến ioc_container này chính là biến $app, xem file bootstrap.php là biết.

Rõ ràng theo cách gọi thông thường khi ta khởi tạo controller băng dòng lệnh $ctrl = new $controller(); thì không thể nào cấp phát được các dependency. Vì vậy cần phải có 1 class giúp ta việc này, và cấp phát tự động hoàn toàn, dựa vào việc đọc code php của controller, tìm xem controller khai báo service nào thì lôi ra cấp phát, tạo instance của service rồi nhét vào instance của controller. Bên trong của cái IoC_container này sẽ là quá trình như sau:

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 [];
    }
}

Lưu ý trên đây là mã giả, thực tế phải xử lý nhiều hơn thế :D. Tuy nhiên hiểu nôm na cấu trúc của IoC_container là vậy.
Thêm nữa IoC_container là một design pattern. Và cái vừa tạo trên đây chỉ là một cách triển khai pattern này với ứng dụng cụ thể là kỹ thuật DI. Ngoài ra thì IoC_container còn nhiều ứng dụng trong nhiều pattern khác nữa chứ không riêng gì DI. Nên chính xác hơn thì class trên có tên là DI_container (implement IoC_container nào đó).

Còn tiếp phần 2.

Đến đây thì hi vọng các bạn đã hiểu sâu hơn về kỹ thuật DI cũng như cách mà các framework khác sử dụng DI_Container trong ứng dụng của họ. Vì bài viết khá dài nên mình sẽ tách làm 2 phần. 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.

Ủng hộ mình bằng cách kipalog, giúp đỡ mình bằng cách comment các bạn nhé.

Cập nhật phần 2 tại link http://kipalog.com/posts/Quan-com-binh-dan--Thuc-don-so-1--Com-binh-dan-Dependency-Injection---Phan-2

0