1. Hai mặt của một vấn đề
Tụi mình đã đào khá sâu vào khái niệm trừu tượng và đã xem qua hai công cụ cực mạnh trong C# – abstract class và interface. Chắc bạn cũng cảm nhận được chúng khá giống nhau, vì cả hai đều cho phép định nghĩa "bộ khung" hành vi mà các class cụ thể sau này phải hiện thực. Nhưng tin mình đi, so sánh chúng cũng như so búa với tua vít: đều là công cụ, nhưng dùng cho mục đích khác nhau.
Bắt đầu với câu hỏi lớn: tại sao lại cần hai công cụ cho một việc? Trong lập trình cũng như ngoài đời, hiếm khi có chuyện "tự nhiên mà có". Nếu có hai công cụ giống nhau, chắc chắn mỗi cái có điểm mạnh riêng và phạm vi sử dụng riêng.
Giờ mình sẽ cho bạn một bảng so sánh nhanh để bạn dễ hình dung tổng thể. Đây kiểu như "phao cứu sinh" giúp bạn nắm được những điểm quan trọng nhất.
| Đặc điểm | Abstract class | Interface |
|---|---|---|
| Tạo instance | Không thể tạo trực tiếp (). |
Không thể tạo trực tiếp (). |
| Triển khai thành viên | Có thể có: – Method/property đã hiện thực. – Method/property abstract (chưa hiện thực). – Field, constructor, static method. |
Có thể có method với default implementation, static abstract và static non-abstract member. Không có instance field và constructor. |
| Access modifier | Có thể có bất kỳ (public, protected, internal, private). | Tất cả member trong interface mặc định là public. Không ghi access modifier (trừ method có default implementation và static member). |
| Kế thừa | Class chỉ kế thừa được một abstract class. | Class có thể implement nhiều interface. |
| Kiểu quan hệ | "Là một" (is-a). Xác định kiểu cơ sở và phần chung của hệ thống phân cấp. | "Có thể làm" (has-a hoặc can-do). Xác định contract hành vi/năng lực. |
| Trạng thái | Có thể lưu trạng thái (instance field). | Có thể có static field, nhưng không có instance field. |
| Mở rộng | Có thể thêm method đã hiện thực mà không ảnh hưởng đến class con. | Có thể thêm method với default implementation mà không làm hỏng class con. |
Thấy ấn tượng chưa? Giờ mình sẽ phân tích kỹ hơn từng điểm nhé.
2. Kế thừa đa cấp
Đây chắc là điểm khác biệt dễ nhớ nhất. Trong C#, cũng như nhiều ngôn ngữ khác (ví dụ Java), một class chỉ kế thừa được một class cha. Dù là class thường hay abstract class – cũng chỉ một thôi! Điều này để tránh "vấn đề kim cương" nổi tiếng (Diamond Problem), khi kế thừa nhiều class mà các method trùng tên thì không biết dùng cái nào.
Còn interface thì class có thể implement bao nhiêu cũng được! Hãy tưởng tượng class của bạn là một người. Người đó có thể là "sinh viên" (kế thừa từ class Student), nhưng đồng thời "biết nấu ăn" (ICookable), "biết lái xe" (IDriveable) và "biết hát" (ISingable). Quá linh hoạt luôn!
abstract class Animal
{
public string Name;
public abstract void MakeSound();
public void Eat() => Console.WriteLine($"{Name} ăn.");
}
interface IFlyable { void Fly(); double MaxFlyingAltitude { get; } }
interface ISwimable { void Swim(); }
class Duck : Animal, IFlyable, ISwimable
{
public double MaxFlyingAltitude => 1000;
public override void MakeSound() => Console.WriteLine($"{Name} kêu quạc quạc!");
public void Fly() => Console.WriteLine($"{Name} bay!");
public void Swim() => Console.WriteLine($"{Name} bơi!");
}
class Program
{
static void Main()
{
var duck = new Duck { Name = "Donald" };
duck.MakeSound();
duck.Eat();
duck.Fly();
duck.Swim();
// Làm việc với object qua các kiểu khác nhau:
Animal a = duck; a.Eat();
IFlyable f = duck; f.Fly();
ISwimable s = duck; s.Swim();
}
}
Nếu class của bạn cần là một phần của hệ thống phân cấp (ví dụ, Dog là Animal), hãy kế thừa từ class (abstract hoặc thường). Nếu class cần có năng lực nào đó (ví dụ, Dog có thể chạy, Cat có thể chạy), mà năng lực này không gắn với một hệ thống phân cấp chặt chẽ, hãy dùng interface. Đây chính là điểm mạnh nhất của interface – cho phép tạo contract cho nhiều class khác nhau, không liên quan gì đến nhau.
3. Dữ liệu ở đâu, lời hứa ở đâu?
Abstract class có thể chứa bất cứ gì:
- Method/property thường (không abstract) đã hiện thực đầy đủ.
- Method/property abstract chưa hiện thực (để override).
- Field (biến instance) lưu trạng thái object.
- Constructor để khởi tạo trạng thái.
- Thậm chí static method/property.
- Và tất nhiên, có thể có mọi access modifier: public, protected, private v.v.
Điều này làm cho abstract class cực mạnh để định nghĩa hành vi đã hiện thực một phần và trạng thái chung cho tất cả class con.
abstract class Employee
{
public string FirstName, LastName;
public decimal Salary { get; protected set; }
public Employee(string first, string last) { FirstName = first; LastName = last; }
public void GetPaid(decimal sum)
{
Salary += sum;
Console.WriteLine($"{FirstName} {LastName} nhận {sum:C}. Lương: {Salary:C}");
}
public abstract void PerformWork();
public abstract void TakeBreak();
}
class Developer : Employee
{
public Developer(string f, string l) : base(f, l) { }
public override void PerformWork() => Console.WriteLine($"{FirstName} viết code.");
public override void TakeBreak() => Console.WriteLine($"{FirstName} uống cà phê.");
}
class Tester : Employee
{
public Tester(string f, string l) : base(f, l) { }
public override void PerformWork() => Console.WriteLine($"{FirstName} tìm bug.");
public override void TakeBreak() => Console.WriteLine($"{FirstName} đá bóng.");
}
class Program
{
static void Main()
{
Employee[] team = {
new Developer("Ivan", "Petrov"),
new Tester("Maria", "Sidorova")
};
foreach (var emp in team)
{
Console.WriteLine($"\n--- Ngày làm việc của {emp.FirstName} {emp.LastName} ---");
emp.PerformWork();
emp.GetPaid(2000);
emp.TakeBreak();
}
}
}
Interface – lại là chuyện khác. Trước C# 8, interface chỉ có thể chứa khai báo method, property, indexer và event. Không có field, không có constructor, không có code hiện thực! Chỉ là "chữ ký" method mà không có thân. Và tất cả member mặc định là public (dù bạn không ghi public). Điều này đảm bảo interface là contract thuần túy, không có chi tiết hiện thực hay trạng thái ẩn.
Từ C# 8 trở đi, interface "mập" hơn chút, có thể có method với default implementation (Default Interface Methods) và static member. Điều này giúp thêm method mới vào interface cũ mà không "phá" hàng triệu dòng code đã implement nó. Nhưng dù vậy, interface vẫn không có instance field và constructor. Đây là giới hạn then chốt để interface luôn là "contract hành vi", không phải "kho dữ liệu".
interface ISaveable
{
void Save(string file);
bool IsDirty { get; }
}
interface ILoadable
{
void Load(string file);
}
class GameProgress : ISaveable, ILoadable
{
public int Level { get; set; }
public string PlayerName { get; set; }
bool _isDirty = true;
public bool IsDirty => _isDirty;
public GameProgress(string name, int level)
{
PlayerName = name; Level = level;
}
public void Save(string file)
{
Console.WriteLine($"Lưu: {PlayerName}, level {Level} -> {file}");
_isDirty = false;
}
public void Load(string file)
{
Console.WriteLine($"Tải từ {file}");
PlayerName = "Người chơi mới"; Level = 5; _isDirty = true;
}
public void UpdateProgress(int newLevel)
{
Level = newLevel; _isDirty = true;
Console.WriteLine($"Cập nhật lên {Level}.");
}
}
class Program
{
static void Main()
{
var game = new GameProgress("Anh hùng", 1);
game.UpdateProgress(3);
ISaveable saver = game;
if (saver.IsDirty) saver.Save("save.dat");
ILoadable loader = game;
loader.Load("save.dat");
Console.WriteLine($"Sau khi tải: {game.PlayerName}, {game.Level}");
}
}
Kết luận: Nếu bạn cần một class cơ sở cung cấp không chỉ contract mà còn code đã hiện thực (logic chung) hoặc lưu trạng thái chung, hãy chọn abstract class. Nếu bạn chỉ cần "checklist" hoặc "hợp đồng" hành vi, không cần code hiện thực hay trạng thái, interface là lựa chọn của bạn.
4. Khi nào dùng interface?
Interface cực hợp khi bạn muốn định nghĩa năng lực hoặc hành vi mà nhiều object khác nhau, không liên quan gì nhau, đều có thể có. Ví dụ:
- IDisposable: bất kỳ object nào cần "giải phóng" sau khi dùng (file, kết nối mạng, database).
- IEnumerable<T>: bất kỳ object nào có thể lặp qua bằng foreach.
- IComparable<T>: bất kỳ object nào có thể so sánh với object cùng loại.
Điều quan trọng là FileStream và SqlConnection đều có thể implement IDisposable, còn List<T> và Dictionary<TKey, TValue> đều có thể implement IEnumerable<T>. Các class này thuộc các hệ thống phân cấp hoàn toàn khác nhau, nhưng lại có năng lực chung do interface định nghĩa.
Ví dụ: Hãy tưởng tượng hệ thống có Xe hơi và Máy bay. Cả hai đều có thể là PhươngTiệnGiaoThông (có thể là abstract class). Nhưng cả Xe hơi, Máy bay, thậm chí Thuyền (nếu bạn thêm vào) – đều có thể di chuyển. Năng lực "di chuyển" này là ứng viên tuyệt vời cho interface IMovable.
interface IMovable
{
void Move(int distance);
}
class Car : IMovable
{
public string Brand;
public Car(string brand) => Brand = brand;
public void Move(int d) => Console.WriteLine($"{Brand} đi {d} km.");
}
class Airplane : IMovable
{
public string Model;
public Airplane(string model) => Model = model;
public void Move(int d) => Console.WriteLine($"{Model} bay {d} km.");
}
class Human : IMovable
{
public string Name;
public Human(string name) => Name = name;
public void Move(int d) => Console.WriteLine($"{Name} đi bộ {d} m.");
}
class Program
{
static void Main()
{
IMovable[] movers = { new Car("Toyota"), new Airplane("Boeing 747"), new Human("Arthur") };
foreach (var item in movers)
item.Move(100);
}
}
Thấy chưa? Bạn có thể tạo list IMovable và gọi method Move() cho từng phần tử mà không cần biết đó là Car, Airplane hay Human. Đó chính là sức mạnh của polymorphism qua interface.
5. Khi nào dùng abstract class?
Abstract class cực hợp khi bạn muốn định nghĩa chức năng cơ bản chung cho nhóm class liên quan chặt chẽ, mà các class đó là các biến thể của một thứ chung. Nó cung cấp sẵn code mà tất cả class con đều cần, đồng thời bắt buộc class con hiện thực phần riêng biệt.
Hãy tưởng tượng bạn có nhóm các loại tài khoản ngân hàng: SavingAccount (tiết kiệm), CheckingAccount (thanh toán), CreditAccount (tín dụng). Tất cả là BankAccount. Tất cả đều có số dư (Balance), đều có thể nạp tiền (Deposit). Nhưng quy tắc rút tiền (Withdraw) thì mỗi loại khác nhau. Lúc này abstract class BankAccount là cứu tinh!
abstract class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; protected set; }
public BankAccount(string acc) { AccountNumber = acc; }
public void Deposit(decimal sum)
{
if (sum > 0)
{
Balance += sum;
Console.WriteLine($"{AccountNumber}: +{sum:C}, Số dư: {Balance:C}");
}
}
public abstract bool Withdraw(decimal sum);
}
class CheckingAccount : BankAccount
{
public CheckingAccount(string acc) : base(acc) { }
public override bool Withdraw(decimal sum)
{
if (Balance >= sum)
{
Balance -= sum;
Console.WriteLine($"{AccountNumber}: -{sum:C}, Số dư: {Balance:C}");
return true;
}
Console.WriteLine($"{AccountNumber}: Không đủ tiền");
return false;
}
}
class CreditAccount : BankAccount
{
public decimal CreditLimit { get; }
public CreditAccount(string acc, decimal limit) : base(acc) => CreditLimit = limit;
public override bool Withdraw(decimal sum)
{
if (Balance - sum >= -CreditLimit)
{
Balance -= sum;
Console.WriteLine($"{AccountNumber}: -{sum:C}, Số dư: {Balance:C}");
return true;
}
Console.WriteLine($"{AccountNumber}: Vượt quá hạn mức");
return false;
}
}
class Program
{
static void Main()
{
var checking = new CheckingAccount("12345");
checking.Deposit(1000);
checking.Withdraw(300);
checking.Withdraw(800);
Console.WriteLine("\n--- Tài khoản tín dụng ---");
var credit = new CreditAccount("67890", 500);
credit.Deposit(200);
credit.Withdraw(400);
credit.Withdraw(400);
}
}
Ở đây BankAccount cung cấp logic nạp tiền (Deposit) và quản lý số tài khoản, số dư. Nhưng logic rút tiền (Withdraw) thì khác nhau, nên để abstract.
6. Khi kết hợp cả hai: cặp đôi hoàn hảo
Thiết kế đẹp và mạnh nhất thường là kết hợp abstract class và interface. Abstract class có thể tự implement một hoặc nhiều interface!
Hãy tưởng tượng: bạn có abstract Animal định nghĩa những thứ cơ bản. Nhưng một số Animal có thể là IMovable, ICarnivore, IPredator v.v. Animal thậm chí có thể cung cấp sẵn code cho IMovable (ví dụ method Move(int speed)), nhưng các class cụ thể như Lion hay Fish sẽ override để di chuyển theo cách riêng.
public interface ISaveable
{
void SaveState(string path);
bool HasChanges { get; }
}
public class Vector3
{
public float X, Y, Z;
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
public override string ToString() => $"({X}, {Y}, {Z})";
}
public abstract class GameObject
{
public string Id { get; }
public Vector3 Position { get; }
protected GameObject(string id, Vector3 pos) { Id = id; Position = pos; }
public abstract void Update();
public void Destroy() => Console.WriteLine($"{Id} bị phá hủy");
}
public class Player : GameObject, ISaveable
{
public int Health { get; private set; }
private bool _hasChanges = true;
public bool HasChanges => _hasChanges;
public Player(string id, Vector3 pos, int health) : base(id, pos) => Health = health;
public override void Update() =>
Console.WriteLine($"{Id} cập nhật, HP: {Health}, Pos: {Position}");
public void TakeDamage(int dmg)
{
Health -= dmg;
_hasChanges = true;
Console.WriteLine($"{Id} nhận {dmg} sát thương. HP: {Health}");
}
public void SaveState(string path)
{
Console.WriteLine($"Lưu {Id} (HP:{Health}) vào {path}");
_hasChanges = false;
}
}
class GameEngine
{
static void Main()
{
var player = new Player("P1", new Vector3(0, 0, 0), 100);
player.Update();
player.TakeDamage(20);
ISaveable saver = player;
if (saver.HasChanges) saver.SaveState("save.json");
GameObject obj = player;
obj.Update();
player.Destroy();
}
}
Trong ví dụ này Player là GameObject (vì kế thừa, dùng chung field Id, Position và method Destroy). Đồng thời Player có thể lưu trạng thái (vì implement interface ISaveable). Cực kỳ linh hoạt và mạnh mẽ!
7. Những lưu ý và lỗi thường gặp
Cố tạo instance abstract class:
Animal myAnimal = new Animal(); – đây là lỗi newbie hay gặp. Nhớ nhé: abstract class chỉ là mẫu, không phải object sẵn dùng. Compiler sẽ báo lỗi ngay.
Quên hiện thực method abstract: Nếu bạn kế thừa abstract class mà class của bạn không phải abstract, bạn bắt buộc phải override (override) tất cả method abstract của class cha. Không thì compiler cũng báo lỗi.
Cố thêm field vào interface: Đây là một trong những "không được" rõ ràng nhất của interface. Từ C# 8+ có static field, nhưng instance field (tức là field thuộc object cụ thể) vẫn bị cấm. Interface là contract hành vi, không phải kho dữ liệu.
Nhầm lẫn virtual và abstract:
- Method/property virtual có code mặc định và có thể override ở class con.
- Method/property abstract không có code và bắt buộc override ở class con đầu tiên không abstract.
- Dùng virtual ở chỗ logic luôn khác nhau sẽ phải viết code rỗng rồi override, không rõ ràng bằng abstract.
Hiểu rõ những khác biệt này và chọn đúng giữa abstract class và interface là kỹ năng then chốt cho mỗi lập trình viên C#. Đây không chỉ là chuyện cú pháp, mà là tư duy kiến trúc. Khi thiết kế hệ thống, hãy tự hỏi: "Các class này có chung hệ thống phân cấp 'là một' không?" và "Các class này có năng lực chung cần cho nhiều kiểu khác nhau không?". Câu trả lời sẽ giúp bạn chọn đúng.
Ở các bài giảng tiếp theo, tụi mình sẽ tiếp tục đào sâu vào thế giới interface, khám phá các tính năng nâng cao mới xuất hiện trong các phiên bản C# gần đây. Nhớ theo dõi nhé!
GO TO FULL VERSION