1. Giới thiệu
Hãy tưởng tượng bạn đang thiết kế một chiếc ô tô hiện đại. Bên trong là hàng trăm chi tiết phức tạp, điện tử nhạy cảm, các tinh chỉnh tỉ mỉ. Rõ ràng là không ai nên tự tiện tiếp cận bộ điều khiển động cơ và vặn lung tung vài con ốc. Nếu ai cũng chọc vào những chỗ không nên, xe có thể hoạt động bất thường, và bạn sẽ phải kiểm tra xem ai đã phá hỏng cái gì.
Lập trình cũng vậy thôi. Khi bạn tạo một class, bạn muốn bảo vệ phần bên trong khỏi bàn tay người ngoài — để không ai vô tình làm hỏng dữ liệu quan trọng hoặc can thiệp vào các method mà thế giới bên ngoài không nên biết. Đó chính là ý nghĩa của đóng gói (encapsulation) — một trong ba nguyên lý cốt lõi của lập trình hướng đối tượng.
Đóng gói cho phép bạn giấu chi tiết cài đặt và xác định rõ cái gì "bên ngoài" có thể truy cập, còn cái gì phải giữ kín bên trong. Giống như bạn chỉ cho người dùng mở cốp xe, còn nắp capo thì khóa kỹ. Class tự quyết định dữ liệu nào sẽ show ra ngoài, dữ liệu nào giữ bí mật. Nhờ vậy, code trở nên đáng tin cậy, logic và dễ bảo trì hơn nhiều.
Nói nghiêm túc hơn, đóng gói là cách gom dữ liệu (field) và hành vi (method) liên quan thành một cấu trúc duy nhất, đồng thời giới hạn truy cập trực tiếp vào các thành phần bên trong object.
2. Modifier truy cập: bảo vệ lãnh thổ của mình
Trong C# (và các ngôn ngữ OOP khác), đóng gói được thực hiện nhờ modifier truy cập. Đây là những "nhãn" xác định thành viên nào của class (field, method, property, v.v.) có thể truy cập từ bên ngoài, còn cái nào chỉ dùng bên trong.
Chúng ta đã gặp chúng rồi, nhưng cùng nhắc lại và mở rộng thêm danh sách modifier:
| Modifier | Truy cập được... |
|---|---|
|
Mọi nơi! Bất kỳ code nào trong project (và cả ngoài project nếu là thư viện) |
|
Chỉ bên trong chính class đó |
|
Từ class này và tất cả class con kế thừa |
|
Chỉ trong phạm vi assembly (project) hiện tại |
|
Hoặc từ assembly hiện tại, hoặc từ class con kế thừa |
|
Chỉ từ class con bên trong assembly hiện tại |
Trong bài này, mình sẽ tập trung vào những cái hay dùng nhất: public, private và nhắc nhẹ về protected thôi cho đỡ nặng đầu.
Phân biệt bên trong và bên ngoài
Thử viết một class Dog nhé:
public class Dog
{
public string Name;
public int Age;
public void Bark()
{
Console.WriteLine($"{Name} nói: Gâu!");
}
}
Ở đây tất cả field và method đều khai báo với modifier public. Nghĩa là ai cũng có thể đổi tên hoặc tuổi của chó:
Dog rex = new Dog("Rex", 5);
rex.Name = "Sharik"; // Đột ngột đổi tên!
rex.Age = -999; // Tuổi âm siêu dị
Nhưng field Name và Age là dữ liệu cực kỳ quan trọng, đâu thể để ai muốn làm gì thì làm.
Đừng làm như thế này nhé
Nếu để tất cả field public, bạn sẽ phá vỡ logic của class: bất kỳ ai cũng có thể làm object trở nên không hợp lệ. Ví dụ, đặt tuổi chó là số âm hoặc đặt tên kiểu "%$#!??".
3. Giấu field: dùng private
Thông thường, field của class nên để private (modifier private). Nghĩa là chỉ có thể thay đổi giá trị từ bên trong class, bên ngoài thì bó tay.
public class Dog
{
private string name;
private int age;
public void Bark()
{
Console.WriteLine($"{name} nói: Gâu!");
}
}
Giờ thử truy cập field từ bên ngoài:
Dog rex = new Dog("Rex", 5);
rex.name = "Sharik"; // Lỗi biên dịch
Compiler sẽ báo ngay: "Không có quyền truy cập!".
Tại sao phải cứng nhắc vậy? Thế linh hoạt đâu?
Tất cả sự "linh hoạt" sẽ được đảm bảo nhờ các method đặc biệt hoặc property (sẽ nói kỹ ở bài sau), cho phép kiểm soát truy cập và thay đổi dữ liệu theo quy tắc nhất định.
4. Đóng gói thực tế: ví dụ kiểm soát dữ liệu
Giả sử bạn muốn chó không thể có tuổi âm:
public class Dog
{
private string name;
private int age;
public Dog(string name, int age)
{
this.name = name;
if (age >= 0)
this.age = age;
else
this.age = 0; // Không cho đặt tuổi "kỳ lạ"
}
public void Bark()
{
Console.WriteLine($"{name} nói: Gâu!");
}
// Method để thay đổi tuổi an toàn
public void SetAge(int newAge)
{
if (newAge >= 0)
age = newAge;
// Có thể thêm else: báo giá trị không hợp lệ
}
}
Giờ thì không ai bên ngoài có thể phá field trực tiếp nữa. Muốn đổi tuổi phải qua method kiểm tra hợp lệ.
5. Field vs method truy cập (getter/setter)
Cách này — để field private và cung cấp method để thao tác — gọi là đóng gói dữ liệu (data encapsulation). Để đọc giá trị field thường có method getter, để ghi thì có setter.
public class Dog
{
private string name;
public Dog(string name)
{
this.name = name;
}
public string GetName()
{
return name;
}
public void SetName(string newName)
{
// Có thể kiểm tra tên hợp lệ ở đây
name = newName;
}
}
Nhưng! Khi C# có property (properties), kiểu method này ít dùng hơn — property giúp code gọn và tiện hơn nhiều (chi tiết ở bài sau nhé).
6. Modifier truy cập cho method và class
Field chưa phải tất cả đâu! Modifier truy cập còn dùng cho method (function thành viên class) và thậm chí cả class luôn.
Method chỉ dùng bên trong class (ví dụ, function phụ cho logic nội bộ) thì nên để private.
Method public — chính là interface của class (không nhầm với từ khóa interface nhé), tức là cái mà người dùng class sẽ dùng.
7. Lỗi thường gặp khi làm việc với đóng gói
Lỗi số 1: cái gì cũng khai báo public.
Nghĩ rằng càng nhiều thứ truy cập được thì càng dễ dùng. Thực tế là bạn mở toang phần bên trong class, những thứ không nên bị thay đổi từ ngoài. Code kiểu này rất dễ bị lỗi và khó kiểm soát, nhất là trong project lớn.
Lỗi số 2: đổi modifier truy cập làm hỏng code bên ngoài.
Khi refactor, bạn có thể vô tình đổi modifier của method hoặc field, và toàn bộ code khác dùng nó sẽ không biên dịch được hoặc chạy sai. Đặc biệt nguy hiểm với public API.
Lỗi số 3: nhầm lẫn giữa biến local và field của class.
Đôi khi dev quên rằng biến khai báo trong method chỉ sống trong method đó. Field của class thì dùng được ở mọi method. Điều này dễ gây lỗi khó hiểu, nhất là khi tên biến trùng nhau.
Lỗi số 4: bỏ qua private và protected.
Nhiều bạn sợ dùng truy cập hạn chế, lo sau này không truy cập được. Nhưng đóng gói là để giấu những thứ không cần thiết, chỉ show ra ngoài cái thực sự cần thôi.
GO TO FULL VERSION