JavaScript và lập trình hướng đối tượng [3]

Cấu trúc của một đối tượng đôi khi rất phức tạp, trong một đối tượng có thể có vô số các thuộc tính, các biến trạng thái, các phương thức và hàm công cụ. Nhưng trên thực tế, khi sử dụng các đối tượng, lập trình viên chỉ quan tâm đến những tính năng thật sự hữu ích mà đối tượng đó mạng lại mà không không quan tâm xem chúng có cấu trúc hay cơ chế phức tạp đến đâu. Để giải quyết vấn đề này, OOP đưa ra khái niệm về tính bao gói (Encapsulation). Liệu JavaScript có cung cấp cho chúng ta khả năng bao gói các đối tượng hay không? chúng ta sẽ cùng nhau khảo sát vấn đề này ngay sau đây.

Vấn đề Private & public

Hầu hết các ngôn ngữ lập trình hướng đối tượng quen thuộc như Java hay C# đều hỗ trợ từ khóa public và private cho các thuộc tính cũng như phương thức trong class, mục đích là tạo ra cơ chế che giấu hay hiển thị các đặc tính của đối tượng này đối với đối tượng khác. Với JavaScript chúng ta cũng có thể tạo ra các pattern để giả lập hai từ khóa này:

Private variables: những biến được khai báo với từ khóa var bên trong object, chỉ có thể được truy nhập bởi các hàm riêng (private functions) hoặc các phương thức đặc quyền(privileged methods) của object.

Private functions: những hàm được khai báo kiểu inline hoặc khai báo kiểu var functionName=function(){…} và được truy cập bởi các phương thức đặc quyền bên trong object.

Privileged methods: những hàm được khai báo kiểu this.methodName=function(){…} bên trong object. Những hàm này có thể được gọi bên ngoài tới object.

Public property: là những thuộc tính được khai báo kiểu this.propertyName, những thuộc tính này có thể thay đổi từ bên ngoài của object.

Public method: là những phương thức được khai báo kiểu ClassName.prototype.methodName=function(){…},

những phương thức này có thể được gọi từ bên ngoài object.

Static property: những thuộc tính được khai báo kiểu ClassName.propertyName=someValue.

Prototype property: những thuộc tính được khai báo kiểu ClassName.prototype.propertyName=someValue.

Hoặc chúng ta có thể xây đựng một cách đơn giản theo các Pattern dưới đây:

—————
//Public:

function Constructor(…) {
this.membername = value;
}

Constructor.prototype.membername = value;

//Private:

function Constructor(…) {
var that = this;
var membername = value;
function membername(…) {…}
}

//chú ý

function membername(…) {…}

//có thể thay thế cho

var membername = function membername(…) {…};

//Privileged

function Constructor(…) {
this.membername = function (…) {…};
}

—————

Như vậy về cơ bản ta có thể chủ động tạo ra đặc tính đóng gói cho đối tượng được tạo ra trong JavaScript.

Trong thế giới của động vật, các đối tượng rất phong phú và đa dạng về đặc điểm cũng như tập tính, và vì vậy để việc quản lý dễ dàng hơn người ta đưa ra khái niệm phân loại các nhóm đối tượng thành lớp, bộ, ngành v.v… Trong lập trình hướng đối tượng, các đối tượng có đặc điểm đặc trưng giống nhau được mô tả chung bởi một lớp. Các đối tượng khác lớp nhưng có cùng chung nguồn gốc được nhóm lại bằng mối quan hệ về thế hệ (generation). Cách làm đó dựa trên đặc tính kế thừa và sự đa hình trong lập trình hướng đối tượng. Với JavaScript chúng ta cũng có thể giả lập khả năng này theo một cách rất riêng sau đây:

Vấn đề kế thừa với prototype
Kế thừa là một đặc tính vô cùng quan trọng trong lập trình hướng đối tượng, nhờ đặc điểm này việc phát triển và bảo trì ứng dụng trở nên đơn giản hơn bao giờ hết. Tuy nhiên JavaScript mặc nhiên không cung cấp đặc tính quan trọng này, muốn cài đặt, chúng ta phải dựa trên một vài pattern khá rắc rối. Nếu như các bạn đã từng tìm hiểu về kế thừa với JavaScript thì hẳn các bạn nhận ra rằng có khá nhiều cách để cài đặt, tuy nhiên trong bài viết này tôi sẽ chỉ giới thiệu một cách mà theo cá nhân là khá đơn giản và tiện lợi khi triển khai, đó là sử dụng prototype và constructor.

