Có thể nói, mẫu hình thiết kế Dependency Injection tạo thành xương sống của toàn bộ kiến trúc AngularJS. Nhờ mẫu hình thiết kế này mà các thành phần của AngularJS được tách rời cô lập nhau khi xây dựng, và tự động gắn kết lại khi vận hành. Việc này giúp giảm đáng kể công sức của các nhà phát triển dụng trong nỗ lực chia tách ứng dụng để dễ dàng phát triển và kiểm thử cũng như lắp ghép các thành phần lại thành ứng dụng hoàn chỉnh.

Nội dung trình bày

Dependency Injection trong AngularJS

Như đã đề cập trong phần giới thiệu, hệ thống Dependency Injection trong AngularJS đã tạo thành xương sống của toàn bộ khung ứng dụng này. Phần bài viết này sẽ giải thích cách thức hoạt động của hệ thống Dependency Injection trong AngularJS.

Các thuật ngữ quy ước

  • Inject (bơm truyền): khái niệm để chỉ hành động lấy giá trị hoặc tham chiếu của một đối tượng (giả sử là A), gán cho thuộc tính của một đối tượng khác (đối tượng B) theo một cách nào đó.
  • Injectable object (đối tượng có thể bơm truyền được): là những đối tượng được AngularJS tạo ra từ khai báo của người dùng, do AngularJS quản lý và có thể bơm truyền vào bên trong các hàm hoặc đối tượng khác.
  • Injected object (đối tượng được bơm truyền): là đối tượng có chứa một số chỗ đánh dấu (thường là tham số của hàm khởi tạo) để AngularJS có thể bơm truyền các đối tượng có thể bơm truyền được vào bên trong nó.
  • Provider (đối tượng cung cấp): là đối tượng do AngularJS tạo ra để chứa các định nghĩa về đối tượng dịch vụ. AngularJS cung cấp một số provider tạo sẵn cũng như cho phép nhà phát triển ứng dụng định nghĩa thêm các provider của riêng mình.
  • Service (đối tượng dịch vụ): khái niệm này được AngularJS sử dụng để nói đến những đối tượng do AngularJS tạo ra, quản lý, và có thể được bơm truyền vào cho những thành phần khác.
  • Instance of Service (thể hiện của đối tượng dịch vụ): khái niệm tương tự như đối tượng dịch vụ và được dùng lẫn lộn với khái niệm này trong AngularJS.

Lưu ý: hầu hết các đối tượng do AngularJS tạo ra và quản lý đều vừa là Injectable object lẫn Injected object. Một số đối tượng, chẳng hạn thuộc loại Value, Constant thì chỉ có thể là Injectable object. Một số đối tượng như các Directive thì chỉ có thể là Injected object.

Cấu trúc Dependency Injection trong AngularJS

Hai nhân tố cơ bản tạo dựng nên hệ thống Dependency Injection trong AngularJS chính là 2 đối tượng dịch vụ cơ bản trong module auto: $provide$injector.

Đối tượng dịch vụ $provide

Sơ đồ hoạt động của $provide

Đối tượng dịch vụ $provide có nhiệm vụ chỉ dẫn cho AngularJS cách thức tạo ra các đối tượng có thể bơm truyền được (injectable object) - các đối tượng này còn được gọi là đối tượng dịch vụ của AngularJS. Những chỉ dẫn của $provide được “ghi lại” dưới dạng một loại đối tượng được gọi chung là provider. Định nghĩa provider được thực hiện thông qua hàm provider() của đối tượng dịch vụ $provide. Câu hỏi đặt ra là $provide từ đâu ra và làm thế nào để sử dụng nó? Câu trả lời rất đơn giản: $provide được AngularJS tạo ra khi khởi tạo ứng dụng và chúng ta có thể lấy nó ra bằng cách gọi hàm config() của module.

var myModule = angular.module('myApp', []);
myModule.config(function($provide) {
  $provide.provider('myProvider', function() {
    this.$get = function() {
      return function(name) {
        alert("Hello, " + name);
      };
    };
  });
});

