1. Giới thiệu
Bạn dùng máy tính hay điện thoại mỗi ngày mà chẳng cần suy nghĩ nhiều. Mở trình duyệt, vào web, chụp ảnh. Bạn đâu cần biết bộ xử lý hoạt động ra sao, RAM làm việc thế nào hay tín hiệu chạy qua chip kiểu gì. Đó là tầng người dùng – tiện lợi, trực quan, che hết mọi thứ rắc rối khỏi bạn.
Nhưng nếu có gì đó trục trặc thì sao? Ví dụ, app không khởi động được nữa. Để cài lại nó, bạn cần biết thêm chút kiến thức – ít nhất là tải ở đâu và cài thế nào. Đó là một tầng trừu tượng khác – tầng hệ thống, kỹ thuật. Còn nếu hỏng phần cứng – ví dụ ổ cứng hay mainboard chết – thì bạn phải hiểu cấu trúc vật lý của thiết bị rồi.
Có rất nhiều tầng trừu tượng, mỗi tầng sẽ che đi sự phức tạp phía sau, chỉ đưa cho bạn đúng cái bạn cần để giải quyết vấn đề.
Trong lập trình cũng vậy thôi. Hãy tưởng tượng bạn đi taxi và nói: “Đến đường Lập Trình Viên, số 42”. Bạn đâu quan tâm tài xế đi đường nào, sang số ra sao hay xăng gì trong bình. Bạn chỉ cần đến nơi. Mọi thứ còn lại – bị ẩn đi. Đó không phải lười đâu, mà là trừu tượng đúng nghĩa: bạn tương tác với hệ thống qua giao diện dễ hiểu, không cần biết chi tiết bên trong.
Ví dụ khác – camera trên điện thoại. Bạn bấm vào icon, chụp ảnh, và ảnh xuất hiện trong gallery. Bạn không cần biết ánh sáng đi qua ống kính thế nào, cảm biến và chip hoạt động ra sao, dữ liệu lưu vào bộ nhớ kiểu gì. Tất cả đều được che dưới một lớp giao diện tiện lợi. Đó chính là trừu tượng – bạn dùng công cụ mạnh mà không cần hiểu cấu tạo bên trong.
Trong thế giới lập trình, nhất là với C# và .NET, trừu tượng không chỉ là tiện lợi mà là điều bắt buộc để sống sót. Không có nó thì khó mà xây dựng được dự án lớn, dễ hiểu, dễ bảo trì. Nó giúp lập trình viên nói chuyện cùng một ngôn ngữ, không bị chìm trong chi tiết của từng module.
2. Tại sao cần trừu tượng trong lập trình?
"Ờ, nghe cũng hay đấy, nhưng mình – một coder tương lai – cần nó để làm gì?" Câu hỏi hay! Trừu tượng không phải để cho vui, mà để mang lại những lợi ích rất thực tế:
- Đơn giản hóa hệ thống phức tạp: Não mình không thể nhớ hết mọi chi tiết của một chương trình khổng lồ. Trừu tượng giúp chia nhỏ vấn đề lớn thành các phần nhỏ dễ kiểm soát. Mỗi phần “giấu” sự phức tạp bên trong, chỉ đưa ra cái thực sự quan trọng. Giống như Lego: mỗi miếng đều đơn giản, nhưng ráp lại thì thành bất cứ thứ gì, khỏi cần biết từng viên gạch làm sao.
- Tăng khả năng đọc và bảo trì code: Code dựa trên trừu tượng sẽ dễ đọc, dễ hiểu hơn nhiều. Khi bạn thấy gọi hàm device.TurnOn(), bạn hiểu ngay ý nghĩa, khỏi cần lội vào hàng trăm dòng code mô tả cách bật đèn hay quạt. Nhờ vậy, code dễ sửa lỗi và thêm tính năng mới hơn.
- Giảm sự phụ thuộc giữa các module: Hãy tưởng tượng code của bạn bật đèn pin mà lại gọi trực tiếp vào hàng loạt thao tác thấp cấp, chỉ dành cho một loại đèn pin cụ thể. Nếu muốn thay đèn pin khác thì sao? Phải sửa lại hết code! Trừu tượng cho phép bạn làm việc với “bất kỳ đèn pin nào” qua một giao diện chung. Nếu thay đổi “ruột” đèn pin, code bật đèn cũng chẳng cần biết. Vì nó chỉ làm việc với đại diện trừu tượng thôi.
- Linh hoạt và dễ thay đổi: Nhờ trừu tượng, bạn có thể thay đổi cài đặt bên trong của một thành phần mà không ảnh hưởng đến phần còn lại của chương trình. Điều này cực kỳ quý giá trong dự án lớn, nơi nhiều team cùng làm các phần khác nhau mà không đụng chạm nhau.
- Phân chia trách nhiệm: Mỗi thành phần trong chương trình (class, method) sẽ có trách nhiệm rõ ràng. Class LightBulb lo phần việc của nó, còn class SmartHomeManager thì quản lý thiết bị, chẳng cần biết chi tiết nhỏ nhặt bên trong.
Trừu tượng không phải là thứ bạn “thêm vào” chương trình như một nguyên liệu. Nó là cách tư duy khi thiết kế code. Đó là khả năng nhìn ra điểm chung và che đi sự khác biệt.
3. Trừu tượng xuất hiện trong C# (và OOP nói chung) như thế nào?
Bạn sẽ ngạc nhiên, nhưng thật ra mình đã dùng trừu tượng từ lâu mà chưa gọi tên thôi! Nó xuất hiện ở nhiều tầng trong chương trình C# của mình:
Class và Object
Bản thân khái niệm class đã là trừu tượng rồi. Class LightBulb trừu tượng hóa ý tưởng “bóng đèn”, có thể bật, tắt, chỉnh độ sáng. Khi tạo object LightBulb myLamp = new LightBulb();, bạn làm việc với trừu tượng này, chứ không phải với từng electron, nguyên tử cụ thể.
Ví dụ: Nhìn vào class LightBulb. Nó có method TurnOn(). Bạn gọi myLamp.TurnOn() là đèn sáng. Nhưng bạn đâu phải viết code điều khiển điện, mở van siêu nhỏ hay khởi động phản ứng nhiệt hạch trong dây tóc (đùa thôi!). Tất cả chi tiết đó đều bị ẩn trong method TurnOn().
Access modifier: Dùng private cho field và method là biểu hiện rõ của encapsulation, mà encapsulation lại là một trong những cách đạt được trừu tượng. Mình làm cho một số dữ liệu hoặc thao tác không thể truy cập từ ngoài, tức là “trừu tượng hóa” người dùng class khỏi sự phức tạp bên trong. Ví dụ, trong app ngân hàng, method _updateBalance() (private, có dấu gạch dưới để nhấn mạnh là chi tiết nội bộ) lo logic cập nhật số dư phức tạp, còn bên ngoài chỉ có Deposit() hoặc Withdraw(). Đó chính là trừu tượng.
Method và Function
Mỗi lần gọi method là bạn đang dùng trừu tượng. Bạn tin tưởng method sẽ làm việc gì đó, mà không cần biết nó làm thế nào.
Ví dụ, nhớ hàm Console.WriteLine("Xin chào, thế giới!"); chứ? Bạn chỉ gọi hàm này và mong text hiện lên màn hình. Bạn đâu cần biết chi tiết: OS cấp phát bộ nhớ ra sao, font chuyển thành pixel thế nào, card đồ họa vẽ lên màn hình kiểu gì?
Nếu phải nghĩ hết mấy thứ đó thì mỗi chương trình nhỏ cũng mất cả tiếng đồng hồ rồi.
Console.WriteLine là một trừu tượng mạnh mẽ. Nó che đi cả đống việc phức tạp phía sau.
Kế thừa và Đa hình
Đây là nơi trừu tượng thể hiện rõ nhất! Khi tạo class cha Animal và các class con Dog, Cat, bạn trừu tượng hóa khái niệm “động vật” biết “phát ra âm thanh”.
Ví dụ, bạn viết Animal myPet = new Dog(); rồi myPet.MakeSound();. Bạn làm việc với trừu tượng Animal. Bạn “trừu tượng hóa” khỏi việc myPet thực ra là Dog. Đa hình cho phép gọi MakeSound() sẽ khác nhau tùy loại (chó sủa, mèo kêu). Bạn lập trình “làm gì” (phát âm thanh), còn “làm thế nào” thì để class con lo. Đó là trừu tượng đỉnh cao!
Interface (mới nhắc sơ, chi tiết sau)
Mình chưa học tới, nhưng nhớ nhé: interface trong C# là cách thể hiện trừu tượng “cái gì mà không cần biết làm sao”. Interface là hợp đồng mô tả tập method, property hoặc event, nhưng không cài đặt gì cả. Nó nói: “Ai implement interface này bắt buộc phải làm được cái này, cái kia”. Sẽ học kỹ ở Bài giảng 111, nhưng nhớ: đây là đỉnh cao trừu tượng trong C#.
Abstract class (cũng chỉ nhắc sơ, chi tiết ở bài sau)
Abstract class là thứ ở giữa class thường và interface. Nó có thể chứa cả method đã cài đặt lẫn abstract method (như đã thấy sơ ở Bài giảng 105), tức là method không có thân hàm và bắt buộc phải được cài đặt ở class con. Abstract class dùng để tạo bộ khung chung, tập chức năng chung, nhưng để lại “lỗ hổng” (abstract method) cho class con tự cài đặt. Sẽ nói kỹ ở bài sau nhé!
Tóm lại: trừu tượng không phải là một cú pháp riêng của C#. Nó là một concept mạnh mẽ, xuyên suốt mọi tầng code, từ method đơn giản đến hệ thống class phức tạp.
4. Ví dụ code: hệ thống quản lý nhà thông minh
Quay lại app của mình và xem trừu tượng giúp nó linh hoạt thế nào nhé. Giả sử mình xây hệ thống “Nhà thông minh”. Ban đầu chỉ có đèn và quạt:
public class LightBulb
{
public string Name;
public LightBulb(string name) => Name = name;
public void TurnOn() => Console.WriteLine($"{Name}: đèn bật");
public void ChangeBrightness(int level) => Console.WriteLine($"{Name}: độ sáng {level}%");
}
public class Fan
{
public string Name;
public Fan(string name) => Name = name;
public void TurnOn() => Console.WriteLine($"{Name}: quạt bật");
public void AdjustSpeed(int speed) => Console.WriteLine($"{Name}: tốc độ {speed}");
}
class Program
{
static void Main()
{
var lamp = new LightBulb("Bếp");
var fan = new Fan("Phòng ngủ");
lamp.TurnOn();
lamp.ChangeBrightness(75);
fan.TurnOn();
fan.AdjustSpeed(3);
}
}
Code này chạy ngon lành khi làm việc với từng thiết bị riêng lẻ. Nhưng nếu muốn nhà thông minh thật sự thông minh và điều khiển tất cả thiết bị cùng lúc thì sao? Ví dụ, bật hết mọi thứ trước khi về nhà?
Nếu bạn thử tạo object[] để lưu các thiết bị, sẽ gặp vấn đề: kiểu object không biết method TurnOn(). Muốn gọi thì phải kiểm tra từng loại và ép kiểu, rất lằng nhằng:
// không có trừu tượng và đa hình:
foreach (object device in allDevices)
{
if (device is LightBulb bulb)
{
bulb.TurnOn();
}
else if (device is Fan fan)
{
fan.TurnOn();
}
// Cứ thế cho mỗi loại thiết bị mới... mệt luôn!
}
Lúc này, inheritance và polymorphism – cùng với encapsulation – là công cụ để đạt trừu tượng. Hãy tạo class cha SmartDevice để trừu tượng hóa khái niệm “thiết bị thông minh”, rồi cho đèn và quạt kế thừa nó.
class SmartDevice
{
public string Name;
public SmartDevice(string name) => Name = name;
public virtual void TurnOn() => Console.WriteLine($"{Name}: thiết bị bật");
public virtual void TurnOff() => Console.WriteLine($"{Name}: thiết bị tắt");
}
class LightBulb : SmartDevice
{
public LightBulb(string name) : base(name) { }
public override void TurnOn() => Console.WriteLine($"{Name}: đèn bật");
public override void TurnOff() => Console.WriteLine($"{Name}: đèn tắt");
public void ChangeBrightness(int x) => Console.WriteLine($"{Name}: độ sáng {x}%");
}
class Fan : SmartDevice
{
public Fan(string name) : base(name) { }
public override void TurnOn() => Console.WriteLine($"{Name}: quạt bật");
public override void TurnOff() => Console.WriteLine($"{Name}: quạt tắt");
public void AdjustSpeed(int s) => Console.WriteLine($"{Name}: tốc độ {s}");
}
class Program
{
static void Main()
{
SmartDevice[] devices =
{
new LightBulb("Bếp"),
new Fan("Phòng ngủ"),
new SmartDevice("Cảm biến")
};
foreach (var d in devices) d.TurnOn();
foreach (var d in devices) d.TurnOff();
// Demo gọi method đặc trưng
foreach (var d in devices)
{
if (d is LightBulb b)
b.ChangeBrightness(50);
if (d is Fan f)
f.AdjustSpeed(2);
}
}
}
Thấy code gọn và linh hoạt hơn hẳn chưa! Giờ bạn có thể thêm bất kỳ loại thiết bị mới nào (ví dụ SmartTV, SmartThermostat) kế thừa SmartDevice, và vòng lặp foreach (SmartDevice device in smartHomeDevices) vẫn chạy ngon lành. Đó là sức mạnh của trừu tượng. Bạn không quan tâm loại thiết bị cụ thể, chỉ tập trung vào khả năng “bật” và “tắt” chung của nó.
Ví dụ này cho thấy inheritance và polymorphism – những gì mình đã học – chính là công cụ để đạt trừu tượng. Mình tạo ra một đại diện chung (SmartDevice), cho phép làm việc với nhiều thiết bị cụ thể (LightBulb, Fan) theo cách giống nhau.
Nhưng có một điểm cần chú ý: trong SmartDevice hiện tại, method TurnOn() và TurnOff() có “cài đặt chung”, chỉ in ra “Thiết bị bật/tắt (cài đặt chung)”. Nếu không có cài đặt chung hợp lý cho mọi thiết bị thì sao? Ví dụ, “thiết bị chung” (SmartDevice trực tiếp) chỉ là cảm biến nhiệt độ, không có nút “BẬT/TẮT”. Hoặc bạn muốn bắt buộc mọi class con phải tự cài đặt các method này? Đó là lúc abstract class và abstract method xuất hiện – sẽ nói kỹ ở bài sau. Chúng là cách mạnh hơn nữa để áp dụng trừu tượng, đảm bảo một số method bắt buộc phải được cài đặt ở class con.
Vậy là mình vừa dạo quanh thế giới trừu tượng – nguyên lý nền tảng của OOP. Ở bài sau, mình sẽ đi sâu vào cách C# cung cấp công cụ đặc biệt – abstract class và abstract method – để thực thi concept này trong code. Chuẩn bị nhé, sẽ còn thú vị hơn!
GO TO FULL VERSION