Bài 12.2: Classes và access modifiers trong TypeScript

Chào mừng trở lại với series Lập trình Web Front-end! Trong bài viết này, chúng ta sẽ cùng nhau đi sâu vào hai khái niệm cực kỳ quan trọng trong TypeScript (và các ngôn ngữ lập trình hướng đối tượng khác): ClassesAccess Modifiers. Đây là những công cụ mạnh mẽ giúp chúng ta tổ chức code một cách hiệu quả, dễ bảo trì và an toàn hơn rất nhiều.

Tại sao lại cần Classes?

Khi ứng dụng của chúng ta ngày càng phát triển, việc quản lý code trở nên phức tạp hơn. Thay vì viết các hàm và dữ liệu rời rạc, chúng ta cần một cách để đóng gói (encapsulate) dữ liệu và hành vi liên quan lại với nhau. Đó chính là lúc Classes tỏa sáng!

Trong lập trình hướng đối tượng (OOP), một Class giống như một bản thiết kế (blueprint) để tạo ra các đối tượng (objects). Nó định nghĩa các thuộc tính (properties) - dữ liệu mà đối tượng sẽ có, và các phương thức (methods) - hành vi mà đối tượng có thể thực hiện.

TypeScript hỗ trợ mạnh mẽ cú pháp Class tương tự như các ngôn ngữ OOP khác như Java hay C#.

Định nghĩa Class trong TypeScript

Cú pháp cơ bản để định nghĩa một Class trong TypeScript rất trực quan:

class TenClassCuaBan {
    // Thuộc tính (Properties)
    thuocTinh1: kieuDuLieu;
    thuocTinh2: kieuDuLieu = giaTriKhoiTao;

    // Constructor (Hàm tạo) - Tùy chọn
    constructor(thamSo1: kieuDuLieu, thamSo2: kieuDuLieu) {
        // Khởi tạo thuộc tính cho đối tượng
        this.thuocTinh1 = thamSo1;
        this.thuocTinh2 = thamSo2;
    }

    // Phương thức (Methods)
    phuongThucCuaBan(thamSo: kieuDuLieu): kieuDuLieuTraVe {
        // Logic xử lý của phương thức
        console.log("Đây là một phương thức");
        return motGiaTri;
    }
}

Hãy xem một ví dụ cụ thể với Class Car (Xe hơi):

class Car {
    // Properties (Thuộc tính)
    make: string; // Hãng xe
    model: string; // Mẫu xe
    year: number; // Năm sản xuất

    // Constructor (Hàm tạo)
    constructor(make: string, model: string, year: number) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // Methods (Phương thức)
    startEngine(): void {
        console.log(`${this.make} ${this.model} động cơ đã khởi động!`);
    }

    getCarInfo(): string {
        return `Xe: ${this.year} ${this.make} ${this.model}`;
    }
}

Trong ví dụ trên:

  • Chúng ta định nghĩa Class Car với ba thuộc tính: make, model, và year.
  • Có một constructor nhận ba tham số tương ứng và gán giá trị cho các thuộc tính của đối tượng mới được tạo ra (sử dụng this).
  • Có hai phương thức: startEngine (không trả về giá trị, nên kiểu trả về là void) và getCarInfo (trả về một chuỗi string).

Để sử dụng Class này, chúng ta tạo đối tượng (instance) từ nó bằng từ khóa new:

const myCar = new Car("Toyota", "Camry", 2022);

console.log(myCar.make); // Output: Toyota
console.log(myCar.getCarInfo()); // Output: Xe: 2022 Toyota Camry
myCar.startEngine(); // Output: Toyota Camry động cơ đã khởi động!

Đối tượng myCar được tạo ra dựa trên bản thiết kế Car, mang đầy đủ các thuộc tính và phương thức đã được định nghĩa.

Access Modifiers: Người Gác Cổng Của Dữ Liệu

Khi làm việc với Class, chúng ta cần kiểm soát việc ai (hoặc phần nào của code) có thể truy cập và thay đổi các thuộc tính hoặc gọi các phương thức của đối tượng. Đây là mục đích chính của Access Modifiers (Bộ điều chỉnh truy cập). Chúng giúp chúng ta thực hiện nguyên tắc đóng gói (encapsulation) trong OOP, bảo vệ dữ liệu nội bộ của đối tượng khỏi bị truy cập hoặc sửa đổi một cách không kiểm soát.

