- vừa được xem lúc

[JavaScript] Bài 30 - Composition in OOP

0 0 12

Người đăng: Semi Art

Theo Viblo Asia

Sau khi tìm hiểu về các đặc trưng Inheritence, Encapsulation, Polymorphism, Abstraction, và cách thức để biểu hiện trong code JavaScript khi tham chiếu từ Java; Có một vài tính năng mà mình cảm thấy rất cần thiết để tìm kiếm một cách thức phù hợp để biểu thị. Đó là:

Thứ nhất, tính năng đóng gói Encapsulation vay mượn một phần từ nhãn protected, giúp giới hạn khả năng truy xuất tới các yếu tố chỉ khả dụng trong dòng kế thừa. Giải pháp được tham khảo từ JavaScript.info bị lệ thuộc khá nhiều vào quy ước logic tự tạo. Tức là về mặt kĩ thuật thì các yếu tố được đặt tên với ký hiệu _ ở phía trước vẫn là public và người viết code sẽ phải tự giới hạn việc viết lệnh truy xuất tới các yếu tố đó.

Thứ hai, Khả năng tạo bề mặt tham chiếu bằng interface tới một object tương thích và ẩn đi tất cả mọi yếu tố khác không liên quan tới interface đó. Cái này trong OOP người ta gọi là blackbox, khi code sử dụng một object không biết chi tiết tới đối tượng mà nó đang tương tác. Giải pháp mà mình tìm thấy trước đó chỉ có thể giúp sử dụng interface như một công cụ thiết kế trừu tượng, để tạo ràng buộc cho giai đoạn viết code triển khai logic chi tiết.

Hm... chắc chắn là phải có một cách thức khác để biểu thị những logic hoạt động như thế này. Ý tưởng chung chung là một ví dụ trong môi trường Java, nơi mà hình thức Đa Kế Thừa Multi-Inheritance không được hỗ trợ ở cấp độ cú pháp của ngôn ngữ, người ta đã mô phỏng lại bằng cách tạo ra một kiến trúc code được gọi là Tổ Hợp Các Đối Tượng Object Composition.

Tức là thay vì việc copy các yếu tố có mặt trong các Trait vào class sử dụng thì người ta sẽ gắn mỗi Trait vào một thuộc tính trong class đó. Và khi thực hiện truy xuất tới một thành phần có trong Trait đó thì cú pháp sẽ là object.trait.method(). Hơi rườm rà, nhưng điều quan trọng là tính năng cần thiết đã xuất hiện.

Vậy chúng ta hãy thử làm một vài ví dụ để xem Object Composition có thể cải thiện được những gì mà các công cụ OOP trong JavaScript chưa hỗ trợ về mặt kĩ thuật.

Protected State

Do JavaScript không có logic đóng gói ở cấp độ thư mục, vì vậy nên khi vay mượn logic hoạt động của nhãn protected từ Java, điều mà chúng ta muốn là: có thể định nghĩa các thuộc tính property không mở như public, nhưng cũng không đóng kín trong phạm vi của class đó như #private, mà có thể khả dụng trong cả phạm vi của các class kế thừa.

Như vậy, đầu tiên để các property không mở giống như public thì chúng ta sẽ định nghĩa tất cả đều là #private.

