1. Giới thiệu
Lớp trừu tượng — là lớp mà bạn không thể tạo trực tiếp bằng new. Nó dùng để làm nền tảng cho các lớp cụ thể hơn được tạo ra từ nó. Kiểu như một bản thiết kế hoặc "bản vẽ", định nghĩa cấu trúc và hành vi chung cho các lớp con, nhưng để chi tiết implement thì để cho lớp con tự xử lý.
Đây là một ví dụ dễ hiểu: tưởng tượng bạn có khái niệm "Phương tiện di chuyển". Ở ngoài đời chẳng ai đi trên "Phương tiện trừu tượng" cả (trừ khi trong tưởng tượng). Có người đi ô tô, có người đi tàu, có người bay máy bay. Lớp trừu tượng là bản vẽ "Phương tiện", mô tả rằng bất kỳ phương tiện nào cũng phải có khả năng di chuyển (Move), nhưng cách di chuyển thì phải để lớp con tự định nghĩa.
// Ví dụ lớp trừu tượng
public abstract class Transport
{
public string Model { get; set; }
// Phương thức trừu tượng — không có thân hàm
public abstract void Move();
}
Nếu bạn thử tạo object kiểu Transport, bạn sẽ bị lỗi compile! Thử đi — compiler sẽ báo ngay kiểu như "không thể tạo instance của lớp trừu tượng".
Khi nào và tại sao cần lớp trừu tượng?
- Muốn gom các đặc điểm và hành vi chung của nhiều object giống nhau vào một chỗ.
- Cần đặt ra "hợp đồng chung": ví dụ, "mọi lớp con đều phải biết di chuyển".
- Có một số method đã implement chung, nhưng một phần logic cần lớp con tự làm rõ.
Lớp trừu tượng là công cụ xịn để thiết kế kiến trúc hệ thống lớn, nơi có những "tính năng" chung cho tất cả, nhưng chi tiết thì mỗi lớp con tự xử lý.
2. Khác biệt giữa lớp trừu tượng và lớp thường
Chỗ này hay bị hỏi, nên mình sẽ giải thích rõ ràng nhé!
Lớp trừu tượng vs Lớp thường
- Lớp thường có thể tạo bằng new, có thể có tất cả method đã implement.
- Lớp trừu tượng:
- không thể tạo trực tiếp (abstract trong khai báo class)
- có thể chứa cả method đã implement lẫn method trừu tượng (không có thân hàm)
- có thể có field, property, constructor và cả static method
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine("Động vật đang ngủ!");
}
public abstract void MakeSound();
}
3. Phương thức và thuộc tính trừu tượng
Phương thức trừu tượng
Phương thức trừu tượng — khai báo trong lớp trừu tượng, nhưng không có thân hàm (không có implement). Lớp con bắt buộc phải implement nó theo ý mình.
public abstract class Animal
{
public string Name { get; set; }
// Phương thức trừu tượng
public abstract void MakeSound();
}
Thuộc tính trừu tượng
Bạn cũng có thể khai báo thuộc tính trừu tượng, để tất cả lớp con bắt buộc phải implement:
public abstract class Shape
{
// Thuộc tính trừu tượng
public abstract double Area { get; }
}
4. Kế thừa từ lớp trừu tượng
Lớp trừu tượng có thể có cả method trừu tượng lẫn method đã implement. Lớp con bắt buộc phải implement tất cả method và property trừu tượng, nếu không thì nó cũng phải là lớp trừu tượng.
Giờ mình sẽ phát triển tiếp "app động vật" đã bắt đầu ở bài trước:
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name} ngủ ngon!");
}
public abstract void MakeSound();
}
Giờ tạo một class thường, không trừu tượng:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Gâu!");
}
}
Thử không implement MakeSound() — bạn sẽ thấy lỗi compile ngay.
Sơ đồ: hệ thống class với lớp trừu tượng
+---------------------+
| Animal (abstract) |
+---------------------+
| Name: string |
| + Sleep() |
| + MakeSound()* |
+---------------------+
^
|
+------+-------+
| |
Dog Cat
* MakeSound() — phương thức trừu tượng
5. Đa hình thực tế: abstraction hoạt động thế nào
Áp dụng luôn vào mini-app nhé. Giả sử bạn có danh sách động vật — ví dụ chó và mèo.
List<Animal> animals = new List<Animal>
{
new Dog { Name = "Sharik" },
new Cat { Name = "Barsik" }
};
foreach (var animal in animals)
{
animal.MakeSound(); // Gọi đa hình: mỗi con sẽ chạy method riêng
animal.Sleep(); // Gọi method đã implement ở lớp cha
}
Class Cat cũng implement tương tự:
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Meo!");
}
}
Lợi ích thực tế trong dự án thật
Lớp trừu tượng dùng rất nhiều trong kiến trúc app lớn. Ví dụ, trong phần mềm vẽ thường có lớp trừu tượng Shape với method kiểu Draw() và property kiểu Area. Các hình cụ thể (hình chữ nhật, hình tròn) sẽ tự implement các method trừu tượng này bằng công thức riêng. Nhờ vậy bạn có thể tạo nhiều loại hình khác nhau mà không phải viết lại toàn bộ logic chương trình.
6. Đặc điểm và giới hạn của lớp trừu tượng
Không thể tạo instance
Compiler sẽ cấm bạn làm như này:
Animal a = new Animal(); // Lỗi!
Chỉ có thể tạo instance từ lớp con (cụ thể).
Lớp trừu tượng có thể chứa:
- Khai báo method trừu tượng (không có implement)
- Method đã implement (có thân hàm)
- Property và field (trừu tượng hoặc đã implement)
- Constructor — đúng, lớp trừu tượng có thể có constructor, nhưng chỉ lớp con mới gọi được, không phải code tạo object trực tiếp.
Lớp trừu tượng hỗ trợ kế thừa
Bạn có thể xây dựng cả hệ thống abstraction:
public abstract class Creature
{
public abstract void Live();
}
public abstract class Animal : Creature
{
public abstract override void Live();
public abstract void MakeSound();
}
Nếu trong chuỗi kế thừa mà class không implement hết method trừu tượng, thì nó cũng phải khai báo là abstract!
7. Lớp trừu tượng và constructor
Lớp trừu tượng có thể có constructor, sẽ được gọi từ constructor của lớp con.
Lưu ý quan trọng: constructor của lớp trừu tượng thường khai báo là protected, để nhấn mạnh chỉ dùng cho lớp con, không phải để tạo instance trực tiếp.
public abstract class Animal
{
public string Name { get; set; }
// Constructor khai báo protected
protected Animal(string name)
{
Name = name;
Console.WriteLine($"Đã tạo động vật tên {name}");
}
public abstract void MakeSound();
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
// base(name) sẽ gọi constructor Animal
Console.WriteLine("Đã tạo chó");
}
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Gâu!");
}
}
Tại sao dùng constructor protected?
- Rõ ràng ý định: protected cho thấy constructor chỉ dành cho lớp con
- Tránh lỗi: dù compiler đã cấm tạo instance bằng new, protected constructor càng làm rõ ý định ở API class, tránh hiểu nhầm
- Best practice: tuân thủ nguyên tắc đóng gói và API rõ ràng
Bạn cũng có thể dùng constructor public trong lớp trừu tượng, nhưng protected sẽ hợp lý hơn với mục đích của nó.
8. So sánh phương thức trừu tượng và virtual
Phương thức trừu tượng là một dạng của virtual method (thực ra luôn virtual, dù không ghi từ đó), nhưng không được phép có implement. Nếu bạn muốn method nào đó lớp con có thể override (nhưng không bắt buộc), dùng virtual. Nếu bắt buộc — chỉ dùng abstract.
public abstract class Worker
{
// Bắt buộc phải implement
public abstract void Work();
// Có thể override, không bắt buộc
public virtual void Rest()
{
Console.WriteLine("Nghỉ tiêu chuẩn!");
}
}
| Phương thức trừu tượng | Phương thức virtual | |
|---|---|---|
| Có implement ở lớp cha không | Không, chỉ có khai báo (không có thân hàm) | Có, có implement mặc định |
| Có thể khai báo ở đâu | Chỉ trong lớp trừu tượng | Bất kỳ class nào không sealed, kể cả abstract |
| Lớp con bắt buộc override không | Bắt buộc với lớp con đầu tiên không abstract | Không bắt buộc. Có thể override hoặc dùng luôn implement cha |
| Có thể gọi trực tiếp không | Không (không có thân hàm) | Có (chạy version của lớp cha) |
| Mục đích chính | Bắt lớp con phải tự implement. Định nghĩa "hợp đồng" | Cho phép lớp con thay đổi hành vi nếu muốn |
9. Ứng dụng lớp trừu tượng trong project của bạn
Tiếp tục phát triển app học tập. Giả sử giờ bạn cần lớp trừu tượng để xử lý input của user (ví dụ các form khác nhau).
public abstract class InputForm
{
public string Title { get; set; }
// Phương thức trừu tượng, xử lý nhập liệu
public abstract void ShowAndHandleInput();
}
public class LoginForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("Nhập login: ");
string login = Console.ReadLine();
Console.Write("Nhập mật khẩu: ");
string password = Console.ReadLine();
Console.WriteLine("Xác thực xong!");
}
}
public class RegistrationForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("Tạo login: ");
string login = Console.ReadLine();
Console.Write("Tạo mật khẩu: ");
string password = Console.ReadLine();
Console.WriteLine("Đăng ký xong!");
}
}
Cách dùng:
InputForm[] forms = new InputForm[]
{
new LoginForm { Title = "Đăng nhập" },
new RegistrationForm { Title = "Đăng ký" }
};
foreach (var form in forms)
{
form.ShowAndHandleInput();
}
Cách này càng mạnh khi app của bạn càng lớn. Lớp trừu tượng là nền tảng tuyệt vời để xây dựng hệ thống mở rộng, nơi nguyên tắc DRY và SOLID (không lặp code, viết module, dễ mở rộng) là bạn thân nhất.
10. Lỗi thường gặp khi làm việc với lớp và phương thức trừu tượng
Lỗi #1: Quên implement method trừu tượng.
Nếu lớp con không implement hết method và property trừu tượng, compiler sẽ báo lỗi. Lỗi này rất hay gặp, nhất là khi làm với code người khác hoặc hệ thống class lớn. Hãy kiểm tra kỹ xem có bỏ sót gì không.
Lỗi #2: Cố gắng vừa abstract vừa static cho một method.
Không được đâu. Method trừu tượng là để implement ở instance, còn static thì không cần instance. Hai cái này trái ngược nhau, compiler sẽ không cho bạn compile.
Lỗi #3: "Lười" không implement hết các thành phần trừu tượng.
Nếu bạn không muốn implement hết method trừu tượng, bạn phải khai báo class đó cũng là abstract. Không phải lỗi nghiêm trọng, nhưng có thể là dấu hiệu kiến trúc rối. Đôi khi nên tách một phần implement vào lớp trừu tượng trung gian để lớp con dễ thở hơn.
GO TO FULL VERSION