Hầu hết các developer đều mong muốn có thể viết được code có khả năng bảo trì, dễ đọc và có thể sử dụng được lại. Cấu trúc của source code trở nên vô cùng quan trọng khi ứng dụng ngày càng phình to hơn. Design pattern đã chứng tỏ được tầm quan trọng giúp chúng ta giải quyết những thách thức như thế này – cung cấp một cơ cấu tổ chức giúp giải quyết các vấn đề thường gặp trong trường hợp cụ thể.
Các web Developer Javascript thường xuyên phải tương tác với các Design Pattern, thậm chí là vô tình, trong khi xây dựng các ứng dụng.
Mặc dù có nhiều các Design Pattern khác nhau được sử dụng trong các trường hợp cụ thể, nhưng các developer có xu hướng chỉ sử dụng một vài Pattern phổ biến nhiều hơn các Pattern khác.
Trong bài viết này chúng ta sẽ chỉ tập trung vào các Pattern thông dụng để giúp cải thiện chất lượng code của bạn và tìm hiểu sâu hơn về Javascript.
Các Design Pattern sẽ được đề cập đến là:
- Module
- Prototype
- Observer
- Singleton
Mỗi Pattern bao gồm nhiều thuộc tính, tuy nhiên, chúng ta sẽ chỉ nhấn mạnh vào những điểm chính sau đây:
- Bối cảnh – Trường hợp / dưới hoàn cảnh nào thì Pattern được sử dụng?
- Vấn đề – Chúng ta cố gắng giải quyết vấn đề nào?
- Giải pháp – Sử dụng Pattern này như thế nào để giải quyết vấn đề được đề xuất?
- Thực hiện – Triển khai thực hiện như thế nào?
Module
Module là Design Pattern phổ biến nhất được sử dụng để duy trì các phần code cụ thể độc lập, không phụ thuộc vào các thành phần khác. Điều này cung cấp các liên kết tách rời, giúp cho code có cấu trúc tốt
Đối với những người đã quen với các ngôn ngữ hướng đối tượng, Module là các “class” trong Javascript. Một trong những ưu điểm của class là tính đóng gói – kiểm soát các trạng thái và các xử lý đang được truy cập từ các class khác. Module Pattern cho phép các cấp truy cập là public hay private (bao gồm các đặc quyền hay giới hạn truy cập đến đâu)
Các Module nên là ngay lập tức (public) – gọi được – hàm – biểu thức giúp cho phép giới hạn truy cập private, nghĩa là, khép kín để bảo vệ các phương thức và các biến (Tuy nhiên nó sẽ trả về object thay cho function). Code sẽ trông giống như thế này:
(function() { // Khai báo các biến và/hoặc các hàm là private return { // Khai báo các biến và/hoặc các hàm là public } })();
Ở đây chúng ta khởi tạo các biến và/hoặc các hàm là private trước khi trả về object mà chúng ta mong muốn. Các Code bên ngoài sẽ không thể truy cập đến các biến private vì nó không nằm trong cùng phạm vi. Chúng ta hãy cụ thể hoá hơn như sau:
var HTMLChanger = (function() { var contents = 'contents' var changeHTML = function() { var element = document.getElementById('attribute-to-change'); element.innerHTML = contents; } return { callChangeHTML: function() { changeHTML(); console.log(contents); } }; })(); HTMLChanger.callChangeHTML(); // Outputs: 'contents' console.log(HTMLChanger.contents); // undefined
Lưu ý rằng hàm callChangeHTML
được public trong object trả về và nó có quyền truy cập / gọi đến hàm changeHTML
. Tuy nhiên, nếu bên ngoài Module, biến content sẽ không thể truy cập được.
Revealing Module Pattern
Mục đích của nó là giúp duy trì đóng gói và chỉ đưa ra chính xác các biến và các phương thức được trả về trong object literal. Chúng ta sẽ thực hiện nó như sau:
var Exposer = (function() { var privateVariable = 10; var privateMethod = function() { console.log('Inside a private method!'); privateVariable++; } var methodToExpose = function() { console.log('This is a method I want to expose!'); } var otherMethodIWantToExpose = function() { privateMethod(); } return { first: methodToExpose, second: otherMethodIWantToExpose }; })(); Exposer.first(); // Output: This is a method I want to expose! Exposer.second(); // Output: Inside a private method! Exposer.methodToExpose; // undefined
Mặc dù, Code của chúng ta trông sạch sẽ hơn nhiều, nhưng rõ ràng là nó vẫn bất tiện trong việc truy cập đến các phương thức Private. Điều này có thể gây khó khăn trong việc Unit Test. Tương tự, chúng ta không thể ghi đè các xử lý public
Prototype
Bất kỳ Developer Javascript nào cũng đã từng nhìn thấy từ khoá Prototype, gặp rắc rối trong việc kế thừa Prototype, hay thực hiện các Prototype trong code của mình. Prototype Design Pattern dựa trên Javascript prototypical inheritance. Mô hình Prototype được sử dụng chủ yếu để tạo các object trong tình huống cần nâng cao hiệu suất.
Object được tạo sao chép từ object ban đầu. Ví dụ trường hợp của Pattern Prototype giúp biểu diễn thao tác cở sở dữ liệu mở rộng, để tạo ra một object được sử dụng cho các phần khác nhau của ứng dụng. Nếu một quá trình khác cần sử dụng object này, thay vì phải thực hiện thao tác cơ sở dữ liệu quan hệ này, nó sẽ được hưởng lợi bằng cách sao chép lại object được tạo trước đó
Hình vẽ này mô tả cách một Prototype Interface được sử dụng để sao chép các phần thực thi cụ thể.
Để sao chép một object thì constructor phải có thể khởi tạo object. Tiếp theo, sử dụng các biến từ khoá prototype và các phương thức giúp liên kết đến cấu trúc object. Hãy xem ví dụ cơ bản sau:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype.go = function() { // Rotate wheels } TeslaModelS.prototype.stop = function() { // Apply brake pads }
Hàm khởi tạo cho phép tạo ra một TeslaModelS đơn. Khi tạo ra một TeslaModelS mới, nó sẽ nắm giữ các state được khởi tạo trong constructor. Thêm vào đó. việc cập nhập hàm go
và shop
đơn giản hơn khi sử dụng prototype. Cách khác tương tự để thêm các hàm trên Prototype miêu tả như sau:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads } }
Revealing Prototype Pattern
Giống như Module Pattern, Prototype Pattern cũng có các hoạt động Revealing. Revealing Prototype Pattern giúp đóng gói các biến hoặc phương thức với public hoặc private vì nó trả về một object literal.
Do nó trả về một object, nên chúng ta sẽ thêm tiền tố cho object Prototype là function. Chúng ta sẽ thử thay đổi ví dụ bên trên bằng cách chúng ta có thể lựa chọn những gì có thể truy cập trên Prototye và những gì thì không được phép.
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype = function() { var go = function() { // Rotate wheels }; var stop = function() { // Apply brake pads }; return { pressBrakePedal: stop, pressGasPedal: go } }();
Chú ý rằng các hàm go
và stop
được bảo vệ khi trả về một object do nằm ngoài phạm vi của object được trả về. Do Javascript hỗ trợ kế thừa Prototype, nên chúng ta không cần viết lại các đặc tính cơ bản.
Observer
Có rất nhiều trường hợp khi chúng ta thay đổi thông tin của phần nào đó trên ứng dụng, thay đổi này cũng cần phải được cập nhập đồng thời ở chỗ khác nữa. Trong AngularJs nếu object $scope
thay đổi, thì một event có thể được đưa ra để thông báo cho các thành phần khác. Observer pattern kết hợp với tính năng này – nếu một object bị thay đổi nó sẽ thông báo đến các object phụ thuộc rằng có thay đổi xảy ra.
Ví dụ điển hình khác là kiến trúc MVC (Model – View – Controller). View sẽ thay đổi khi mà Model bị thay đổi. Một lợi ích quan trọng của việc này là nó giúp tách biệt View với Model, qua đó giảm phụ thuộc lẫn nhau.
Như trên hình vẽ ở trên, các object cần dùng là Subject
, Observer
và các Concrete
object. Subject chứa các tham chiếu đến các object observer là Concrete để thông báo khi có bất kỳ thay đổi nào xảy ra. Observer object là một Abstract class mà cho phép các object Concrete thực hiện phương thức notify()
Chúng ta hãy cùng xem một ví dụ AngualarJs chứa Observer Pattern qua đó quản lý event.
// Controller 1 $scope.$on('nameChanged', function(event, args) { $scope.name = args.name; }); ... // Controller 2 $scope.userNameChanged = function(name) { $scope.$emit('nameChanged', {name: name}); };
Với Observer Pattern điều quan trọng là phải phân biệt rõ đối tượng độc lập( hay ở đây là Subject).
Một chú ý quan trọng là mặc dù Observer Pattern có nhiều ưu điểm, nhưng một trong những nhược điểm của nó là làm giảm hiệu suất đáng kể khi số lượng theo dõi tăng. Một trong những observer ai cũng biết đó là watchers. Trong AngualarJs chúng ta có thể theo dõi các biến, các hàm và các object. $$digest chạy theo chu kỳ và thông báo đến mỗi watchers những giá trị mới bất cứ khi nào một object trong phạm vi bị thay đổi
Chúng ta có thể tạo các Subject và Observer của chúng ta trong Javascript. Hãy cùng xem nó sẽ được thực thi như thế nào sau đây
var Subject = function() { this.observers = []; return { subscribeObserver: function(observer) { this.observers.push(observer); }, unsubscribeObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.splice(index, 1); } }, notifyObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers[index].notify(index); } }, notifyAllObservers: function() { for(var i = 0; i < this.observers.length; i++){ this.observers[i].notify(i); }; } }; }; var Observer = function() { return { notify: function(index) { console.log("Observer " + index + " is notified!"); } } } var subject = new Subject(); var observer1 = new Observer(); var observer2 = new Observer(); var observer3 = new Observer(); var observer4 = new Observer(); subject.subscribeObserver(observer1); subject.subscribeObserver(observer2); subject.subscribeObserver(observer3); subject.subscribeObserver(observer4); subject.notifyObserver(observer2); // Observer 2 is notified! subject.notifyAllObservers(); // Observer 1 is notified! // Observer 2 is notified! // Observer 3 is notified! // Observer 4 is notified!
Publish/Subscribe
Publish/Subcrible sử dụng kênh topic/event nằm giữa các object muốn nhận các thông báo (Subscriber – Người đăng ký nhận thông tin) và object đưa ra event (Publisher – người đưa ra các thông tin). Hệ thống event này cho phép code định nghĩa ứng dụng – event cụ thể mà có thể truyền các tham số tuỳ chỉnh đang chứa các giá trị Subcriber cần.
Điều này khác với Observer Pattern bởi vì mọi Subcriber thực hiện xử lý event thích hợp qua đó đăng ký và nhận thông báo về chủ đề do Publisher đưa ra.
Nhiều Developer chọn tổ hợp Publish/Subcribe Design Pattern với Observer mặc dù chúng có sự khác biệt. Các Subcriber trong Publish/Subcribe pattern được thông báo thông qua các message. Nhưng các Observer được thông báo bằng cách thực hiện một xử lý giống như Subject ở trên.
Singleton
Singleton chỉ cho phép một khởi tạo duy nhất, nhưng nhiều instance của cùng một Object. Singleton hạn chế client tạo nhiều Object sau khi object đầu tiên được tạo, nó sẽ trả về các instance của chính nó
Việc tìm một trường hợp sử dụng Singleton là rất khó với hầu hết những người chưa sử dụng nó trước đây. Một trường hợp là sử dụng máy in văn phòng. Nếu có 10 người trong văn phòng và tất cả họ đều sử dụng một máy in, mười máy tính sẽ dùng chung một máy in. Bằng cách chia sẻ một máy in, họ chia sẻ các tài nguyên giống nhau
var printer = (function () { var printerInstance; function create () { function print() { // underlying printer mechanics } function turnOn() { // warm up // check for paper } return { // public + private states and behaviors print: print, turnOn: turnOn }; } return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } }; function Singleton () { if(!printerInstance) { printerInstance = intialize(); } }; })();
Phương thức create
là private vì chúng ta không muốn client có thể truy cập nó, tuy nhiên chú ý rằng phương thức getInstance
là public. Mỗi nhân viên văn phòng có thể sinh ra một phiên bản Printer
bằng cách gọi phương thức getInstance
như sau:
var officePrinter = printer.getInstance();
Trong AngularJs, Singleton là tương đối thịnh hành, đáng chú ý nhất là service, factory và provider. Vì chúng ta cần duy trì state và cung cấp truy cập đễn các nguồn tài nguyên, tạo 2 instance để hạn chế quan điểm của service/factory/provider được phân chia.
Giới hạn về tốc độ xảy ra trong các ứng dụng chạy đa luồng khi có nhiều hơn một tiến trình truy cập vào cùng một tài nguyên. Singleton dễ bị trường hợp ảnh hưởng đến tốc độ, vì nếu không có instance nào được khởi tạo trước, 2 tiến trình đồng thời sau đó có thể tạo 2 object thay vì trả về một instance. Điều này làm ảnh hưởng đến mục đích của Singleton. Do dó các lập trình viên phải xử lý đồng bộ riêng khi code trong các ứng dụng chạy đa luồng