Javascript Design pattern: module pattern - CommonJS
Sau một thời gian tương đối dài làm việc với javascript, tôi nhận thấy tầm quan trọng của việc áp dụng các design pattern vào trong việc thiết kế tổng thể của dự án. Phải thú nhận là tôi đã chú ý và quan tâm tới việc áp dụng design pattern từ lâu, nhưng đã không thể áp dụng được vào dự án vì nhiều ...
Sau một thời gian tương đối dài làm việc với javascript, tôi nhận thấy tầm quan trọng của việc áp dụng các design pattern vào trong việc thiết kế tổng thể của dự án. Phải thú nhận là tôi đã chú ý và quan tâm tới việc áp dụng design pattern từ lâu, nhưng đã không thể áp dụng được vào dự án vì nhiều lý do: tính chất gấp gáp của dự án, chưa có kinh nghiệm thực tế làm việc với dự án đủ lớn và có độ phức tạp cao... Nên sau dự án lần này, tôi muốn tìm hiểu kỹ hơn và áp dụng design pattern vào trong những dự án sắp tới.
Để áp dụng được vào công việc ngay, tôi chọn những pattern có tính ứng dụng cao, mang tính nền tảng để có cái nhìn tổng thể về luồng chương trình. Hôm nay tôi sẽ chia sẻ về module pattern.
Trong kiến trúc tổng thể của bất kỳ ứng dụng mạnh mẽ nào, bạn sẽ thấy đều có sự xuất hiện của các modules, đóng vai trò là các thành phần tích hợp. Chúng giúp việc quản lý các đoạn mã rõ ràng, và có tổ chức. Trong thế giới javascript, bạn có vài lựa chọn sau để module hóa ứng dụng như Module pattern, Object literal, AMD modules, CommonJS modules hay ECMAScript Harmony modules.
Sau đây là các pattern được áp dụng trong Module pattern:
Anonymous Closures
Closures là một trong những tính năng tuyệt vời của javascript, nó đóng vai trò cơ bản giúp tạo ra Module pattern. Nó chỉ bao gồm hai bước, tạo ra một hàm vô danh, sau đó thực thi nó ngay. Tất cả mã nguồn được thực thi trong hàm đó được gói gọn trong một closure, tại đây các trạng thái có thể được duy trì một cách private, cho đến khi chúng ta trả lại thông qua lời gọi return.
(function () { // ... tất cả các biến và hàm trong đây sẽ được che dấu và không thể truy xuất được từ bên ngoài // nhưng từ đây, vẫn có thể gọi được các biến và hàm được định nghĩa ở bên ngoài }());
Hãy chú ý đến () bao quanh cả hàm anonymous của chúng ta, đều này là bắt buộc vì từ khóa function khi đứng riêng sẽ được coi là một định nghĩa hàm, tuy nhiên khi đặt trong (), nó sẽ trở thành một mô tả hàm.
Global Import
Javascript có một tính năng được biết đến là implied globals, khi một biến được sử dụng, bộ biên dịch sẽ tìm kiếm tuần tự từ scope của nó, rồi trở ngược lên các scope phía trên, cho đến câu lệnh chỉ rõ biến đó được khai báo bởi var. Trong trường hợp không tìm thấy, biến đó được coi là global. Điều này dẫn đến nguy cơ conflict mã nguồn cao và việc quản lý mã nguồn sẽ rất khó khăn.
Để khắc phục tình trạng này, chúng ta truyền các biến globals vào lời gọi hàm anonymous, nó sẽ hoạt động như thể được importa vào trong closure scope. Cách này vừa rõ ràng, lại nhanh hơn so với implied globals.
(function ($, YAHOO) { // ở đây chúng ta có thể gọi các biến jQuery bằng $ hay YAHOO }(jQuery, YAHOO));
Module Export
Vừa rồi chúng ta đã xem cách sử dụng biến globals, còn nếu muốn tạo ra thì sao? câu trả lời là sử dụng return.
var MODULE = (function () { var my = {}, privateVariable = 1; function privateMethod() { // ... } my.moduleProperty = 1; my.moduleMethod = function () { // ... }; return my; }());
Augmentation
Hiện tại, giới hạn của Module pattern là toàn bộ mã nguồn phải được viết trong 1 file. Rõ ràng nó sẽ không dễ dàng khi làm việc với hàng ngàn đoạn mã chỉ trong 1 file. Giải pháp ở đây là sử dụng augment modules. Đầu tiên là import module, rồi thêm các thuộc tính, cuối cùng là export. Chúng ta sẽ xem qua ví dụ sau:
var MODULE = (function (my) { my.anotherMethod = function () { // added method... }; return my; }(MODULE));
Sau khi thực thi, một hàm mới anotherMethod đã được thêm vào MODULE, không những thế, việc augmentation vẫn sẽ đảm bảo duy trì các trạng thái private cho chúng ta.
Loose Augmentation
Ở ví dụ trên thì chúng ta cần có trước MODULE, rồi sẽ augment nó sau, nhưng thực tế mà nói, không phải lúc nào chúng ta cũng có MODULE trước. May mắn thay, javascript làm rất tốt những tác vụ đòi hỏi thực thi bất đồng bộ. Nên chúng ta có thể tạo ra các modules, load chúng theo thứ tự với loose augmentation. Mỗi file sẽ có cấu trúc như sau:
var MODULE = (function (my) { // các logic... return my; }(MODULE || {}));
Ở ví dụ trên, việc import sẽ tự động tạo ra object nếu module chưa tồn tại. Và nhờ đó bạn có thể sử dụng các công cụ như LABjs và load tất cả các modules cùng một lúc.
Tight Augmentation
Loose augmentation cũng có những giới hạn của nó. Quan trọng nhất là việc không thể override các thuộc tính của module mà không gây ra lỗi. Ngoài ra là việc không thể sử dụng các thuộc tính module từ các file khác trong quá trình khởi tạo. Tight augmentation sẽ cho phép override. Đây là một ví dụ đơn giản:
var MODULE = (function (my) { var old_moduleMethod = my.moduleMethod; my.moduleMethod = function () { // method được override, và chung ta vẫn có thể gọi đến old_moduleMethod }; return my; }(MODULE));
Mặc dù chúng ta override MODULE.moduleMethod, một reference đến old_moduleMethod vẫn được tạo ra trong trường hợp cần thiết.
Cloning and Inheritance
var MODULE_TWO = (function (old) { var my = {}, key; for (key in old) { if (old.hasOwnProperty(key)) { my[key] = old[key]; } } var super_moduleMethod = old.moduleMethod; my.moduleMethod = function () { // override hàm của module clone nhưng vẫn có thể gọi được đến old.moduleMethod }; return my; }(MODULE));
Pattern này có một nhược điểm là nếu các thuộc tính là object hoặc hàm thì việc clone là không triệt để, nhiều hơn một object sẽ cùng trỏ đến một object hoặc hàm. Khi một clone thay đổi, nó sẽ dẫn đến thay đổi ở tất cả các reference khác. Chúng ta có thể khắc phục được phần nào bằng recursive cloning, nhưng sẽ khá phức tạp.
Cross-File Private State
Một giới hạn lớn của việc định nghĩa module trong nhiều file là các thuộc tính không thể truy cập lẫn nhau. Ví dụ sau đây có thể khắc phục được điều đó:
var MODULE = (function (my) { var _private = my._private = my._private || {}, _seal = my._seal = my._seal || function () { delete my._private; delete my._seal; delete my._unseal; }, _unseal = my._unseal = my._unseal || function () { my._private = _private; my._seal = _seal; my._unseal = _unseal; }; // permanent access to _private, _seal, and _unseal return my; }(MODULE || {})); Các file module sẽ thiết lập các thuộc tính thông qua biến `_private`. Nó sẽ lập tức có thể truy xuất được tại các file khác. Khi module này kết thúc việc load, chúng ta chỉ cần gọi MODULE._seal(), để đảm bảo các thuộc tính được giữ private. Trong trường hợp module này được augmentation một lần nữa, chúng ta chỉ cần gọi `_unseal()` và sau đó là `_seal()`.
Kết luận
Để có thể xây dựng được một ứng dụng mạnh mẽ, chúng ta không thể bỏ qua việc áp dụng đúng và kết hợp các pattern được nêu trên.