Giao diện (interface), Lớp (class) và kiểu Generic trong TypeScript– Phần 4

Giao diện (interface), Lớp (class) và kiểu Generic trong TypeScript– Phần 4
access_time 11/20/2015 12:00:00 AM
person Đào Minh Giang

Các kiểu generic

Các kiểu generic là một cách viết mã rằng sẽ giải quyết với bất kỳ kiểu đối tượng nào nhưng vẫn duy trì tính trọn vẹn kiểu đối tượng. Cho đến giờ, ta đã sử dụng các interface, class và các kiểu cơ bản của TypeScript để đảm bảo code kiểu mạnh (strongly typed) trong các ví dụ của ta. Nhưng chuyện gì xảy ra nếu một đoạn mã cần làm việc với bất kỳ kiểu đối tượng nào?

Một ví dụ là, giả sử ta muốn viết đoạn mã lặp trên một mảng các đối tượng và trả về một xâu chuỗi các giá trị của chúng. Vậy, đưa ra một danh sách các số, như [1, 2, 3], rồi trả về là “1, 2, 3”. Hay, đưa ra danh sách chuỗi [“first”, “second”, “third”] thì trả về kết quả là ‘first, second, third”. Chúng ta có thể viết vài đoạn mã chấp nhận giá trị với kiểu any nhưng điều này có thể phát sinh các lỗi khi không kiểm soát tính an toàn về kiểu. Chúng ta muốn đảm bảo tất cả các phần tử trong mảng cùng kiểu, thì kiểu generic chính là ứng cử viên sáng giá cho việc này.

Cú pháp Generic

Ta cùng viết một class với tên là Concatenator làm việc với bất kỳ kiểu đối tượng nào, nhưng vẫn đảm bảo tính toàn vẹn kiểu. Tất cả các đối tượng JavaScript có một hàm toString, hàm này được gọi bất kỳ khi nào cần một chuỗi từ đối tượng đó vào lúc thực thi, vậy ta sử dụng hàm toString để tạo một class generic cho tất cả các giá trị vào từ một mảng.

Đoạn mã thực thi generic của lớp Concatenator như sau:

class Concatenator<T> {
    concatenateArray(inputArray: Array<T>): string {
        var returnString = "";
        for (var i = 0; i < inputArray.length; i++) {
            if (i > 0)
                returnString += ",";
            returnString += inputArray[i].toString();
        }
        return returnString;
    }
}

Điều đầu tiên ta cần chú ý là cú pháp khai báo class, Concatenator  < T >. Cú pháp < T> là cú pháp được dùng để chỉ ra một kiểu generic, và tên được dùng cho kiểu generic này trong phần mã còn lại là T. Hàm concatenateArray cũng sử dụng cú pháp kiểu generic này, Array < T >. Nó chỉ ra rằng đối số inputArray phải là một mảng của kiểu ban đầu được sử dụng để tạo ra thể hiện của class này.

Tạo phiên bản các class generic

Để sử dụng một thể hiện của class generic này, ta cần xây dựng class và báo với trình biên dịch qua cú pháp < > kiểu thực sự của T là gì. Chúng ta có thể sử dụng kiểu bất kỳ cho kiểu T trong cú pháp generic này, bao gồm các kiểu trên cơ sở JavaScript, các class của TypeScript hay thậm chí các interface TypeScript:

var stringConcatenator = new Concatenator<string>();
var numberConcatenator = new Concatenator<number>();
var personConcatenator = new Concatenator<IPerson>();

Chú ý cú pháp ta sử dụng để tạo phiên bản class Concatenator. Đầu tiên ta tạo một thể hiện của class generic Concatenator và chỉ ra rằng nó cần thay thế kiểu generic ( T ) bằng kiểu string ở tất cả các chỗ mà T được sử dụng trong mã. Tương tự vậy, trong ví dụ thứ hai ta tạo tiếp một thể hiện của class generic Concatenator và chỉ định kiểu number làm đối số kiểu. Ở ví dụ cuối ta sử dụng lại kiểu interface IPerson cho kiểu generic T.

Nếu ta sử dụng nguyên lý thay thế đơn giản áp dụng cho thể hiện stringConcatenator (sử dụng string), đối số inputArray phải là kiểu Array<string>. Tương tự vậy, thể hiện numberConcatenator của class generic sử dụng số, và đối số inputArray phải là mảng các số. Để kiểm nghiệm lý thuyết này, ta cùng tạo một mảng chuỗi và số, và xem trình biên dịch nói gì nếu ta thử phá vỡ nguyên tắc này:

var stringArray: string[] = ["first""second""third"];
var numberArray: number[] = [1, 2, 3];
var stringResult = stringConcatenator.concatenateArray(stringArray);
var numberResult = numberConcatenator.concatenateArray(numberArray);
var stringResult2 = stringConcatenator.concatenateArray(numberArray);
var numberResult2 = numberConcatenator.concatenateArray(stringArray);

