Typescript-Cuộc phiêu lưu với OOP (phần 1)
Nhắc đến lập trình hướng đối tượng (OOP) có 4 tính chất đặc thù mà bất kỳ Dev nào cũng phải nắm rõ như “khẩu quyết tâm pháp”. Mặc dù được đưa vào môn học cơ bản trong trường đại học, nhưng khi được hỏi thì mình thấy đa phần các bạn sinh viên mới ra trường thường hay mơ hồ và có cái hiểu chưa đúng về lập trình OOP. Ở series này, mình sẽ cùng các bạn trên hành trình tìm về những khái niệm xưa cũ nhưng trên một ngôn ngữ mới toanh Typescript. Series sẽ gồm 4 phần cũng tương ứng với 4 tính chất đặc thù trong OOP.
1. Tính trừu tượng (abstraction)
2. Tính đóng gói (encapsulation)
3. Tính đa hình (polymorphism)
4. Tính kế thừa (inheritance)
Vũ khí đầu tiên Abstraction
1. Vũ khí đầu tiên, abstraction?
Nghe đến abstraction hay trừu tượng bạn nghĩ ngay đến một cái gì đó không có thực, không tồn tại đúng không nào?
Thực ra thì trong OOP có tồn tại một lớp gọi là lớp trừu tưởng, nó hoàn toàn có thực. Nó trừu tượng ở chỗ, nó không thể dùng để tạo ra một thể hiện (instance) như những lớp bình thường khác. Lớp tượng này bạn có thể hiểu nó chỉ là một bộ bộ khung để bạn có thể tạo ra các lớp con của nó.
Ngoài ra các bạn cũng nên biết qua về khái niệm hàn lâm của nó, đừng thấy phức tạp quá mà bỏ qua nhé, cứ nghiền ngẫm rồi một ngày sẽ lĩnh hội thôi (nguồn wikipedia)
Tính trừu tượng (abstraction): Đây là khả năng của chương trình bỏ qua hay không chú ý đến một số khía cạnh của thông tin mà nó đang trực tiếp làm việc lên, nghĩa là nó có khả năng tập trung vào những cốt lõi cần thiết. Mỗi đối tượng phục vụ như là một “động tử” có thể hoàn tất các công việc một cách nội bộ, báo cáo, thay đổi trạng thái của nó và liên lạc với các đối tượng khác mà không cần cho biết làm cách nào đối tượng tiến hành được các thao tác. Tính chất này thường được gọi là sự trừu tượng của dữ liệu.
Tính trừu tượng còn thể hiện qua việc một đối tượng ban đầu có thể có một số đặc điểm chung cho nhiều đối tượng khác như là sự mở rộng của nó nhưng bản thân đối tượng ban đầu này có thể không có các biện pháp thi hành. Tính trừu tượng này thường được xác định trong khái niệm gọi là lớp trừu tượng hay lớp cơ sở trừu tượng.
Đến đây là thấy đủ “trừu tượng” rồi heng, chúng ta hãy cùng xem đối với Typescript chúng ta sẽ khai báo một lớp trừu tượng như thế nào nhé.
2. Typescript sử dụng nó như thế nào?
Để thể hiện được tính chất “trừu tưởng”, Typescript đã sử dụng Abstract classvà Interface để sử dụng được tính chất này.
Đến nay mặc dù OOP đã rất phổ biến nhưng đa số developer vẫn còn khá mơ hồ về việc phân biệt hai khái niệm Abstract class và Interface, đây cũng là sợi dây thừng mà các các nhà tuyển dụng thường xuyên tròng vào cổ các bạn (nói chơi thôi nha ahihi). Nhưng tạm bỏ qua vấn đề này, chúng ta hãy đi thẳng đến cách làm thế nào để khai vào một Abstract class và một Interface nhé.
Abstract classes
Từ khóa để khai báo một lớp trừu tượng là “abstract class”, các phương thức muốn là trừu tượng thì sử dụng từ khỏa “abstract” trước tên hàm.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
abstract class Warrior { readonly name: string; public weapon: string; constructor(name: string) { this.name = name; } sayHi(): void { console.log(`Hello, I am ${this.name}`); } abstract arm(weapon: string): void; // hàm này phải được triển khai ở lớp dẫn xuất } class SuperWarrior extends Warrior { constructor(name: string) { super(name); // hàm khởi tạo trong lớp dẫn xuất phải gọi super() } arm(weapon): void { console.log(`${this.name} is a super warrior fighting with ${weapon}`); } fly(): void { console.log(`${this.name} can fly`); } } let hercules: Warrior; // đúng! nếu tạo một tham chiếu với kiểu dữ liệu là lớp trừu tượng hercules = new Warrior(); // lỗi: không thể tạo một thể hiện của lớp trừu tượng hercules = new SuperWarrior(); // đúng! thể hiện được tạo ra từ lớp con của lớp trừu tượng hercules.arm(); hercules.sayHi(); hercules.fly(); // lỗi: phương thức không tồn tại trong lớp trừu tượng. |
Interface
Từ khóa để khai báo một interface là “interface”, các phương thức không thể định nghĩa code xử lý. Lớp thực thi có thể implement nhiều interface cùng lúc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
interface IWarrior { readonly name: string; public weapon: string; sayHi(): void; // Không thể định nghĩa code xử lý, chỉ có thể khai báo. phải được triển khai ở lớp thực thi arm(weapon: string): void; // hàm này phải được triển khai ở lớp thực thi } interface ISpecialPower { fly(): void; // hàm này phải được triển khai ở lớp thực thi } class SuperWarrior implements IWarrior, ISpecialPower { // có thể implement được nhiều interface constructor(name: string) { this.name = name; } sayHi(): void { console.log(`Hello, I am ${this.name}`); } arm(weapon): void { console.log(`${this.name} is a super warrior fighting with ${weapon}`); } fly(): void { console.log(`${this.name} can fly`); } } let hercules: IWarrior; // đúng! nếu tạo một tham chiếu với kiểu dữ liệu là interface hercules = new IWarrior(); // lỗi: không thể tạo một thể hiện của interface hercules = new SuperWarrior(); // đúng! thể hiện được tạo ra từ lớp con của interface hercules.arm(); hercules.sayHi(); hercules.fly(); // lỗi: phương thức không tồn tại trong interface IWarrior |
Đến đây thì chúng ta cũng nhận ra một số sự khác biệt giữa 2 thể hiện của tính đối tượng trong OOP là Abstract class và Interface. Sự khác nhau giữa chúng đó chính là mục đích sử dụng:
- Abstract class: là một class cha cho tất cả các class có cùng bản chất. Bản chất ở đây được hiểu là kiểu, loại, nhiệm vụ của class. Hai class cùng hiện thực một interface có thể hoàn toàn khác nhau về bản chất.
- Interface: là một chức năng mà bạn có thể thêm và bất kì class nào. Từ chức năng ở đây không đồng nghĩa với phương thức (hoặc hàm). Interface có thể bao gồm nhiều hàm/phương thức và tất cả chúng cùng phục vụ cho một chức năng.
Điểm lại một số đặc trưng của cả 2:
Features | Interface | Abstract class |
Multiple inheritance | Một class có thể hiện thực nhiều interface.(tạm coi là thừa kế) | Không hỗ trợ đa thừa kế |
Default implementation | Không thể định nghĩa code xử lý chỉ có thể khai báo. | Có thể định nghĩa thân phương thức và property. |
Access Modifiers | Mọi phương thức property đều mặc định là public. | Có thể xác định modifie. |
Adding functionality | Mọi phương thức và property của interface cần được hiện thực trong class. | Không cần thiết. |
Fields and Constants | Không | Có |
So sánh giữa Interface
và Abstract class
Nói tóm lại, việc khai báo một Abstract class hay một Interface khá đơn giản, nhưng mục đích thực sự của “Trừu tượng” này là gì? chúng ta hãy cùng đến với mục tiếp theo.
3. Món vũ khí này khi nào thì sài? tại sao nên sài nó?
Khái niệm đăng sau tính trừu tượng là chúng ta đang che dấu đi sự phức tạp của việc triển khai từ các lớp kế thừa mà chỉ cần quan tâm đến những Interface của chúng. Nếu vẫn còn khó hiểu hãy theo dõi ví dụ dưới đây.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
interface IWarrior { name: string; weapon: string; sayHi(): void; // Không thể định nghĩa code xử lý, chỉ có thể khai báo. phải được triển khai ở lớp thực thi arm(weapon: string): void; // hàm này phải được triển khai ở lớp thực thi } class SuperWarrior implements IWarrior { // có thể implement được nhiều interface constructor(name: string) { this.name = name; } sayHi(): void { console.log(`Hello, I am ${this.name}`); } arm(weapon): void { console.log(`${this.name} is a super warrior fighting with ${weapon}`); } } class SuperHero implements IWarrior { // có thể implement được nhiều interface constructor(name: string) { this.name = name; } sayHi(): void { console.log(`Hi, I am ${this.name}, ahihi`); } arm(weapon): void { console.log(`${this.name} is a super hero fighting with ${weapon}`); } fly(): void { console.log(`${this.name} can fly`); } } let superman: IWarrior; // biến hercules tham chiếu với kiểu dữ liệu là interface IWarrior superman = new SuperHero(); // nếu khởi tạo superman bằng class SuperHero thì superman là một siêu anh hùng superman = new SuperWarrior(); // nếu khởi tạo superman bằng Class SuperWarrior thì superman sẽ là siêu chiến binh superman.arm(); superman.sayHi(); superman.fly(); // lỗi: phương thức không tồn tại trong interface IWarrior |
Dễ dàng nhận thấy rằng superman được tham chiếu tới interface IWarrior, lúc này chúng ta sẽ không cần quan tâm superman sẽ là Siêu Anh Hùng hay là một Siêu Chiến Binh, chúng ta chỉ cần quan tâm interface IWarrior có 2 thuộc tính là name và weapon cùng với 2 phương thức sayHi()và arm(). Mọi chuyện đã trở nên vô cùng đơn giản.
Trong thực tế, bạn có thể bắt gặp tính trừu tượng ở đâu đó khi sử dụng các gói thư viện của bên thứ 3 (third-party). Khi bạn extends hoặc implements các Class hoặc Interface của họ, bỗng dưng hệ thống báo lỗi và bắt bạn phải override một phương thức nào của lớp đó, thì chắc chắn bạn sẽ hiểu ngay lớp đó chính là lớp trừu tượng. Như ví dụ dưới đây.
1 2 3 4 5 6 7 8 9 10 11 12 |
import { OnInit } from '@angular/core' export class PeekABoo implements OnInit { constructor(private logger: LoggerService) { } // implement phương thức `ngOnInit` của interface OnInit ngOnInit() { this.logIt(`OnInit`); } logIt(msg: string) { this.logger.log(`#${nextId++} ${msg}`); } } |
4. Tạm kết
Vậy đấy, chúng ta vừa mới biết về 1 trong 4 thứ vũ khí lợi hại mà OOP mang lại. Nếu tìm hiểu sâu hơn nữa, chúng ta sẽ thấy tính trừu tượng được áp dụng rất nhiều trong Design Pattern và thường xuyên được các cao thủ mang ra để lòe thiên hạ.
Bài viết vẫn còn khá nhiều thiếu sót trong cách trình bày và chỉ là cách hiểu của mình về OOP, các bạn có ý kiến đóng góp thì đừng ngại để lại comment dưới phần bình luận nhé.
Phần 2: Encapsulation – Tuyệt kỹ ẩn thân.
Lê Xuân Quỳnh – Developer @ JANETO
TRUNG TÂM ĐÀO TẠO LẬP TRÌNH VIÊN JANETO
THÔNG TIN LIÊN HỆ
Hotline: 0933 06 7997 – 0933267337
Fanpage: facebook.com/laptrinhvienio
Channel: YouTube/laptrinhvienio
Email: tuyensinh@laptrinhvien.io
Địa chỉ: Tầng 2, Tòa nhà The Morning Star – 57 Quốc lộ 13, Phường 26, Quận Bình Thạnh, Tp. Hồ Chí Minh.