Đoạn mã trên tạo ra một provider có tên là myProvider. Với provider này, AngularJS sẽ chịu trách nhiệm: tạo ra một đối tượng dịch vụ (service) bằng cách gọi hàm $get() định nghĩa bên trong myProvider; bơm truyền service vừa tạo này vào bất cứ đối tượng có thể bơm truyền được (chẳng hạn là một Controller). Đối tượng có thể bơm truyền được là những đối tượng được tạo ra từ một provider nào đó và chứa tham số có tên myProvider trong hàm định nghĩa provider của nó. Ví dụ sau minh họa cách myProvider được sử dụng:

var injector = angular.injector(['myApp', 'ng']);

// injected object is a function
function myFunction(myProvider) {
  myProvider('Hung');
  return 1;
}
var result = injector.invoke(myFunction); // result contains 1

// injected object is a class
function myClass(myProvider) {
  this.myService = myProvider;
  this.sayHello = function() { 
    this.myService('Hung'); 
  }
  return this;
}
var object = injector.invoke(myClass);
object.sayHello();

// injected object is a controller
myModule.controller('myController', function($scope, myProvider) {
  $scope.onClick = function() {
    myProvider('Hung');
  };
});
Hàm factory()

Để đơn giản hóa mã lệnh định nghĩa provider trong hàm provider(), AngularJS cung cấp hàm khai báo factory() là phiên bản giản lược của provider(), bằng cách giản lược $provide.provider(‘myProvider’, function() { this.$get = function() { … }; }); thành $provide.factory(‘myProvider’, function() { … });. Với cách này, ta đã giảm bớt được một cấp khối lệnh trong khai báo.

myModule.config(function($provide) {
  $provide.factory('myProvider', function($window) {
    return function(name) {
      $window.alert("Hello, " + name);
    };
  });
});

Khai báo trên tương đương với khai báo đầy đủ sau:

myModule.config(function($provide) {
  $provide.provider('myProvider', function() {
    this.$get = function($window) {
      return function(name) {
        $window.alert("Hello, " + name);
      };
    };
  });
});

Để giản lược hơn nữa, AngularJS đã cung cấp thêm các hàm provider()factory() trong đối tượng Module, đơn giản hóa khai báo myModule.config(function($provide) { $provide.factory( … ); }); thành khai báo myModule.factory( … ); });, nhờ đó các hàm khai báo trên sẽ được viết lại ngắn gọn hơn rất nhiều:

myModule.factory('myProvider', function($window) {
  return function(name) {
    $window.alert("Hello, " + name);
  };
});

myModule.provider('myProvider', function() {
  this.$get = function($window) {
    return function(name) {
      $window.alert("Hello, " + name);
    };
  };
});
Hàm service()

Hàm service() cho phép tạo ra đối tượng bằng cách sử dụng hàm khai báo làm hàm khởi tạo đối tượng. Giá trị trả lại của service() không phải là kết quả thực thi hàm khai báo như trong factory() mà là kết quả áp dụng toán tử new đối với hàm khai báo đó.

myModule.service('myWallet', function($log) {
  var balance = 0;
  this.checkBalance = function () { return balance; };
  this.deposit = function (amount) { balance += amount; };
  this.withdraw = function (amount) { balance -= amount; };
});

Hàm service() cũng là trường hợp đặc biệt của provider(). Ví dụ trên tương đương với khai báo sau:

myModule.provider('myWallet', function() {
  this.$get = function($log) {
    return new function() {
          var balance = 0;
          this.checkBalance = function () { return balance; };
          this.deposit = function (amount) { balance += amount; };
          this.withdraw = function (amount) { balance -= amount; };
        }
    }();
  };
});
Hàm value()

Trong trường hợp muốn khai báo với AngularJS đối tượng, hàm hoặc giá trị mà không cần (đôi khi chúng ta không muốn) bơm truyền các đối tượng phụ thuộc, khi đó sử dụng hàm value() để khai báo.