Tuy nhiên, vấn đề của chúng ta bắt đầu khi ta thử truyền một mảng các số cho stringConcatenator, biến mà đã được cấu hình để chỉ sử dụng chuỗi. Và một lần nữa, nếu ta cố truyền một mảng chuỗi cho numberConcatenator, biến mà đã được cấu hình để chỉ sử dụng mảng số, TypeScript sẽ sinh lỗi như sau:

Build: Argument of type 'number[]' is not assignable to parameter of type 'string[]'.
Build: Argument of type 'string[]' is not assignable to parameter of type 'number[]'.

Như bạn thấy trong thông báo lỗi này, TypeScript đã chỉ rõ ra đối số ta sử dụng không đúng với kiểu của tham số yêu cầu.

Chú ý: Các ràng buộc với kiểu generic là một đặc tính chỉ có ở thời điểm biên dịch của TypeScript. Nếu ta xem mã JavaScript được sinh ra sẽ không nhìn thấy bất kỳ đoạn code nào thực hiện việc đảm bảo các nguyên tắc phải được thực hiện ở mã JavaScript sinh ra. Tất cả các ràng buộc và cú pháp generic đơn giản bị biên dịch bỏ đi. Trong trường hợp kiểu generics, mã JavaScript sinh ra thực sự là một phiên bản rất đơn giản mã của ta, không có các ràng buộc nào được nhìn thấy.

Sử dụng kiểu T

Khi ta sử dụng kiểu generic, điểm quan trọng cần chú ý là tất cả mã trong định nghĩa của một class generic hay hàm generic phải coi các thuộc tính của T như là bất kỳ kiểu object này. Ta cùng phân tích kỹ hơn cách thực thi hàm concatenateArray:

·         Hàm kiểu mạnh concatenateArray sử dụng đối số inputArray với kiểu là Array<T>. Có nghĩa là bất kỳ đoạn mã nào sử dụng đối số inputArray chỉ có thể sử dụng hàm và thuộc tính chung cho tất cả các mảng, không cần quan tâm đến kiểu của mảng đang chứa. Trong ví dụ của ta, inputArray xuất hiện ở hai vị trí.

o   Đầu tiên, trong vòng lặp, chú ý là nơi chúng ta sử dụng thuộc tính inputArray.length. Tất cả các mảng đều có thuộc tính leng để chỉ ra số lượng phần tử mảng có, vì thế sử dụng inputArray.length sẽ làm việc với bất kỳ mảng nào, mà không cần quan tâm đến kiểu của mảng đang chứa.

o   Thứ hai, ta tham chiếu một đối tượng trong mảng khi sử dụng cú pháp inputArray[i]. Tham chiếu này thực sự trả về một đối tượng đơn với kiểu T. Chú ý rằng bất cứ khi nào ta sử dụng T trong mã, ta phải sử dụng các hàm và thuộc tính chung cho kiểu đối tượng kiểu T. May mắn thay cho ta, ta sử dụng chỉ hàm toString, là hàm có ở mọi đối tượng JavaScript, nên không cần quan tâm đến kiểu là gì. Do đó đoạn mã generic sẽ biên dịch thành công.

Ta cùng kiểm thử lý thuyết kiểu T bằng cách tạo một class riêng cho class Concatenator :

class MyClass {
    private _name: string;
    constructor(arg1: number) {
        this._name = arg1 + "_MyClass";
    }
}
var myArray: MyClass[] = [new MyClass(1), new MyClass(2), new MyClass(3)];
var myArrayConcatentator = new Concatenator<MyClass>();
var myArrayResult = myArrayConcatentator.concatenateArray(myArray);
console.log(myArrayResult);

Trong ví dụ này, ta tạo một class là MyClass có một constructor nhận một giá trị số với tên là arg1. Sau đó nó thuộc tính nội bộ là _name được gán bằng việc ghép giá trị của arg1 với một chuỗi “_MyClass”.

Kế tiếp ta tạo một mảng với tên là myArray rồi tạo vài thể hiện của MyClass cho mảng này.

Sau đó ta tạo mới một thể hiện của class generic Concatenator  với việc chỉ định tham số kiểu là class MyClass.

Từ đó ta gọi hàm concatenateArray với đối số truyền vào là myArray, kết quả từ hàm được lưu vào biến myArrayResult.

Cuối cùng ta in kết quả ra cửa sổ console. Khi thực thi đoạn mã trên trình duyệt ta nhận được kết quả sau:

[object Object],[object Object],[object Object]

Không giống như ta mong đợi. Kết quả bất ngờ này sinh ra bởi chuỗ thể hiện một đối tượng – không phải là một kiểu JavaScript cơ sở - diễn giải ra là [object type]. Bất kỳ đối tượng tùy biến nào bạn viết ra đều có thể ghi đè hàm toString để cung cấp đầu ra thân thiện với người dùng. Ta có thể sửa lại đoạn mã trên khá đơn giản bằng cách ghi đè hàm toString trong class này như sau:

class MyClass {

    private _name: string;

    constructor(arg1: number) {

        this._name = arg1 + "_MyClass";

    }

    toString(): string {

        return this._name;

    }

}

Với việc thay thế hàm toString mặc định của JavaScript, kết quả thực thi sẽ đúng như ta mong đợi:

1_MyClass,2_MyClass,3_MyClass

Ràng buộc về kiểu của T

Khi sử dụng kiểu generic, có lúc ta muốn các ràng buộc về kiểu của T chỉ là một kiểu cụ thể nào đó hoặc một tập các kiểu nào đó. Trong trường hợp đó, ta không muốn mã generic của mình chấp nhận bất kỳ kiểu nào mà chỉ cho phép với một tập các đối tượng riêng. TypeScript sử dụng tính kế thừa để thực hiện việc đó với kiểu generic. Như ở ví dụ, ta cùng xem lại mã mẫu thiết kế Factory ở bài trước sử dụng một class PersonPrinter, nó được thiết kế đặc biệt để làm việc với các class thực thi interface IPerson.

class PersonPrinter<T extends IPerson> {
    print(arg: T) {
        console.log("Person born on "
            + arg.getDateOfBirth()
            + " is a "
            + arg.getPersonCategory()
            + " and is " +
            this.getPermissionString(arg)
            + "allowed to sign."
        );
    }
    getPermissionString(arg: T) {
        if (arg.canSignContracts())
            return "";
        return "NOT ";
    }
}

Trong đoạn mã này, ta định nghĩa một class PersonPrinter, sử dụng cú pháp generic. Chú ý là kiểu generic T được dẫn xuất từ interface IPerson, khi ta sử dụng từ khóa extends trong < T extends IPerson >. Điều này dẫn đến việc dùng kiểu T ở bất kỳ đâu sẽ được thay thế bởi interface IPerson, và như thế, chỉ cho phép các hàm hay thuộc tính được định nghĩa trong interface IPerson được sử dụng ở những nơi T được dùng.

Hàm print nhận một tham số arg với kiểu T. Sử dụng nguyên tắc generic, ta thấy rằng việc sử dụng arg chỉ cho phép các hàm đã được khai báo trong IPerson.

Hàm print tạo một chuỗi để ghi vào cửa sổ console, và và chỉ sử dụng các hàm định nghĩa trong interface IPerson là: getDateOfBirth and getPersonCategory. Theo trình tự để sinh ra một câu đúng về ngữ pháp ta gọi tới hàm getPermissionString nhận một đối số kiểu T. Hàm này đơn giản là gọi tới canSignContracts() và trả về chuỗi rỗng nếu đúng, ngược lại trả về chuỗi “NOT”.

Để minh họa cách sử dụng class này, ta cùng xem đoạn mã sau:

window.onload = () => {
    var personFactory = new PersonFactory();
    var personPrinter = new PersonPrinter<IPerson>();
    var child = personFactory.getPerson(new Date(2010, 0, 21));
    var adult = personFactory.getPerson(new Date(1969, 0, 21));
    var infant = personFactory.getPerson(new Date(2014, 0, 21));
    console.log(personPrinter.print(adult));
    console.log(personPrinter.print(child));
    console.log(personPrinter.print(infant));
}

Đầu tiên, ta tạo mới một thể hiện của class PersonFactory. Sau đó tạo một thể hiện của class generic PersonPrinter, với kiểu cho đối số T là IPerson. Đồng nghĩa là bất kỳ class nào truyền vào thể hiện của PersonPrinter phải thực thi interface IPerson. Và ta cũng biết ở ví dụ bài trước là PersonFactory sẽ trả về một thể hiện của class Infant, Child hay Adult, mà mỗ class này đã thực thi IPerson.

Kế đó ta khai báo các biến với tên child, adult và infant lần lượt nhận kết quả trả về khi gọi hàm getPerson của đối tượng personFactory. Ba dòng mã cuối đơn giản ta chỉ in ra kết quả được trả về khi gọi hàm print của đối tượng personPrinter để trả về một câu hoàn chỉnh mô tả thông tin từng thể hiện class Child, Adult, Infant. Kết quả cuối cùng khi thực thi đoạn mã trên trình duyệt:

Person born on Tue Jan 21 1969 is a Adult and is allowed to sign.
Person born on Thu Jan 21 2010 is a Child and is NOT allowed to sign.
Person born on Tue Jan 21 2014 is a Infant and is NOT allowed to sign.

Interface kiểu generic

Chúng ta cũng có thể sử dụng các interface với cú pháp kiểu generic. Với class PersonPrinter, thì interface tương ứng được định nghĩa là:

interface IPersonPrinter<T extends IPerson> {
    print(arg: T): void;
    getPermissionString(arg: T): string;
}