Trước hết, giả sử chúng ta có hai class ParentClass và ChildClass. Để thực hiện cho ChildClass kế thừa ParentClass ta lần lượt các bước sau:

•Cho prototype của ChildClass là một thể hiện của ParentClass:ChildClass.prototype=new ParentClass();

• Cài đặt lại thuộc tính constructor cho ChildClass: ChildClass.prototype.constructor=ChildClass;

Như vậy với hai bước đơn giản ta đã thực hiện được kế thừa trong JavaScript. Tuy nhiên còn khá nhiều vấn đề nảy sinh trong khi bạn xây dựng các class phức tạp, ví dụ như làm thế nào để gọi phương thức của ParentClass trong khi ChildClass đã overridden nó, hay vấn đề về virtual class tức là class chỉ có thể kế thừa mà không cho phép tạo một thể hiện cho nó. Chúng ta sẽ lần lượt giải quyết vấn đề này tiếp sau đây.

Nhưng trước khi đi vào các vấn đề đã nêu ta hãy làm một ví dụ thú vị sau:

—————————-
//Class Động vật có vú
function Mammal(name){
this.name=name;
this.offspring=[];//Mùa sinh sản!!!
}

//Phương thức sinh con (hoặc có con)
Mammal.prototype.haveABaby=function(){
var newBaby=new Mammal(“Baby “+this.name);
this.offspring.push(newBaby);
return newBaby;
}