myModule.config(function($provide) {
  $provide.value('myValue', function(name) {
    alert("Hello, " + name);
  });
});

Khai báo trên tương đương với khai báo sau:

myModule.config(function($provide) {
  $provide.provider('myValue', function() {
    this.$get = function() {
      return function(name) {
        alert("Hello, " + name);
      };
    };
  });
});

Phiên bản giản lược của khai báo này:

myModule.value('myValue', function(name) {
  alert("Hello, " + name);
});

Như đã đề cập ở bên trên, đối tượng dịch vụ tạo ra từ hàm value() sẽ không thể bơm truyền bất cứ thứ gì vào cho nó được. Tại sao lại như vậy? là vì trong AngularJS, các tham số tiếp nhận giá trị bơm truyền đều phải nằm ở khai báo hàm $get. Khai báo của hàm value() sẽ nằm ở bên trong một hàm $get mà hàm $get này AngularJS đã định nghĩa cứng là không có tham số.

Đối tượng dịch vụ $injector

Đối tượng dịch vụ $injector có nhiệm vụ tạo ra các đối tượng thể hiện của dịch vụ (instances of services) từ các khai báo provider được định nghĩa bởi $provide, đồng thời bơm truyền các đối tượng dịch vụ đã có vào cho thể hiện của đối tượng dịch vụ vừa mới tạo. Mỗi ứng dụng AngularJS có một đối tượng $injector được tự động tạo ra khi ứng dụng khởi động.

Sơ đồ hoạt động của $injector

Làm thế nào để lấy đối tượng $injector tương ứng với một ứng dụng? Chúng ta sử dụng hàm angular.injector(), với các tham số là danh sách module của ứng dụng cần nạp.

// create an injector
var $injector = angular.injector(['myApp', 'ng']);

Giống như các đối tượng dịch vụ của AngularJS như: $log, $window,… và các đối tượng dịch vụ do chúng ta tạo ra, $injector có thể được bơm truyền vào bất cứ hàm nào muốn có đối tượng này để thao tác:

// $injector is an injectable object!
myModule.controller('myController', function($scope, $injector, myProvider) {
  $scope.onClick = function() {
    // do something with $injector
    myProvider("AngularJS's users");
  };
});
Hàm $injector.get()

Khi đã có đối tượng $injector, chúng ta có thể lấy đối tượng thể hiện của các service đã được khai báo trước đó bằng cách sử dụng hàm $injector.get(). Nếu là lần đầu gọi đến, đối tượng thể hiện sẽ được $injector tạo mới, lưu lại và trả tham chiếu về cho người dùng. Trong những lần gọi thể hiện của đối tượng dịch vụ đó tiếp theo, $injector.get() sẽ tìm trong danh sách có sẵn và trả về tham chiếu đến đối tượng có sẵn cho người dùng. Như vậy, 2 lần gọi liên tiếp của hàm $injector.get() sẽ cho ra cùng một thể hiện của đối tượng dịch vụ.

var app = angular.module('myApp', []);

app.service('myWallet', function() {
  var balance = 0;
  this.checkBalance = function () { return balance; };
  this.deposit = function (amount) { balance += amount; };
  this.withdraw = function (amount) { balance -= amount; };
});

var injector = angular.injector(['myApp', 'ng']);

var wallet1 = injector.get('myWallet');
wallet1.deposit(20);
alert(wallet1.checkBalance()); // will display 20

var wallet2 = injector.get('myWallet');
wallet2.withdraw(5);
alert(wallet2.checkBalance()); // will display 15
Hàm $injector.invoke()

Đối tượng dịch vụ $injector còn có khả năng bơm truyền các thể hiện của đối tượng dịch vụ của AngularJS cho các hàm bên ngoài thông qua hàm $injector.invoke(). Ví dụ minh họa đã được trình bày trong phần “Đối tượng dịch vụ $provide” ở bên trên, ở đây chỉ xin nhắc lại:

var myModule = angular.module('myApp', []);