class Person { #name = "Somebody" #age = 1001 do () { console.log ("Just a method") }
} //. Person // - main - - - - - - - - - var wukong = new Person()
console.log (wukong.#name)
// - error: #name is private

Tuy nhiên bây giờ chúng ta cũng lại muốn các class kế thừa Person cũng có thể truy xuất trực tiếp tới các thuộc tính này. Tức là cần hỗ trợ thao tác extends có thể mang các thuộc tính này từ class Person sang các class kế thừa.

Nếu vậy, lúc này chúng ta lại cần các thuộc tính này được mở public khi thực hiện thao tác kế thừa. Và điều này chỉ khả thi nếu như chúng ta tách các thuộc tính ra một class khác.

Ở đây mình tạm sử dụng từ Entity (thực thể) để đặt tên cho các class đại diện, có thêm các phương thức method mô tả khả năng hành động, tương tác, mô phỏng thực thể người; Còn từ Record (bản ghi) được sử dụng để đặt tên cho class dữ liệu, mô tả một bản ghi trong database.

Xét trên giao diện sử dụng các class Entity, thì các #stateprivate nên có thể giới hạn khả năng truy xuất từ code định nghĩa bên ngoài dòng kế thừa. Còn ở các class Record thì các trường dữ liệu name, age, v.v... đều là public nên có thể được kế thừa bởi các class Record khác.

Như vậy trong trường hợp PersonRecord có chứa nhiều thuộc tính trạng thái thì chúng ta sẽ có thể được hỗ trợ bởi thao tác extends và không phải định nghĩa lại khi cần tạo ra một class kế thừa PersonEntity.

const Person = { /* namespace */ }
const VNese = { /* namespace */ } Person.Record = class { name = "Someone" age = 1001
} //...Record Person.Entity = class { #state = new Person.Record () do () { console.log ("Just a method.") }
} //...Entity VNese.Record = class extends Person.Record { jobs = ["crafter", "teacher"]
} //..Record VNese.Entity = class extends Person.Entity { #state = new VNese.Record () intro () { console.log (this.#state.name) console.log (this.#state.age) console.log (this.#state.jobs) }
} //..Entity // - main - - - - - - - - - wukong = new VNese.Entity ()
wukong.intro () // Ok
console.log (wukong.#name) // Error
node main.js Someone
1001
[ 'crafter', 'teacher' ] /home/semiart/Documents/draft-code/main.js:38
wukong.#name () ^ SyntaxError: Private field '#name' must be declared in an enclosing class

Như vậy là chúng ta đã có #name khả dụng đối với code được định nghĩa bên trong class kế thừa và được ẩn khỏi code định nghĩa bên ngoài. Cách thức xử lý protected state đối với abstract class cũng hoàn toàn tương tự nên chúng ta không cần thiết phải thêm code ví dụ nữa.

Về vấn đề đóng gói các phương thức method thì thực sự không hẳn cần thiết, bởi trên thực tế thì những method cần ẩn đi ở dạng private hay protected đều là một dạng sub-program hỗ trợ cho các method để mở ở dạng public. Và như vậy chúng ta đều có thể chuyển thành các sub-program định nghĩa bên ngoài không gian của các class, sau đó thì từ các method chính sẽ có thể truyền vào các tham số cần thiết các sub-program hỗ trợ.

Blackbox Referencing

Trước hết hãy cùng xem xét lại tính năng này trong Java mà chúng ta đã biết qua ví dụ của bài viết trước. Giả sử chúng ta đang có một class Person triển khai implements các giao diện CrafterTeacher. Một thao tác tham chiếu qua interface Crafter sẽ không thể sử dụng các phương thức không được khai báo trong interface đó.

interface Crafter { void work (); void rest ();
}
interface Teacher { void intro (); void teach ();
}
Person someone = new Person (); Teacher asteacher = someone ();
asteacher.teach (); // Ok Crafter ascrafter = someone;
ascrafter.teach (); // Error

Tính năng này được thể hiện trong Java nhờ các tên định danh đều được định kiểu rõ ràng và trình biên dịch sẽ có thể thực hiện logic tìm tới định nghĩa của interface đang sử dụng. Tuy nhiên đối với một ngôn ngữ kiểu động dynamic-typing như JavaScript thì chúng ta cần tự tìm giải pháp. Vẫn sẽ là Object Composition. Và đây là phép tham chiếu qua một interface trong JavaScript:

var someone = new Person.Entity () var { asteacher } = someone
asteach.intro ()
asteach.teach () // Ok var { ascrafter } = someone
ascrafter.teach () // Error

Điều đó có nghĩa là mỗi interface sẽ cần triển khai bằng một trait tương ứng và gắn vào một thuộc tính của class Entity sử dụng trait đó.

const Person = { /* Entity */ }
const Teacher = { /* Trait */ } Person.Record = class { name = "Someone" age = 1001 constructor (name, age, knowledge) { this.name = name this.age = age }
} //...Record Person.Entity = class { #state = null asteacher = null ascrafter = null constructor (name, age, knowledge) { this.#state = { /* empty */ } // - uses Traits this.asteacher = new Teacher.Trait (this.#state, knowledge) this.ascrafter = new Object () // Crafter.Trait // - override Properties in Traits Object.assign (this.#state, new Person.Record (name, age)) }
} //...Entity Teacher.Record = class { knowledge = 1001 constructor (knowledge) { this.knowledge = knowledge }
} //....Record Teacher.Trait = class { #state = null constructor (superstate, knowledge) { this.#state = superstate var record = new Teacher.Record (knowledge) Object.assign (this.#state, record) } intro () { console.log ("As a teacher..") console.log ("Name: " + this.#state.name) console.log ("Age: " + this.#state.age) } teach () { console.log ("Knowledge: " + this.#state.knowledge) }
} //....Trait // - main - - - - - - - - - wukong = new Person.Entity ("Wukong", 500, 72) var { asteacher } = wukong
asteacher.intro () // Ok
asteacher.teach () // Ok var { ascrafter } = wukong
ascrafter.teach () // Error