//Hàm trả về tên con vật
Mammal.prototype.toString=function(){
return ‘[Mammal “‘+this.name+'”]’;
}

//Class Mèo kế thừa Class Động vật có vú
Cat.prototype = new Mammal();
Cat.prototype.constructor=Cat;

//Constructor của class Cat
function Cat(name){
this.name=name;
}

//Hàm trả về tên con vật
Cat.prototype.toString=function(){
return ‘[Cat “‘+this.name+'”]’;
}

//Tạo Mr.Bill !!!

var someAnimal = new Mammal(‘Mr. Bill’);

//Tạo mèo Tom
var myPet = new Cat(‘Tom’);
alert(‘someAnimal is ‘+someAnimal);//Trả về ‘someAnimal is [Mammal “Mr. Bill”]’
alert(‘myPet is ‘+myPet);//Trả về ‘myPet is [Cat “Tom”]’

//Cho mèo sinh con (kế thừa từ Mammal)
myPet.haveABaby();

//Thông báo về số con của mèo Tom
alert(myPet.offspring.length);

alert(myPet.offspring[0]);//Trả về [Mammal “Baby Tom”]’

————–

*Vấn đề Super & Sub Class

Hãy thử mở rộng ví dụ trên để ta có dịp minh họa cách mà một class con gọi đến một phương thức của class cha trong khi nó đã được overridden. Ta sẽ muốn rằng ngay sau khi mèo con được sinh ra nó sẽ kêu một tiếng “meeoo!” chẳng hạn. Để làm được điều này ta sẽ viết một hàm haveBaby của riêng class Cat trong đó sẽ gọi lại hàm haveBaby trong class Mammal:

—————–

Cat.prototype.haveABaby=function(){
Mammal.prototype.haveABaby.call(this);
alert(“mew!”);
}

——————

Ở đây, các bạn hãy nhớ lại cách thức sử dụng phương thức call của đối tượng Function mà ta đã từng đề cập. Như vậy với việc sử dụng call() ta hoàn toàn có thể làm được giống như phương thức “super()” trong Java và các ngôn ngữ khác.

Do vậy, từ bây giờ để cho tiện thì tại sao chúng ta không cài đặt luôn một “super” cho class của chúng ta. Làm điều đó không mấy phiền hà như sau:

————————–
Cat.prototype=newMammal();

Cat.prototype.constructor=Cat;

Cat.prototype.parent=Mammal.prototype;//”super”

Cat.prototype.haveABaby=function(){
var theKitten = this.parent.haveABaby.call(this);//”super(this)”
alert(“mew!”);
return theKitten;
}
———————–

//Các bạn sẽ có thắc mắc nhỏ là tại sao không dùng từ super thay cho parent? Lí do là vì hình như JavaScript có ý định sài từ này trong các phiên bản tương lai thì phải! Một điều nữa nếu bạn băn khoăn là từ parent đã được DOM sử dụng khi truy cập đến các node, điều này thì cứ vô tư đi vì đây là JavaScript mà!:D

*Vấn đề virtual Class

Một số ngôn ngữ lập trình hướng đối tượng có giải quyêt vấn đề về virtual class, tức là một class không thể có một thể hiện của chính nó, nhưng có thể kế thừa từ nó. Như trong ví dụ trên, ta muốn thêm vào một class LivingThing mà Mammal sẽ kế thừa từ nó, nhưng ta không muốn ai đó lại có thể tạo ra một LivingThing không mong muốn (chẳng hạn LivingStone!!:P). Với JavaScript ta có thể thực hiện điều này bằng cách thay thế function bằng một object cho virtual class.

——————
//Khai báo class kiểu JSON

LivingThing={
beBorn : function(){
this.alive=true;
}
}

//…

Mammal.prototype = LivingThing;

Mammal.prototype.parent = LivingThing;

//Để ý rằng không phải là ‘LivingThing.prototype’

Mammal.prototype.haveABaby=function(){
this.parent.beBorn.call(this);
var newBaby=new this.constructor(“Baby “+this.name); this.offspring.push(newBaby);
return newBaby;
}

—————-

Như vậy nếu một ai đó khai báo như sau:

—————–
var stone= new LivingThing(); // Sẽ gây lỗi

—————–

Bởi vì LivingThing bây giờ không phải là kiểu function mà có kiểu là object, do đó không thể coi nó như là một constructor với từ khóa new.

Như các bạn đã thấy, với cách cài đặt kế thừa trên ta luôn phải thực hiện hai dòng lệnh bắt buộc mỗi khi thực hiện kế thừa. Để cho tiện lợi, ta có thể mở rộng khả năng này cho bản thân object Function trong JavaScript, và coi đó như là một thuộc tính vốn có của JavaScript:

————————

Function.prototype.inheritsFrom=function(parentClsOrObj){
if (parentClsOrObj.constructor == Function ){
//Normal Inheritance
this.prototype = new parentClsOrObj;
this.prototype.constructor = this;
this.prototype.parent=parentClsOrObj.prototype;
} else {
//Pure Virtual Inheritance
this.prototype = parentClsOrObj;
this.prototype.constructor = this;
this.prototype.parent = parentClsOrObj;
}
return this;
}
//
LivingThing = {
beBorn : function(){
this.alive = true;
}
}

//

function Mammal(name){
this.name=name;
this.offspring=[];
}

Mammal.inheritsFrom( LivingThing );

Mammal.prototype.haveABaby=function(){
this.parent.beBorn.call(this);
var newBaby=new this.constructor( “Baby “+this.name); this.offspring.push(newBaby);
return newBaby;
}

//

function Cat( name ){
this.name=name;
}

Cat.inheritsFrom(Mammal );

Cat.prototype.haveABaby=function(){
var theKitten = this.parent.haveABaby.call(this);
alert(“mew!”);
return theKitten;
}

Cat.prototype.toString=function(){
return ‘[Cat “‘+this.name+'”]’;
}

//
var tom = new Cat( “Tom” );

var kitten = tom.haveABaby( ); // mew!

alert( kitten ); // [Cat “Baby Tom”]

////////////////////////////////////////////////////////
——————

Phù!!!Giá như JavaScript hỗ trợ tốt OOP thì đỡ biết mấy!

Với đặc tính linh hoạt và mềm dẻo có lẽ còn có rất nhiều cách thức khác thể hiện OOP trong JavaScript, tuy nhiên câu chuyện đã khá dài, xem ra chỉ phù hợp với những ai muốn xây cho mình một framework mạnh như JQuery chẳng hạn mới quan tâm hơn về vấn đề này. Do vậy chuỗi bài về OOP trong JavaScript xin khép lại ở đây để chuyển sang những vấn đề thiết thực và gần gũi, thường nhật hơn với các bạn lập trình. Hy vọng bài viết đem lại nhiều điều bổ ích cho các bạn lập trình trong quá trình chinh phục thử thách trên những dòng code.

1 – 2 – 3

(Hết)

Tài nguyên:

[link] http://www.w3schools.com/js/

[eBook] Head First JavaScript by Michael Morrison

[eBook] JavaScript Bible by Danny Goodman

One thought on “JavaScript và lập trình hướng đối tượng [3]”

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *