Design patten trong Javascript có những gì thú vị? (Phần 1)

Nói đến design patten, thông thường người ta hay nghĩ đến những ngôn ngữ hỗ trợ mạnh về static-class nhưng có bao giờ bạn nghĩ đến design patterns cho javascripts một ngôn ngữ dựa trên untyped dynamic prototype ?

JavaScript là một ngôn ngữ dựa trên nguyên mẫu động không định kiểu. Tuy nhiên điều đó lại làm cho nó dễ dàng một cách đáng ngạc nhiên để thực hiện một số patten.

Trong bài viết này mình sẽ giới thiệu đến các bạn về một số design patten được dùng trong javascript mà có thể là rất quên thuộc với các bạn. Hãy bắt đầu với ví dụ đầu tiên về sự khác biệt giữa javascript với một ngôn ngữ định kiểu static class-based.

Singleton

Ý tưởng của mẫu singleton là chỉ có một thể hiện của một lớp. Điều này có nghĩa là khi lần thứ hai bạn sử dụng cùng một lớp để tạo một đối tượng mới, bạn sẽ nhận được cùng một đối tượng đã được tạo lần đầu tiên. Trong JavaScript không có class, chỉ có các objects. Khi bạn tạo một đối tượng mới, thực sự không có đối tượng giống như vậy và đối tượng khi được tạo thực chất đã là một singleton.