myModule.factory('myProvider', function() {
  return function(name) {
    alert("Hello, " + name);
  };
});

var injector = angular.injector(['myApp', 'ng']);

// injectable thing is a function
function myFunction(myProvider) {
  myProvider('Hung');
  return 1;
}
var result = injector.invoke(myFunction); // result contains 1

Rõ ràng là chúng ta có thể bơm truyền các thể hiện của đối tượng dịch vụ vào bên trong bất cứ hàm nào được kích hoạt bởi $injector.invoke(). Các hàm định nghĩa trong AngularJS được kích hoạt bằng $injector.invoke() (ngầm định bởi AngularJS) cũng có khả năng được bơm truyền. Các hàm này bao gồm:

  • Các hàm định nghĩa controller
  • Các hàm định nghĩa directive
  • Các hàm định nghĩa filter
  • Các hàm định nghĩa factory
  • Các hàm định nghĩa service
  • Các hàm $get trong định nghĩa provider

Hàm khai báo value luôn trả lại một giá trị tĩnh (static value), do đó chúng không được kích hoạt thông qua $injector.invoke(), do đó không thể bơm truyền bất cứ thứ gì vào cho chúng.

Chú giải phụ thuộc (Dependency Annotation)

Trong tất cả các ví dụ minh họa ở trên, chúng ta đều quy ước các tham số muốn được bơm truyền thể hiện của đối tượng dịch vụ đều phải có tên trùng với tên khai báo provider của service đó. Việc đặt tên tham số tiếp nhận injection trùng với tên provider là để chỉ chỗ (một cách “ngầm hiểu” với nhau) cho AngularJS rằng tôi muốn được bơm truyền đối tượng dịch vụ ứng với provider trùng tên vào chỗ này đây. Những quy định về cách đặt tên nhằm chỉ chỗ cho AngularJS biết chỗ để bơm truyền được gọi là chú giải phụ thuộc. AngularJS hỗ trợ 3 cách chú giải khác nhau nhằm giúp cho người lập trình có sự lựa chọn phù hợp trong quá trình viết ứng dụng.

Chú giải suy diễn (Annotation by Inference)

AngularJS giả định rằng nếu không có khai báo nào khác, thì tên tham số của hàm cấu tạo đối tượng (constructor) chính là tên của dependency. Do đó, nó sẽ phân tích và trích rút tên của các tham số của hàm cấu tạo đối tượng, sau đó sử dụng $injector để xác định các dependency có tên ở trên, rồi “truyền” (inject) các dependency này vào các tham số tương ứng trong lời gọi hàm khi khởi tạo đối tượng.

// using annotation by inference
myModule.factory('myActivity', function($http, myProvider, myWallet) {
  return function() {
    // do something with $http
    myWallet.deposit(20);
    myProvider(myWallet.checkBalance());
  };
});

Lưu ý: Do sử dụng tên đầy đủ của tham số như là tên của dependency cần xác định, nên chú giải suy diễn chỉ làm việc với mã lệnh chưa bị tối giản (non-minified), chưa bị làm rối (non-obfuscated). Tại sao như vậy? lý do rất đơn giản là các bộ tối giản mã (JavaScript Minifier) hoặc làm rối mã (Javascript Obfuscator) đều tự động đổi tên các tham số của hàm sao cho tên những tham số có số lượng ký tự tối thiểu (thường chỉ có 1-2 ký tự), trong khi tên của dependency không bị đổi hoặc bị đổi thành một chuỗi khác.

Do hạn chế không thể sử dụng khi mã nguồn được tối giản nên chú giải suy diễn chỉ được sử dụng trong trường hợp viết mã lệnh nhanh, không cần tối ưu như viết ví dụ minh họa, lập trình các ứng dụng nhỏ không cần tối giản mã lệnh.

Chú giải tường minh (Explicit Annotation)

AngularJS cung cấp cho chúng ta một phương thức để xác định tường minh các dependency cần dùng đến khi gọi một hàm khởi tạo: khai báo thuộc tính $inject của hàm khởi tạo, chứa danh sách tên các dependency. Quá trình xử lý Dependency Injection sẽ sử dụng thuộc tính chú giải $inject để xác định các dependency cần cung cấp cho lời gọi hàm khởi tạo đối tượng.