Interface này tìm kiếm trông tương tự định nghĩa class của ta, chỉ có sự khác biệt là hàm print và getPermissionString không có phần mã thực thi. Ta giữ cú pháp kiểu generic <T > và cũng chỉ ra T phải thực thi interface IPerson. Để sử dụng interafce này với class PersonPrinter ta điều chỉnh lại định nghĩa class như sau:

class PersonPrinter<T extends IPerson> implements IPersonPrinter<T> {
 }

Cú pháp này dường như khá dễ hiểu. Như ta đã nhìn thấy trước đây, ta dùng từ khóa implements đi kèm theo định nghĩa class và sau đó sử dụng tên interface. Tuy nhiên, chú ý rằng ta truyền kiểu T vào định nghĩa interface IPersonPrinter là kiểu generic IPersonPrinter<T>. Và nó phù hợp với định nghĩa interface generic IPersonPrinter.

Một interface định nghĩa các class generic sau đó còn bảo vệ mã của ta khỏi việc vô tình điều chỉnh. Như ở ví dụ này, giả sử nếu ta thử định nghĩa lại class PersonPrinter mà T không thỏa mãn ràng buộc là kiểu IPerson:

class PersonPrinter<T> implements IPersonPrinter<T> {
 }

Ở đây, ta loại bỏ ràng buộc kiểu T trong class PersonPrinter. TypeScript sẽ sinh lỗi biên dịch sau:

Build: Type 'T' does not satisfy the constraint 'IPerson'.

Lỗi này chỉ ra cho ta định nghĩa class: kiểu T được sử dụng trong code (PersonPrinter<T>) phải sử dụng kiểu T được mở rộng từ IPerson.

Tạo mới đối tượng trong kiểu generic

Hết lần này đến lần khác, các class generic cần tạo một đối tượng với kei6ù được truyền vào là một kiểu T generic. Ta cùng xem đoạn mã sau:

class FirstClass {
    id: number;
}
class SecondClass {
    name: string;
}
class GenericCreator<T> {
    create(): T {
        return new T();
    }
}
var creator1 = new GenericCreator<FirstClass>();
var firstClass: FirstClass = creator1.create();
var creator2 = new GenericCreator<SecondClass>();
var secondClass: SecondClass = creator2.create();

Ở đây, ta có hai class được định nghĩa, FirstClass và SecondClass. Trong đó FirstClass chỉ có một thuộc tính public id, SecondClass thì có một thuộc tính public là name. Sau đó ta có một class generic nhận một kiểu T và có một hàm với tên là create. Hàm create cố gắng tạo một thể hiện của kiểu T.

Ở 4 dòng mã cuối cùng thể hiện cách ta dùng class generic này. Biến creator1 tạo một thể hiện mới của class GenericCreator sử dụng cú pháp hợp lệ tạo biến với kiểu FirstClass.  Biến creator cũng tạo một thể hiện mới của class GenericCreator nhưng lần này ta sự dụng SecondClass làm đối số cho tham số kiểu. Thật không may là nếu ta biên dịch đoạn mã này TypeScript sẽ sinh lỗi biên dịch:

Build: Cannot find name 'T'.

Theo tài liệu TypeScript, theo trình tự để cho phép một class generic tạo một đối tượng của kiểu T, ta cần một tham chiếu tới kiểu T bởi một hàm constructor của nó. Mặt khác chúng ta cần truyền vào định nghĩa class một đối số. Hàm create cần viết lại như sau:

class GenericCreator<T> {
    create(arg1: { new (): T }): T {
        return new arg1();
    }
}

Ta cùng xem từng phần trong hàm create:

Đầu tiên, ta truyền vào đối số với tên là arg1, đối số này sau đó được định nghĩa với kiểu là { new(): T}. Đây là một mẹo nhỏ để cho phép ta tham chiếu tới T có hàm constructor. Chúng ta định nghĩa một kiểu nặc danh new quá tải hàm new() và trả về một kiểu T. Có nghĩa là đối số arg1 của hàm là kiểu mạnh và có một constructor trả về một kiểu T. Sự thực thi của hàm này chỉ đơn giản trả về một thể hiện của biến arg1. Với cách sử dụng cú pháp này sẽ loại bỏ lỗi biên dịch mà ta gặp lúc trước.

Tuy nhiên, với sự thay đổi này nghĩa là ta cần truyền định nghĩa class vào hàm create như sau:

var creator1 = new GenericCreator<FirstClass>();
var firstClass: FirstClass = creator1.create(FirstClass);
var creator2 = new GenericCreator<SecondClass>();
var secondClass: SecondClass = creator2.create(SecondClass);

Chú ý là sự thay đổi trong sử dụng hàm create trên dòng 2 và 5. Lúc này, ta cần truyền định nghĩa class cho kiểu của T là đối số cho hàm create: create(FirstClass) và create(SecondClass). Thử chạy đoạn code này trên trình duyệt của bạn và xem điều gì sẽ xảy ra. Trên thực tế, class generic sẽ tạo mới đối tượng kiểu FirstClass và SecondClass đúng như ta mong đợi.