var obj = {
 myprop: 'my value'
;

Trong JavaScript, các đối tượng không bao giờ bằng nhau trừ khi chúng là cùng một đối tượng, vì vậy ngay cả khi bạn tạo một đối tượng giống hệt nhau với các thuộc tính nhưng nó sẽ không cùng với đối tượng đầu tiên.

var obj2 = {
myprop = 'my value'
} 
obj == obj2 // false
obj === obj2 // false

Vì vậy, bạn có thể nói rằng mỗi khi bạn tạo một đối tượng bằng cách sử dụng đối tượng, bạn thực sự tạo ra một singleton mà không phải sử dụng một cú pháp nào.

Using new

JavaScript không có class, vì vậy định nghĩa nguyên văn cho singleton không mang nhiều ý nghĩa về mặt kỹ thuật. Nhưng JavaScript có cú pháp new để tạo các đối tượng bằng các constructor functions và đôi khi bạn có thể muốn triển khai singleton bằng cú pháp này. Ý tưởng là khi bạn sử dụng new để tạo một số đối tượng sử dụng cùng một constructor, bạn sẽ chỉ nhận được các con trỏ mới cho cùng một đối tượng.

var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

Trong ví dụ này, uni chỉ được tạo lần đầu tiên khi hàm tạo được gọi. Lần thứ hai (và lần thứ ba, thứ tư, …) cùng một đối tượng nni được trả về. Đây là lý do tại sao uni === uni2, vì về cơ bản chúng là hai tham chiếu trỏ đến cùng một đối tượng. Vậy làm thế nào chúng ta có thể làm được điều này trong JavaScript?

Chúng cần xây dựng Universe constructor, đối tượng này được lưu lại khi nó được tạo ra và sau đó trả lại lần thứ hai cho khi constructor được gọi. Có một số giải pháp để thực hiện:

  • Bạn có thể sử dụng một biến toàn cục để lưu trữ thể hiện
  • Bạn có thể lưu trữ trong một thuộc tính tĩnh trong hàm constructor. Functions trong JavaScript là các đối tượng, vì vậy chúng có thể có các thuộc tính. Chẳng hạn như Universe.instance và lưu trữ đối tượng ở đó. Đây là một giải pháp tốt, gọn gàng với nhược điểm duy nhất là thuộc tính thì có thể truy cập công khai bởi mã bên ngoài của bạn có thể thay đổi nó.
  • Bạn có thể wrap các thể hiện trong một closure. Điều này giữ cho các thể hiện đủ private và không thể truy cập bên ngoài hàm constructor.

Instance in a Static Property

function Universe() {
    // do we have an existing instance?
    if (typeof Universe.instance === "object") {
        return Universe.instance;
    }
    // proceed as normal
    this.start_time = 0;
    this.bang = "Big";
    // cache
    Universe.instance = this;
    // implicit return:
    // return this;
}
// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

Đây là một giải pháp đơn giản với nhược điểm duy nhất là instance bị public. Nó không chắc rằng các mã khác sẽ thay đổi nó do nhầm lẫn.

Instance in a Closure

Một cách khác để thực hiện singleton giống như class là sử dụng một closure để bảo vệ single instance. Bạn có thể thực hiện điều này bằng cách sử dụng private static member.

function Universe() {
    // the cached instance
    var instance = this;
    // proceed as normal
    this.start_time = 0;
    this.bang = "Big";
    // rewrite the constructor
    Universe = function () {
        return instance;
    };
}
// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

Hàm tạo ban đầu được gọi lần đầu tiên và nó trả về giá trị this như bình thường. Sau đó, lần thứ hai, lần thứ ba, v.v. Hàm tạo được viết lại có quyền truy cập vào biến thể hiện thông qua closure và chỉ cần trả về nó. Hạn chế là hàm bị viết lại (trong trường hợp này là hàm constructor của Universe()) sẽ mất bất kỳ thuộc tính nào được thêm vào giữa thời điểm khởi tạo ban đầu và khởi tạo lại.

// adding to the prototype
Universe.prototype.nothing = true;
var uni = new Universe();
// again adding to the prototype
// after the initial object is created
Universe.prototype.everything = true;
var uni2 = new Universe();

// only the original prototype was
// linked to the objects
uni.nothing; // true
uni2.nothing; // true
uni.everything; // undefined
uni2.everything; // undefined
// that sounds right:
uni.constructor.name; // "Universe"
// but that's odd:
uni.constructor === Universe; // false

Nếu mục đích là trỏ đến constructor nguyên mẫu ban đầu thì chỉ cần chỉnh sửa lại một chút:

function Universe() {
    // the cached instance
    var instance;
    // rewrite the constructor
    Universe = function Universe() {
        return instance;
    };
    // carry over the prototype properties
    Universe.prototype = this;
    // the instance
    instance = new Universe();
    // reset the constructor pointer
    instance.constructor = Universe;
    // all the functionality
    instance.start_time = 0;
    instance.bang = "Big";
    return instance;
}

Factory

Mục đích của factory là tạo ra các đối tượng. Nó thường được triển khai trong một class hoặc một method tĩnh của một class, có các mục đích sau:

  • Thực hiện các thao toán tử lặp lại khi thiết lập các đối tượng tương tự .
  • Cung cấp một cách để tạo các đối tượng mà không cần biết chính xác loại (class) cụ thể thông qua factory.

Điểm thứ hai là quan trọng hơn trong các ngôn ngữ static class, là việc không cần thiết để tạo các thể hiện của các lớp. Trong JavaScript, phần triển khai này khá dễ dàng.

Các đối tượng được tạo bởi factory method (hoặc class) là do thiết kế kế thừa từ cùng một đối tượng cha. Chúng là các lớp con cụ thể thực hiện chức năng chuyên biệt. Đôi khi cha chúng là cùng một lớp có chứa phương thức factory. Thông qua ví dụ dưới đây sẽ giúp bạn phần nào hiểu được về factory :

  • Một constructor CarMaker.
  • Một static method của CarMaker được gọi là Factory(), tạo ra các đối tượng car.
  • Các constructors chuyên dụng CarMaker.Compact, CarMaker.SUV và CarMaker.Convertible được kế thừa từ CarMaker. Tất cả chúng sẽ được định nghĩa là thuộc tính tĩnh của lớp cha để có thể tìm lại khi cần.

Trước tiên, hãy xem cách triển khai đã hoàn thành sẽ được sử dụng như thế nào:

var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
corolla.drive(); // "Vroom, I have 4 doors"
solstice.drive(); // "Vroom, I have 2 doors"
cherokee.drive(); // "Vroom, I have 17 doors"

var corolla = CarMaker.factory ('Compact') sẽ là dễ nhận biết nhất trong mô hình factory. Bạn có một phương thức nhận tham số đầu vào là string và trả về một object với từng kiểu loại của đối tượng mà không phải dùng đến new constructors chúng thực chất chỉ là một hàm tạo các đối tượng dựa trên một loại được xác định bởi một chuỗi truyền vào.

// parent constructor
function CarMaker() { }
// a method of the parent
CarMaker.prototype.drive = function () {
    return "Vroom, I have " + this.doors + " doors";
};
// the static factory method
CarMaker.factory = function (type) {
    var constr = type,
        newcar;
    // error if the constructor doesn't exist
    if (typeof CarMaker[constr] !== "function") {
        throw {
            name: "Error",
            message: constr + " doesn't exist"
        };
    }
    // at this point the constructor is known to exist
    // let's have it inherit the parent but only once
    if (typeof CarMaker[constr].prototype.drive !== "function") {
        CarMaker[constr].prototype = new CarMaker();
    }
    // create a new instance
    newcar = new CarMaker[constr]();
    // optionally call some methods and then return...
    return newcar;
};
// define specific car makers
CarMaker.Compact = function () {
    this.doors = 4;
};
CarMaker.Convertible = function () {
    this.doors = 2;
};
CarMaker.SUV = function () {
    this.doors = 24;
};

Không có gì khó khăn trong việc thực hiện mô hình factory. Tất cả những gì bạn cần làm là tìm kiếm hàm tạo để tạo một đối tượng theo từng loại yêu cầu. Trong trường hợp này, một quy ước đặt tên đơn giản đã được sử dụng để ánh xạ các loại đối tượng đến các hàm tạo tạo chúng.

Trên đây là 2 patten mà mình mới đọc được trong chương 7 cuốn sách Javascript Patten mà mình muốn chia sẻ. Và nó còn một số patten quen thuộc nữa. Nếu bạn muốn biết trước thì có thể tìm đọc trong cuốn này. Còn không thì đợi mình sẽ chia sẻ trong phần 2 của bài viết nhá.

Tài liệu tham khảo:

http://sd.blackball.lv/library/JavaScript_Patterns_%282010%29.pdf

Add a Comment

Scroll Up