Ở đây có một chút lưu ý nhỏ trong code ví dụ của trait, thông thường thì các trait sẽ được định nghĩa kèm theo đầy đủ các thuộc tính property cần sử dụng cho các phương thức method có mặt trong trait đó. Tuy nhiên, để làm rõ logic tham chiếu tới superstate nên mình đã không định nghĩa nameage trong trait Teacher.Record.

Ở vị trí ascrafter = new Crafter.Trait() sử dụng một new Object() rỗng thay thế để tượng trưng; Vì chúng ta chỉ đang thử tham chiếu tới phương thức teach() không có trong dự định thiết kế Crafter.Trait.

node main.js As a teacher..
Name: Wukong
Age: 500
Age: 72
Knowledge: 1001 /home/semiart/Documents/draft-code/main.js:69
ascrafter.teach () ^ TypeError: ascrafter.teach is not a function

Oh.. và với cách thức thực hiện như thế này, nếu muốn một property nào trong wukong.#state có thể được mở ra cho code bên ngoài sử dụng thì chúng ta có thể tạo thêm một trait có tên dạng như asaccess giữ tham chiếu tới #superstate.

Sau đó asaccess sẽ có thể cung cấp ra bên ngoài các phương thức mở publicreturn #state.property muốn để mở. Như vậy, chúng ta vẫn có thể quyết định các property sẽ có thể được truy xuất ở cấp độ public hoặc protected nếu mong muốn.

Tổng kết

Như vậy là với thiết kế class Entity bao gồm một object #state duy nhất và các object trait giữ tham chiếu tới superstate = entity.#state. Chúng ta đã có thể thực sự vay mượn được logic hoạt động của các protected property và giới hạn tham chiếu qua interface của một ngôn ngữ định kiểu tĩnh như Java.

Ngoài các chủ đề liên quan đã giới thiệu thì OOP còn một hạng mục phổ biến nữa được gọi là Design Patterns. Nếu như bạn có thời gian để tìm hiểu thêm về chủ đề này thì...

Liên kết tham khảo: [Design Patterns] Một Số Dạng Thức Triển Khai Trong OOP

Đây cũng là bài viết cuối cùng trong chuỗi bài viết giới thiệu về OOP trong Sub-Series này. Và bây giờ chúng ta sẽ chuyển qua một mô hình lập trình khác có tên gọi là Data-Driven Programming.

(chưa đăng tải) [JavaScript] Bài 31 - Event-Driven Programming

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 501

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 414

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 136

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 117

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 93

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 229