Kiểm tra kiểu vào thời điểm thực thi

Mặc dù trình biên dịch TypeScript sẽ sinh lỗi biên dịch với các đoạn mã không đúng kiểu, thì việc kiểm tra kiểu này được biên dịch bên ngoài mã JavaScript được sinh ra. Nghĩa là cơ chế thực thi JavaScript không biết gì về interface hay kiểu generic của TypeScript. Vậy bằng cách nào để ta có thể được báo vào lúc thực thi rằng một class thực thi một interface.

JavaScript có một số hàm mà ta có thể sử dụng khi làm việc với các đối tượng, chúng sẽ nói với ta kiểu của đối tượng là gì, hay nếu một đối tượng là một thể hiện đối tượng khác. Với thông tin về kiểu, ta có thể sử dụng từ khóa JavaScript typeof và với thông tin về thể hiện, ta có thể dùng từ khóa instanceof. Ta cùng xem các hàm đó trả về gì, khi ta cho chúng một số class TypeScript đơn giản và xem nếu ta có thể sử dụng chúng để báo khi nào một class thực thi một interface.

Đầu tiên là một class đơn giản

class TcBaseClass {
    id: number;
    constructor(idArg: number) {
        this.id = idArg;
    }
}

Class TcBaseClass có một thuộc tính id và một constructor thiết lập thuộc tính này trên cơ sở tham số truyền vào.

Sau đó, ta có một class dẫn xuất từ TcBaseClass:

class TcDerivedClass extends TcBaseClass {
    name: string;
    constructor(idArg: number, nameArg: string) {
        super(idArg);
        this.name = name;
    }
    print() {
        console.log(this.id + " " + this.name);
    }
}

Class TcDerivedClass này dẫn xuất (hay mở rộng) từ class cơ sở TcBaseClass và thêm một thuộc tính name, một hàm print. Constructor của class dẫn xuất này phải gọi constructor của class cơ sở, truyền vào đối số idArg thông qua hàm super.

Bây giờ, ta tạo một biến với tên là base là một thể hiện mới của class TcBaseClass và tạo tiếp biến với tên là derived là một thể hiện mới của class TcDerivedClass như sau:

var base = new TcBaseClass(1);
var derived = new TcDerivedClass(2, "second");

Và để kiểm tra, ta cùng xem hàm typeof trả về gì cho từng class đó

console.log("typeof base: " + typeof base);
console.log("typeof derived: " + typeof derived);

Khi thực thi trên trình duyệt, ta sẽ nhận được kết quả trên cửa sổ console là:

typeof base: object
typeof derived: object

Điều đó nói lên rằng, cơ chế thực thi JavaScript xem một thể hiện của một class là một đối tượng.

Và bây giờ ta cùng chuyển sang từ khóa instanceof, và sử dụng nó để kiểm tra một đối tượng được dẫn xuất từ đối tượng khác:

console.log("base instance of TcBaseClass : " + (base instanceof TcBaseClass));
console.log("derived instance of TcBaseClass: " + (derived instanceof TcBaseClass));

Đoạn mã này sẽ sinh ra kết quả là:

base instance of TcBaseClass : true
derived instance of TcBaseClass: true

Bây giờ, ta cùng xem khi sử dụng từ khóa typeof  với các thuộc tính, hàm của class thì sẽ trả về gì:

console.log("typeof base.id: " + typeof base.id);
console.log("typeof derived.name: " + typeof derived.name);
console.log("typeof derived.print: " + typeof derived.print);

Và đây là kết quả khi thực thi:

typeof base.id: number
typeof derived.name: string
typeof derived.print: function

Như bạn thấy, JavaScript khi thực thi nhận diện chính xác thuộc tính id của kiểu cơ sở là number, và với lớp dẫn xuất thì thuộc tính name là kiểu string và hàm print là function.

Vậy bằng cách nào chúng ta có thể biết vào thời điểm chạy kiểu của một đối tượng là gì? Câu trả lời đơn giản là không dễ gì để biết. Chúng ta chỉ có thể biết khi một đối tượng là một thể hiện của một đối tượng khác, hay nếu một thuộc tính là một trong các kiểu JavaScript cơ bản. Nếu ta thử sử dụng hàm instanceof để thực thi thuật toán kiểm tra kiểu, chúng ta cần kiểm tra đối tượng vào với mọi kiểu được biết trong cây đối tượng của ta, như thế chắc chắn không phải là ý hay. Mặt khác chúng ta cũng không sử dụng instanceof để kiểm tra khi một class thực thi một interface, khi interface của TypeScript đã được trình biên dịch bỏ đi.

Reflection

Các ngôn ngữ với kiểu tĩnh khác cho phép cơ chế thực thi truy vấn một đối tượng, xác định kiểu của đối tượng là gì, và cũng truy vấn các giao diện mà đối tượng thực thi. Tiến trình này được gọi là reflection (sự phản chiếu)

Như ta đã xem, sử dụng hàm typeof hay instanceof của JavaScript, chúng ta có thể lấy được một số thông tin khi thực thi về một đối tượng. Ở mức cao của các khả năng này, chúng ta có thể sử dụng hàm getPrototypeOf để trả về một số thông tin về constructor class. Hàm getPrototypeOf trả về một chuỗi, từ đó ta có thể phân tích chuỗi này để xác định tên của class. Không may là, sự thực thi của hàm getPrototypeOf trả về các chuỗi hơi khác nhau giữa các phụ thuộc vào trình duyệt được sử dụng. Đồng thời, nó cũng chỉ được thực thi trong ECMAScript 5.1 và cao hơn, một lần nữa, có thể dẫn đến lỗi khi thực thi khi chạy trên các trình duyệt cũ hay các trình duyệt di động.

Một hàm JavaScript khác ta có thể dùng để lấy thông tin thực thi về một đối tượng là hàm hasOwnProperty. Đây là thành phần của JavaScript từ ECMAScript 3, và chỉ tương thích với hầu hết mọi trình duyệt, cả desktop lẫn di động. Hàm này trả về true hay false để chỉ ra một đối tượng có một thuộc tính mà bạn cần tìm.

Trình biên dịch TypeScript giúp chúng ta lập trình JavaScript theo cách hướng đối tượng bằng cách sử dụng interface, nhưng các interface này sẽ bị loại bỏ khi biên dịch và không xuất hiện trong mã JavaScript sinh ra. Ta cùng xem một ví về điều này với đoạn mã TypeScript sau:

interface IBasicObject {
    id: number;
    name: string;
    print(): void;
}
class BasicObject implements IBasicObject {
    id: number;
    name: string;
    constructor(idArg: number, nameArg: string) {
        this.id = idArg;
        this.name = nameArg;
    }
    print() {
        console.log("id:" + this.id + ", name" + this.name);
    }
}

Ví dụ đơn giản này định nghĩa một interface và thực thi trong một class. Interface IBasicObject có một thuộc tính người dùng kiểu number, thuộc tính name kiểu string, và một hàm print. Định nghĩa class BasicObject thực thi tất cả các thuộc tính và hàm bắt buộc. Bây giờ ta xem mã JavaScript được TypeScript sinh ra:

var BasicObject = (function () {
    function BasicObject(idArg, nameArg) {
        this.id = idArg;
        this.name = nameArg;
    }
    BasicObject.prototype.print = function () {
        console.log("id:" + this.id + ", name" + this.name);
    };
    return BasicObject;
})();

Trình biên dịch TypeScript sinh ra mã JavaScript không gộp bất kỳ mã liên quan tới interface IBasicObject. Tất cả gì ta có ở đây là một mã bao đóng cho định nghĩa class BasicObject. Interface IBasicObject mặc dù sử dụng bởi trình biên dịch TypeScript, không tồn tại ở mã JavaScript được sinh ra. Vì thế, ta nói rằng nó đã được "loại bỏ khi biên dịch".

Vì thế nó thể hiện cho ta một số vấn đề khi thực thi tính năng dạng như reflection trong JavaScript:

* Ta không thể nói rằng vào lúc thời gian chạy là một đối tượng thực thi một interface TypeScript bởi các interface TypeScript được loại bỏ khi biên dịch.

* Chúng ta không thể lặp qua các thuộc tính của đối tượng bằng cách dùng hàm getOwnPropertyNames với các trình duyệt ECMAScript 3 cũ.

* Chúng ta không thể sử dụng hàm getPrototypeOf với trình duyệt ECMAScript 3 cũ để xác định tên một class.

* Thực thi của hàm getPrototypeOf không thống nhất giữa các trình duyệt.

* Chúng ta không thể sử dụng từ khoá instanceof để xác định một kiểu class mà không so sánh nó với các kiểu đã biết.

Kiểm tra một đối tượng cho một hàm

Vậy bằng cách nào chúng ta báo vào lúc thực thi khi một đối tượng thực thi một interface?

Trong sách Pro JavaScript Design Patterns, Ross Harmes và Dustin Diaz thảo luận về một tình thể khó khăn, và đưa ra một giải pháp khá đơn giản. Chúng ta có thể gọi một hàm từ một đối tượng sử dụng một chuỗi mà chứa tên hàm, và sau đó kiểm tra khi kết quả là hợp lệ, hay undefined. Trong sách đó, họ đã xây dựng một hàm tiện ích sử dụng nguyên lý này, để kiểm tra vào thời điểm chạy khi một đối tượng có một tập các thuộc tính và phương thức được định nghĩa. Các phương thức và thuộc tính được định nghĩa đó giữ mã trong JavaScript như các mảng chuỗi đơn giản. Các mảng chuỗi đó hành động như một đối tượng “siêu dữ liệu” ("metadata") cho mã của ta mà chúng ta có thể truyền vào trong hàm tiện ích kiểm tra.

Hàm tiện ích FunctionChecker có thể được viết trong TypeScript như sau:

class FunctionChecker {
    static implementsFunction(
        objectToCheck: any, functionName: string): boolean {
        return (objectToCheck[functionName] != undefined &&
            typeof objectToCheck[functionName] == 'function');
    }
}

Class FunctionChecker có một hàm tĩnh đơn, với tên là implementsFunction, mà sẽ trả về hoặc true hay false. Hàm implementsFunction nhận một đối số là objectToCheck và một chuỗi với tên là functionName. Chú ý là kiểu của objectToCheck được chỉ định là kiểu any. Đây là một trường hợp hiếm hoi khi việc sử dụng kiểu any là kiểu TypeScript đúng đắn.

Trong hàm implementsFunction, ta sử dụng một kiểu cú pháp JavaScript đặc biệt để đọc chính hàm đó từ đối tượng chính nó, sử dụng cú pháp [ ] với một thể hiện của đối tượng, và tham chiếu nó qua tên: objectToCheck[functionName]. Nếu kết quả của typeof trả về là “function”, thì ta biết rằng đối tượng này thực thi hàm. Ta cùng xem qua một số cách dùng hàm này:

var myClass = new BasicObject(1, "name");
var isValidFunction = FunctionChecker.implementsFunction(myClass, "print");
console.log("myClass implements the print() function :" + isValidFunction);
isValidFunction = FunctionChecker.implementsFunction( myClass, "alert");
console.log("myClass implements the alert() function :" + isValidFunction);

Dòng 1, đơn giản là tạo một thể hiện của class BaseObject, và gán vào biến myClass. Dòng 2 sau đó gọi hàm implementsFunction của ta, truyền vào thể hiện của class và chuỗi “print”. Dòng 3 ghi lại kết quả ra console. Dòng 4 và 5 lặp lại tiến trình, nhưng kiểm tra xem thể hiện myClass thực thi hàm “alert” Kết quả của mà này sẽ như sau:

myClass implements the print() function :true
myClass implements the alert() function :false

Hàm implementsFunction này cho phép chúng ta dò ra một đối tượng và kiểm tra xem nó có một hàm với tên cụ thể nào không. Mở rộng khái niệm một chút, mang lại cho ta một cách đơn giản để thực hiện kiểm tra kiểu vào thời gian chạy. Tất cả chúng ta cần là một danh sách các hàm (hay thuộc tính) của đối tượng JavaScript cần thực thi. Danh sách của các hàm (hay thuộc tính) được mô tả là “siêu dữ liệu” (“metadata”) của class.

Kiểm tra interface với generic

Kỹ thuật này được Ross và Dustin mô tả, giữ danh thông tin “siêu dữ liệu” về các interface, dễ dàng được thực thi trong TypeScript. Nếu ta định nghĩa một class giữ “siêu dữ liệu” này cho từng interface của ta, sau đó chúng ta có thể dùng chúng để kiểm tra các đối tượng vào lúc chạy. Ta cùng đặt một interface giữ một mảng các tên phương thức (hoặc danh sách tên thuộc tính) để kiểm tra một đối tượng.

interface IInterfaceChecker {
    methodNames?: string[];
    propertyNames?: string[];
}

Interface IInterfaceChecker rất đơn giản – một tùy chọn mảng methodNames và một tùy chọn mảng propertyNames. Bây giờ thực thi interface để mô tả các thuộc tính và phương thức cần thiết của interface TypeScript IBasicObject:

class IIBasicObject implements IInterfaceChecker {
    methodNames: string[] = ["print"];
    propertyNames: string[] = ["id""name"];
}

Chúng ta bắt đầu với một định nghĩa class thực thi interface IInterfaceChecker. Class này có tên là IIBasicObject, với tiền tố là hai ký tự I. Đây là một quy tắc đặt tên đơn giản để chỉ ra rằng class IIBasicObject  giữ “siêu dữ liệu” cho interface IBasicObject mà ta đã định nghĩa trước đó. Mảng methodNames chỉ ra interface này cần thực thi phương thức print, và mảng propertyNames chỉ ra interface này cần gộp hai thuộc tính id và name.

Phương thức này định nghĩa siêu dữ liệu cho một đối tượng là một giải pháp rất đơn giản cho vấn đề của ta. Trong khi điều này có thể bắt buộc chúng ta giữ các đối tượng “siêu dữ liệu” đồng bộ với các interface TypeScript, bây giờ ta đã có cái chúng ta cần theo trình tự để kiểm tra xem một đối tượng thực thi một interface đã được định nghĩa chưa.

Mặt khác, chúng ta có thể sử dụng những gì ta biết về kiểu generic để thực thi một class InterfaceChecker mà sử dụng các đối tượng class “siêu dữ liệu”:

class InterfaceChecker<T extends IInterfaceChecker> {
    implementsInterface(
        classToCheck: any,
        t: { new (): T; }
    ): boolean
{
        var targetInterface = new t();
        var i, len: number;
        for (i = 0, len = targetInterface.methodNames.length; i < len; i++) {
            var method: string = targetInterface.methodNames[i];
            if (!classToCheck[method] ||
                typeof classToCheck[method] !== 'function') {
                console.log("Function :" + method + " not found");
                return false;
            }
        }
        for (i = 0, len = targetInterface.propertyNames.length; i < len; i++) {
            var property: string = targetInterface.propertyNames[i];
            if (!classToCheck[property] ||
                typeof classToCheck[property] == 'function') {
                console.log("Property :" + property + " not found");
                return false;
            }
        }
        return true;
    }
}
var myClass = new BasicObject(1, "name");
var interfaceChecker = new InterfaceChecker();
var isValid = interfaceChecker.implementsInterface(myClass, IIBasicObject);
console.log("myClass implements the IIBasicObject interface :" + isValid);

Ta bắt đầu với một class generic, với tên là InterfaceChecker, chấp nhận đối tượng T bất kỳ mà thực thi interface IInterfaceChecker. Nhắc lại là định nghĩa của interface IInterfaceChecker chỉ gồm một mảng methodNames và một mảng propertyNames. Class này chỉ có một hàm với tên là implementsInterface trả về một giá trị boolean – true nếu class thực thi tất cả các thuộc tính và method, và false nếu không. Tham số đầu tiên, classToCheck, là một thể hiện class mà ta đang truy vấn với “siêu dữ liệu” interface. Tham số thứ hai sử dụng cú pháp generic mà ta đã thảo luận trước đây cho phép tạo một thể hiện của T – cái mà trong trường hợp này là kiểu any thực thi interface IInterfaceChecker.

Phần thân của mã là một sự mở rộng của class FunctionChecker ta đã thảo luận trước đây. Đầu tiên ta tạo một thể hiện kiểu T và gán cho biến targetInterface. Sau đó ta lặp qua các chuỗi trong mảng methodNames và kiểm tra đối tượng classToCheck của ta có thực thi các hàm đó không.

Kế đến ta lặp lại tiến trình này để kiểm tra các chuỗi trong mảng propertyNames.

Vài dòng mã cuối thể hiện cách sử dụng class InterfaceChecker này:

·         Đầu tiên ta tạo một thể hiện của BasicObject và gán cho biến myClass, sau đó tạo một thể hiện của class InterfaceChecker rồi gán vào biến interfaceChecker.

·         Dòng gần cuối gọi hàm implementsInterface, truyền vào thể hiện myClass và IIBasicObject. Chú ý rằng ta không truyền vào thể hiện của class IIBasicObject, mà ta chỉ truyền vào định nghĩa class, để từ đó mã generic của ta sẽ tạo một thể hiện nội bộ cuae class IIBasicObject.

·         Dòng cuối cùng ghi lại kết quả thông báo true hay false ra console. Kết quả của dòng cuối khi thực thi là:

myClass implements the IIBasicObject interface :true

Bây giờ, chạy đoạn code với một đối tượng không hợp lệ:

var noPrintFunction = { id: 1, name: "name" };
isValid = interfaceChecker.implementsInterface(noPrintFunction, IIBasicObject);
console.log("noPrintFunction implements the IIBasicObject interface:" + isValid);

Biến noPrintFunction có hai thuộc tính id và name, nhưng không có thực thi hàm print. Kết quả của đoạn mã này sẽ là:

Function :print not found
noPrintFunction implements the IIBasicObject interface :false

Bây giờ chúng ta đã có một cách xác định vào thời điểm chạy rằng một đối tượng thực thi một interface được định nghĩa hay chưa. Kỹ thuật này có thể được dùng với các thư viện JavaScript mà bạn không kiểm soát được – hay thậm chí trong nhóm lớn khi API cho một thư viện riêng biệt là được nhất trí về nguyên tắc, trước khi các thư viện được viết ra. Trong trường hợp đó, một khi phiên bản mới của thư viện được phân phối, người sử dụng có thể nhanh chóng và dễ dàng đảm bảo rằng API phù hợp với các đặc tả theo thiết kế.

Các interface được sử dụng trong một số các mẫu thiết kế, và thậm chí qua đó chúng ta có thể thực thi các mẫu thiết kế sử dụng TypeScript, chúng ta muốn tiếp tục củng cố mã của chúng ta bằng cách thực hiện kiểm tra vào thời gian chạy với một interface của đối tượng. Kỹ thuật này cũng mở ra khả năng viết một IOC (Inversion of Control – đảo ngược sự điều khiển) chứa trong TypeScript, hay một sự thực thi của Mẫu thiết kế Domain Events.


vertical_align_top
share
Chat...