// using explicit annotation
var myActivityFactory = function(xhttp, varProvider, varWallet) {
  return function() {
    // do something with xhttp
    varWallet.deposit(20);
    varProvider(varWallet.checkBalance());
  };
};
myActivityFactory.$inject = ['$http', 'myProvider', 'myWallet'];
myModule.factory('myActivity', myActivityFactory);

Phương thức này cho phép các bộ tối giản mã hoặc các bộ làm rối mã có thể đổi tên các tham số của các hàm mà vẫn đảm bảo xác định đúng và truyền chính xác các dependency cho các hàm đó.

Chú giải nội tuyến (Inline Annotation)

Phương thức chú giải thứ ba mà AngularJS cung cấp cho chúng ta chính là chú giải nội tuyến (Inline Annotation). Cách hoạt động của loại chú giải này cũng giống với chú giải tường minh, tuy nhiên nó cho phép khai báo danh sách tên dependency nội tuyến bên trong định nghĩa hàm khởi tạo.

// using inline annotation
myModule.factory('myActivity', ['$http', 'myProvider', 'myWallet', 
  function(xhttp, varProvider, varWallet) {
    return function() {
      // do something with xhttp
      varWallet.deposit(20);
      varProvider(varWallet.checkBalance());
    };
  }
]);

Về mặt cú pháp, chú giải nội tuyến cho phép truyền một mảng thay vì một hàm trong phần khai báo hàm khởi tạo đối tượng của AngularJS. Các phần tử của mảng là danh sách tên của các dependency cần truyền cho tham số hàm khởi tạo, phần tử cuối của mảng chính là khai báo hàm khởi tạo đối tượng. Cách khai báo này giúp chúng ta tránh được việc sử dụng thêm một thuộc tính trong việc khai báo hàm khởi tạo.

Ví dụ trực quan minh họa Injection

Nếu có sự so sánh giữa Dependency Injection trong lập trình với Injection trong y tế (Medical Injection), chúng ta sẽ thấy có sự tương đồng khá thú vị. Phần này sẽ trình bày một ví dụ nhỏ, minh họa cho sự tương đồng đó.

Giả sử có một bệnh nhân (patient), được điều trị tại bệnh viện (hospital). Anh ta tham gia vào một quá trình điều trị (treatment), trong đó được điều dưỡng (nurse) tiêm 2 loại vacxin tên là vaccineXvaccineY. vaccineX có tác dụng điều trị căn bệnh X của anh ta, còn vaccineY có chức năng ngăn ngừa căn bệnh Y. Khi đó, quá trình thực hiện này có thể được minh họa thông qua đoạn chương trình sau:

var $hospital = angular.module('Hospital', []);

$hospital.factory('vaccineX', function($window) {
  return {
    isInjected: function(name) {
      $window.alert("The VaccineX was injected into patient " +
          "to cure the disease X.");
    }
  };
});

$hospital.factory('vaccineY', function($window) {
  return {
    isInjected: function(name) {
      $window.alert("The VaccineY was injected into patient " +
          "to prevent the disease Y.");
    }
  };
});

$hospital.factory('patient', ['vaccineX', 'vaccineY', 
    function(biceps, buttock) {
        return {
            treat: function() {
                biceps.isInjected();
                buttock.isInjected();
            }
        }
    }
]);

var $nurse = angular.injector(['Hospital', 'ng']);

var Treatment = function(patient) {
    patient.treat();
}

$nurse.invoke(Treatment);

Đoạn mã lệnh trên có thể minh họa trực quan bằng hình vẽ bên dưới. Đối chiếu hình vẽ và mã lệnh bên trên, bạn thấy Dependency Injection còn khó hiểu nữa hay không?

Sơ đồ minh họa trực quan khái niệm Injection

Tài liệu tham khảo

Comments