TypeScript cung cấp ba loại Access Modifiers chính:

  1. public
  2. private
  3. protected

Hãy cùng tìm hiểu chi tiết từng loại.

1. public
  • Đây là mặc định nếu bạn không chỉ định Access Modifier nào.
  • Các thành viên (thuộc tính hoặc phương thức) được đánh dấu là public có thể được truy cập từ mọi nơi: bên trong Class, từ các thể hiện (instance) bên ngoài Class, và từ các Class con (derived classes).

Ví dụ:

class PublicExample {
    public publicProperty: string;

    public constructor(value: string) {
        this.publicProperty = value;
    }

    public publicMethod(): void {
        console.log("Đây là phương thức Public.");
        console.log("Có thể truy cập publicProperty:", this.publicProperty);
    }
}

const instance1 = new PublicExample("Tôi là dữ liệu Public");

console.log(instance1.publicProperty); // ✅ Có thể truy cập từ bên ngoài
instance1.publicMethod(); // ✅ Có thể gọi từ bên ngoài

// Mặc định là public:
class AnotherPublicExample {
    // Không cần public, đây là mặc định
    defaultPublicProperty: string = "Tôi cũng là Public";

    defaultPublicMethod(): void {
        console.log("Đây là phương thức Public (mặc định).");
    }
}

const instance2 = new AnotherPublicExample();
console.log(instance2.defaultPublicProperty); // ✅ Có thể truy cập
instance2.defaultPublicMethod(); // ✅ Có thể gọi

Các thành viên public giống như biển hiệu hoặc cánh cửa mở của ngôi nhà Class, ai cũng có thể nhìn thấy và tương tác trực tiếp.

2. private
  • Các thành viên được đánh dấu là private chỉ có thể được truy cập từ bên trong Class nơi chúng được định nghĩa.
  • Chúng không thể được truy cập từ các thể hiện (instance) bên ngoài Class, và cũng không thể được truy cập từ các Class con kế thừa.
  • Sử dụng private giúp bảo vệ dữ liệu nội bộ, chỉ cho phép thay đổi thông qua các phương thức công khai (public methods) mà Class cung cấp. Đây là nguyên tắc đóng gói mạnh mẽ nhất.

Ví dụ:

class PrivateExample {
    private privateProperty: number; // Dữ liệu riêng tư

    constructor(initialValue: number) {
        this.privateProperty = initialValue;
    }

    // Phương thức public để cho phép truy cập giá trị private một cách có kiểm soát
    public getPrivateValue(): number {
        console.log("Truy cập privateProperty từ bên trong Class.");
        return this.privateProperty;
    }

    // Phương thức private chỉ sử dụng nội bộ
    private internalMethod(): void {
        console.log("Đây là phương thức Private chỉ dùng nội bộ.");
        // Có thể truy cập privateProperty từ đây
        console.log("privateProperty trong internalMethod:", this.privateProperty);
    }

    public demonstrateInternalUse(): void {
        this.internalMethod(); // ✅ Có thể gọi phương thức private từ bên trong Class
    }
}

const instance3 = new PrivateExample(42);

// console.log(instance3.privateProperty); // ❌ LỖI: Property 'privateProperty' is private and only accessible within class 'PrivateExample'.
// instance3.internalMethod(); // ❌ LỖI: Property 'internalMethod' is private and only accessible within class 'PrivateExample'.

console.log(instance3.getPrivateValue()); // ✅ OK: Truy cập thông qua phương thức public
instance3.demonstrateInternalUse(); // ✅ OK: Gọi phương thức public để kích hoạt phương thức private

Thành viên private giống như những căn phòng bí mật trong ngôi nhà Class, chỉ có "người chủ" (code bên trong Class) mới biết và có chìa khóa để vào. Người ngoài chỉ có thể tương tác thông qua các cửa công khai (public methods).

3. protected
  • Các thành viên được đánh dấu là protected có thể được truy cập từ bên trong Class nơi chúng được định nghĩa, và từ bên trong các Class con (Class kế thừa từ Class gốc).
  • Chúng không thể được truy cập từ các thể hiện (instance) bên ngoài Class hierarchy (hệ thống phân cấp Class).
  • protected thường được sử dụng khi bạn muốn chia sẻ một số thành viên nội bộ với các Class con, nhưng vẫn muốn ẩn chúng khỏi thế giới bên ngoài.

Để minh họa protected, chúng ta cần sử dụng khái niệm kế thừa Class (sẽ được nói sâu hơn trong bài khác, nhưng hiểu đơn giản là Class con kế thừa tất cả thành viên của Class cha).

Ví dụ:

class BaseClass {
    protected protectedProperty: string; // Dữ liệu được bảo vệ

    constructor(value: string) {
        this.protectedProperty = value;
    }

    protected protectedMethod(): void { // Phương thức được bảo vệ
        console.log("Đây là phương thức Protected từ BaseClass.");
        console.log("Có thể truy cập protectedProperty từ BaseClass:", this.protectedProperty);
    }
}

class DerivedClass extends BaseClass { // DerivedClass kế thừa từ BaseClass
    constructor(value: string) {
        super(value); // Gọi constructor của Class cha
    }

    public accessProtectedMembers(): void {
        // ✅ Có thể truy cập protectedProperty từ DerivedClass (là Class con)
        console.log("Truy cập protectedProperty từ DerivedClass:", this.protectedProperty);

        // ✅ Có thể gọi protectedMethod từ DerivedClass
        this.protectedMethod();
    }

    // ❌ Không thể truy cập private members của BaseClass (nếu có)
    // private basePrivateProperty: number; // Giả định BaseClass có private member
    // this.basePrivateProperty; // Sẽ báo lỗi
}

const instance4 = new DerivedClass("Tôi là dữ liệu Protected");

// console.log(instance4.protectedProperty); // ❌ LỖI: Property 'protectedProperty' is protected and only accessible within class 'BaseClass' and its subclasses.
// instance4.protectedMethod(); // ❌ LỖI: Property 'protectedMethod' is protected and only accessible within class 'BaseClass' and its subclasses.

instance4.accessProtectedMembers(); // ✅ OK: Gọi phương thức public để truy cập các thành viên protected từ bên trong DerivedClass

Thành viên protected giống như một cửa riêng trong ngôi nhà Class, chỉ những "thành viên trong gia đình" (các Class con kế thừa) mới có thể sử dụng, còn người lạ (các instance bên ngoài) thì không.

Tóm Lược về Access Modifiers

Modifier Truy cập từ cùng Class Truy cập từ Class con Truy cập từ Instance bên ngoài
public ✅ Có ✅ Có ✅ Có
private ✅ Có ❌ Không ❌ Không
protected ✅ Có ✅ Có ❌ Không

Tại sao Access Modifiers lại quan trọng?

Sử dụng Access Modifiers không chỉ là tuân thủ các nguyên tắc OOP, mà còn mang lại những lợi ích thiết thực:

  • Đóng gói (Encapsulation): Giúp nhóm dữ liệu và hành vi liên quan lại với nhau, đồng thời ẩn đi chi tiết triển khai nội bộ. Điều này giúp Class trở thành một "hộp đen" với giao diện công khai rõ ràng.
  • Kiểm soát truy cập: Ngăn chặn việc dữ liệu quan trọng bị truy cập hoặc sửa đổi ngẫu nhiên từ bên ngoài. Buộc người dùng Class phải sử dụng các phương thức được cung cấp để tương tác với dữ liệu nội bộ.
  • Dễ bảo trì và phát triển: Khi thay đổi cách một Class xử lý dữ liệu nội bộ (private), miễn là giao diện public không đổi, code sử dụng Class đó sẽ không bị ảnh hưởng. Điều này giúp việc refactor và mở rộng code dễ dàng hơn nhiều.
  • Tăng tính dễ hiểu: Rõ ràng chỉ ra phần nào của Class là dành cho sử dụng công khai và phần nào là dành cho nội bộ.

Comments

There are no comments at the moment.