### MỨC ĐỘ 1: NỀN TẢNG CƠ BẢN
*Đây là những thứ đầu tiên cần học để hiểu cách một chương trình hoạt động.*
**1. Cấu trúc chương trình C#**
Hiểu cách một chương trình khởi chạy.
```csharp
using System;
class Program {
static void Main() { Console.WriteLine("Hello!"); }
}
```
**2. Biến và Kiểu dữ liệu (Value Type & Nullable)**
Cách lưu trữ thông tin cơ bản.
```csharp
int age = 25; // Kiểu số nguyên
double price = 10.5; // Kiểu số thực
bool isReady = true; // Kiểu đúng/sai
int? optionalAge = null; // Nullable: có thể trống
```
**3. Toán tử**
Thực hiện các phép tính.
```csharp
int sum = 5 + 5; // Toán học
bool check = (10 > 5); // So sánh
bool logic = (true && false); // Logic
```
**4. Câu lệnh điều kiện (if-else, switch)**
Rẽ nhánh chương trình.
```csharp
if (age >= 18) Console.WriteLine("Người lớn");
else Console.WriteLine("Trẻ em");
switch (day) { case 1: Console.WriteLine("Thứ 2"); break; }
```
**5. Vòng lặp (for, while)**
Lặp lại một hành động.
```csharp
for (int i = 0; i < 3; i++) Console.WriteLine(i);
```
**6. Enum**
Để hiểu về **Enum (Kiểu liệt kê)**, hãy tưởng tượng bạn đang xây dựng một hệ thống quản lý đơn hàng. Một đơn hàng chỉ có thể ở một vài trạng thái nhất định: *Đang chờ, Đang giao, Đã giao, hoặc Đã hủy*.
Nếu không có Enum, bạn sẽ phải dùng số:
* 0 = Đang chờ
* 1 = Đang giao
* ...
Nhìn vào code `if (status == 0)`, bạn sẽ rất nhanh quên số 0 nghĩa là gì. **Enum ra đời để thay thế những con số "vô hồn" đó bằng những cái tên có ý nghĩa.**
---
### 1. Cách định nghĩa và sử dụng
Thay vì nhớ các con số, ta đặt tên cho chúng:
```csharp
// 1. Định nghĩa tập hợp các hằng số
enum OrderStatus
{
Pending, // Mặc định là 0
Processing, // Mặc định là 1
Shipped, // Mặc định là 2
Delivered, // Mặc định là 3
Cancelled // Mặc định là 4
}
class Program
{
static void Main()
{
// 2. Sử dụng Enum
OrderStatus myOrder = OrderStatus.Shipped;
// 3. Kiểm tra trạng thái bằng tên thay vì bằng số
if (myOrder == OrderStatus.Shipped)
{
Console.WriteLine("Đơn hàng đang trên đường đến với bạn!");
}
}
}
```
---
### 2. Tại sao Enum lại cực kỳ hữu ích?
#### A. Code cực kỳ dễ đọc (Readable)
Hãy so sánh hai đoạn code sau:
* **Cách dùng số (Xấu):** `if (state == 3)` -> 3 là gì? Phải lật tài liệu ra xem.
* **Cách dùng Enum (Tốt):** `if (state == OrderStatus.Delivered)` -> Nhìn vào hiểu ngay là "Đã giao hàng".
#### B. Tránh sai sót (Type Safety)
Nếu bạn dùng kiểu số (`int`), bạn có thể vô tình gán `status = 99;` (trong khi chỉ có từ 0-4).
Với Enum, C# sẽ ngăn chặn bạn gán những giá trị không nằm trong danh sách đã định nghĩa.
#### C. Bản chất là con số
Thực tế, máy tính vẫn lưu Enum dưới dạng số nguyên để tiết kiệm bộ nhớ. Bạn có thể ép kiểu (cast) qua lại:
```csharp
int statusValue = (int)OrderStatus.Shipped;
Console.WriteLine(statusValue); // Kết quả in ra: 2
```
---
### 3. Ví dụ thực tế khác: Các ngày trong tuần
```csharp
enum DaysOfWeek
{
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
void CheckWorkDay(DaysOfWeek day)
{
if (day == DaysOfWeek.Saturday || day == DaysOfWeek.Sunday)
{
Console.WriteLine("Nghỉ ngơi thôi!");
}
else
{
Console.WriteLine("Đi làm nào!");
}
}
```
**Tóm lại:** Hãy dùng **Enum** bất cứ khi nào bạn có một danh sách các lựa chọn **cố định và có giới hạn**. Nó giúp code của bạn sạch sẽ, chuyên nghiệp và cực kỳ dễ bảo trì.
---
### MỨC ĐỘ 2: LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG - OOP (INTERMEDIATE)
*Bắt đầu tư duy theo hướng quản lý mã nguồn chuyên nghiệp.*
**7. Lớp (Class) & Đối tượng (Object)**
Khuôn mẫu và thực thể.
```csharp
class Dog { public string Name; }
Dog myDog = new Dog() { Name = "Ki" };
```
**8. Phương thức (Method) & Constructor**
Hành động của đối tượng và cách khởi tạo.
```csharp
class Person {
public Person() {
Console.WriteLine("Khởi tạo");
} // Constructor
public void SayHi() {
Console.WriteLine("Hi!");
} // Method
}
```
**9. Access Modifiers & Properties (get/set)**
Kiểm soát quyền truy cập dữ liệu.
Để hiểu về **Access Modifiers** (Phạm vi truy cập) và **Properties** (Thuộc tính), hãy tưởng tượng bạn có một chiếc **Máy rút tiền (ATM)**.
* **Số dư tài khoản** bên trong máy phải được bảo mật (không ai được tự ý sửa con số này).
* Bạn chỉ có thể **Xem** số dư hoặc **Nạp/Rút** tiền thông qua các nút bấm có sẵn.
Trong lập trình C#, chúng ta sử dụng Access Modifiers và Properties để thực hiện cơ chế này.
---
### 1. Access Modifiers (Phạm vi truy cập)
Đây là các từ khóa quy định "ai" được phép nhìn thấy và sử dụng các biến/hàm:
* **public**: Công khai. Ai cũng có thể truy cập từ bên ngoài lớp.
* **private**: Riêng tư. Chỉ nội bộ bên trong lớp đó mới được dùng. (Đây là mặc định nếu bạn không ghi gì).
* **protected**: Chỉ lớp đó và các lớp con (kế thừa) mới được dùng.
---
### 2. Properties (get / set)
Thay vì để người ngoài "chạm trực tiếp" vào biến (field), ta dùng Property như một cái cổng bảo vệ:
* **get**: Cho phép đọc giá trị.
* **set**: Cho phép ghi/sửa giá trị (có thể kèm theo điều kiện kiểm tra).
---
### 3. Ví dụ cụ thể: Quản lý số dư tài khoản
Nếu bạn để biến là `public`, người dùng có thể nhập số âm, làm hỏng logic của chương trình:
`acc.Balance = -1000; // Sai logic nhưng code vẫn chạy nếu dùng public field`
**Giải pháp dùng Property:**
```csharp
class BankAccount
{
// 1. Field (Biến thực): Để là private để giấu kín thông tin bên trong
private double _balance;
// 2. Property (Cổng bảo vệ): Để là public để bên ngoài tương tác
public double Balance
{
get
{
// Cho phép xem số dư
return _balance;
}
set
{
// Trước khi cập nhật, kiểm tra xem tiền nạp vào có hợp lệ không
if (value >= 0)
{
_balance = value;
}
else
{
Console.WriteLine("Lỗi: Số tiền không được âm!");
}
}
}
}
// Cách sử dụng:
class Program
{
static void Main()
{
BankAccount myAcc = new BankAccount();
// Gán giá trị qua 'set'
myAcc.Balance = 500; // Hợp lệ, _balance sẽ là 500
myAcc.Balance = -100; // Không hợp lệ, sẽ báo lỗi ra màn hình
// Đọc giá trị qua 'get'
Console.WriteLine("Số dư hiện tại: " + myAcc.Balance);
}
}
```
---
### 4. Tại sao phải làm phức tạp như vậy? (Lợi ích)
1. **Bảo mật dữ liệu (Encapsulation):** Bạn không cho phép người khác sửa trực tiếp dữ liệu gốc.
2. **Kiểm soát logic (Validation):** Như ví dụ trên, bạn ngăn chặn được việc nhập số âm vào tài khoản.
3. **Read-only (Chỉ đọc):** Nếu bạn muốn người dùng chỉ được xem mà không được sửa, bạn chỉ cần bỏ phần `set` đi.
```csharp
public string AccountNumber { get; } = "123-456-789"; // Chỉ cho xem, không cho sửa
```
4. **Auto-Implemented Properties:** Nếu bạn không cần kiểm tra điều kiện phức tạp, C# cho phép viết cực ngắn gọn:
```csharp
public string Name { get; set; } // C# tự tạo biến private ngầm bên dưới
```
**Tóm lại:**
- **Access Modifiers** là "ổ khóa".
- **Properties** là "người gác cổng" thông minh, giúp bạn kiểm soát dữ liệu ra vào một cách an toàn.
**10. Tính Đóng gói (Encapsulation)**
Giấu dữ liệu nhạy cảm. (Sử dụng `private` và `Property`).
**11. Tính Kế thừa (Inheritance)**
Sử dụng lại mã nguồn.
```csharp
class Animal { public void Eat() {} }
class Cat : Animal { } // Cat kế thừa từ Animal
```
**12. Tính Đa hình (Polymorphism)**
Một hành động nhưng thực hiện theo nhiều cách khác nhau.
Để hiểu về **Tính Đa hình (Polymorphism)**, hãy nhớ cụm từ này: **"Một lời lệnh, nhiều cách phản hồi"**.
Hãy tưởng tượng bạn là một "Đạo diễn". Bạn đứng trước một nhóm diễn viên gồm: một Ca sĩ, một Vũ công và một Nghệ sĩ xiếc. Bạn hô to: **"Hãy biểu diễn đi!"**
* Người ca sĩ sẽ **hát**.
* Người vũ công sẽ **nhảy**.
* Nghệ sĩ xiếc sẽ **tung hứng**.
Cùng một mệnh lệnh "Biểu diễn", nhưng mỗi người thực hiện theo cách riêng của họ. Đó chính là Đa hình.
---
### Ví dụ 1: Tiếng kêu của động vật (Dễ hiểu nhất)
Chúng ta có lớp cha `Animal` và các lớp con như `Dog`, `Cat`.
```csharp
// 1. Lớp cha (Định nghĩa hành động chung)
class Animal
{
// Từ khóa 'virtual' cho phép các lớp con được quyền thay đổi nội dung hàm này
public virtual void Sound()
{
Console.WriteLine("Động vật phát ra tiếng kêu chung chung...");
}
}
// 2. Các lớp con (Thực hiện hành động theo cách riêng)
class Dog : Animal
{
// Từ khóa 'override' dùng để ghi đè/thay đổi nội dung từ lớp cha
public override void Sound()
{
Console.WriteLine("Gâu gâu!");
}
}
class Cat : Animal
{
public override void Sound()
{
Console.WriteLine("Meo meo!");
}
}
```
### Tại sao Đa hình lại "mạnh mẽ"? (Phần quan trọng)
Sức mạnh thực sự của Đa hình nằm ở chỗ bạn có thể điều khiển một danh sách các đối tượng khác nhau thông qua lớp cha mà **không cần biết chính xác** chúng là con gì.
```csharp
class Program
{
static void Main()
{
// Tạo một danh sách các "Động vật" (có cả Chó và Mèo trong đó)
List<Animal> myAnimals = new List<Animal>();
myAnimals.Add(new Dog());
myAnimals.Add(new Cat());
myAnimals.Add(new Animal());
// CHỖ NÀY LÀ ĐA HÌNH:
foreach (var item in myAnimals)
{
// Chúng ta chỉ gọi hàm Sound().
// C# sẽ tự biết item đó là Dog để kêu "Gâu", hay Cat để kêu "Meo".
item.Sound();
}
}
}
```
---
### Ví dụ 2: Ứng dụng thực tế - Thanh toán điện tử
Giả sử bạn làm trang web bán hàng, có nhiều phương thức thanh toán.
```csharp
class PaymentMethod
{
public virtual void ProcessPayment() { Console.WriteLine("Đang xử lý thanh toán..."); }
}
class CreditCard : PaymentMethod
{
public override void ProcessPayment() { Console.WriteLine("Đang quét thẻ tín dụng và trừ tiền..."); }
}
class Momo : PaymentMethod
{
public override void ProcessPayment() { Console.WriteLine("Đang mở app Momo để quét mã QR..."); }
}
// Khi khách hàng nhấn nút "Thanh toán", bạn chỉ cần gọi:
// payment.ProcessPayment();
// Code sẽ tự chạy đúng logic của loại thẻ khách chọn mà không cần dùng nhiều câu lệnh if-else.
```
---
### Tổng kết các từ khóa:
1. **`virtual` (Tại lớp cha):** "Tôi có hàm này, các con tôi **có thể** thay đổi nó nếu muốn."
2. **`override` (Tại lớp con):** "Tôi đang thay đổi lại hành động của cha tôi cho phù hợp với tôi."
3. **Lợi ích:** Giúp code linh hoạt, dễ mở rộng. Khi bạn muốn thêm một con `Pig` kêu "Ụt ịt", bạn chỉ cần tạo lớp mới và `override` hàm `Sound`, không cần sửa lại code ở lớp cha hay ở hàm `Main`.
**13. Interface**
Bản thiết kế (hợp đồng) cho các lớp.
```csharp
interface ILogger { void Log(); }
```
**14. Static**
Thành phần thuộc về lớp, không cần tạo đối tượng.
```csharp
public static int Count = 0;
```
---
### MỨC ĐỘ 3: TỐI ƯU HÓA VÀ XỬ LÝ DỮ LIỆU
*Làm việc với cấu trúc dữ liệu và các tính năng đặc thù của C#.*
**15. Kiểu tham chiếu (Reference Type)**
Hiểu về vùng nhớ (string, class, array).
**16. Collection (List, Dictionary, ArrayList)**
Lưu trữ danh sách dữ liệu linh hoạt.
```csharp
List<string> names = new List<string> { "An", "Bình" };
```
**17. Generic**
Để hiểu về **Generic**, hãy tưởng tượng bạn có một chiếc **Máy đóng gói**.
* Nếu không có Generic: Bạn phải xây một cái máy riêng cho đóng gói **Táo**, một cái máy riêng cho đóng gói **Cam**, và một cái máy riêng cho đóng gói **Sữa**. Dù quy trình "cho vào hộp -> dán băng keo" là y hệt nhau.
* Với Generic: Bạn chỉ cần xây **một chiếc máy duy nhất** có một cái phễu "vạn năng". Bạn đổ cái gì vào (Táo, Cam, hay Sữa), nó cũng sẽ đóng gói đúng loại đó cho bạn.
Trong lập trình, **Generic** cho phép bạn định nghĩa các hàm hoặc lớp mà không cần chỉ định ngay kiểu dữ liệu (int, string, bool...). Kiểu dữ liệu sẽ được truyền vào như một tham số khi bạn sử dụng.
---
### 1. Ví dụ khi KHÔNG dùng Generic (Thủ công)
Nếu bạn muốn viết một lớp để lưu trữ dữ liệu, bạn phải viết rất nhiều lớp tương tự nhau:
```csharp
class IntBox { public int Data; }
class StringBox { public string Data; }
// Sử dụng:
IntBox box1 = new IntBox(); box1.Data = 10;
StringBox box2 = new StringBox(); box2.Data = "Hello";
```
=> **Vấn đề:** Code bị lặp lại quá nhiều (Redundant).
---
### 2. Ví dụ KHI dùng Generic (Đa năng)
Chúng ta sử dụng ký hiệu `<T>` (T viết tắt của Type - Kiểu dữ liệu). `T` đóng vai trò là một "biến" đại diện cho kiểu dữ liệu sẽ dùng sau này.
```csharp
// T là một kiểu dữ liệu chưa xác định
class MyBox<T>
{
public T Data;
public void ShowInfo()
{
Console.WriteLine($"Kiểu dữ liệu: {typeof(T)}, Giá trị: {Data}");
}
}
// Cách sử dụng:
class Program
{
static void Main()
{
// 1. Dùng cho kiểu int
MyBox<int> intBox = new MyBox<int>();
intBox.Data = 123;
intBox.ShowInfo();
// 2. Dùng cho kiểu string
MyBox<string> strBox = new MyBox<string>();
strBox.Data = "Xin chào Generic";
strBox.ShowInfo();
// 3. Dùng cho kiểu bool
MyBox<bool> boolBox = new MyBox<bool>();
boolBox.Data = true;
}
}
```
---
### 3. Ví dụ về Hàm Generic (Generic Method)
Bạn có thể viết một hàm có thể in bất cứ mảng nào:
```csharp
public void PrintArray<T>(T[] array)
{
foreach (T item in array)
{
Console.Write(item + " ");
}
}
// Sử dụng:
int[] numbers = { 1, 2, 3 };
string[] names = { "An", "Bình", "Chi" };
PrintArray<int>(numbers); // In mảng số
PrintArray<string>(names); // In mảng chữ
```
---
### Lợi ích cực lớn của Generic:
1. **Tái sử dụng code:** Viết một lần, dùng cho mọi kiểu dữ liệu (như `List<T>` của C#).
2. **An toàn kiểu dữ liệu (Type Safety):** Nếu bạn tạo `List<int>`, bạn không thể vô tình thêm một `string` vào đó. Trình biên dịch sẽ báo lỗi ngay lập tức.
3. **Hiệu năng cao:** Không tốn chi phí chuyển đổi kiểu dữ liệu (Boxing/Unboxing) như khi dùng kiểu `object` ngày xưa.
**Tóm lại:** Generic giúp bạn viết code **"Thông minh hơn, ít tốn sức hơn"**. Khi thấy ký hiệu `<T>`, hãy hiểu đó là một cái "khuôn" sẵn sàng đúc cho bất cứ kiểu dữ liệu nào bạn bỏ vào.
**18. Exception (Ngoại lệ)**
Xử lý lỗi khi chương trình đang chạy.
```csharp
try { int x = 0; int y = 5/x; } catch { Console.WriteLine("Lỗi chia cho 0"); }
```
**19. Type Inference (var) & Cú pháp mới**
```csharp
var x = 10; // Tự hiểu là int
var name = person?.Name; // Null-conditional: Nếu person null thì name null, không lỗi.
string s = input ?? "Default"; // Null-coalescing: Nếu input null thì lấy "Default".
```
---
### MỨC ĐỘ 4: KỸ THUẬT LẬP TRÌNH CHUYÊN SÂU
*Dành cho người muốn xây dựng hệ thống phức tạp, hiệu năng cao.*
**20. Delegate & Lambda Expression**
Truyền hàm như một tham số.
```csharp
Func<int, int> square = x => x * x;
```
**21. LINQ**
Truy vấn dữ liệu như SQL ngay trong code.
```csharp
var result = list.Where(n => n > 5).ToList();
```
**22. Async / Await & Multithreading (TPL)**
Lập trình bất đồng bộ, giúp ứng dụng không bị "treo".
```csharp
await Task.Run(() => DoSomething());
```
**23. Event**
Cơ chế thông báo khi có sự kiện xảy ra.
Để hiểu về **Event (Sự kiện)**, hãy tưởng tượng một ví dụ thực tế: **Kênh YouTube và Người đăng ký.**
* **Kênh YouTube (Publisher - Người phát):** Khi họ đăng video mới, họ sẽ "phát đi một thông báo". Họ không cần biết cụ thể ai đang xem, họ chỉ việc nhấn nút "Đăng".
* **Người đăng ký (Subscriber - Người nhận):** Những ai đã nhấn nút "Đăng ký" và "Bật chuông" sẽ tự động nhận được thông báo và thực hiện hành động (mở điện thoại lên xem).
Trong lập trình, Event giúp các lớp (class) liên lạc với nhau mà không cần phụ thuộc quá chặt chẽ vào nhau.
---
### Ví dụ: Hệ thống Nồi cơm điện thông minh
Giả sử bạn có một cái nồi cơm điện. Khi cơm chín, nó sẽ phát ra một sự kiện. Bạn có thể kết nối sự kiện này với "Loa báo" hoặc "Điện thoại" để nhận thông báo.
#### 1. Lớp phát sự kiện (Publisher)
```csharp
public class RiceCooker
{
// 1. Khai báo event: "Khi cơm chín"
// Action là một loại delegate (hàm) không trả về giá trị
public event Action OnCooked;
public void StartCooking()
{
Console.WriteLine("Đang nấu cơm...");
Thread.Sleep(2000); // Giả lập thời gian nấu
// 2. Kích hoạt sự kiện (Phát thông báo)
// Dấu ?. dùng để kiểm tra xem có ai đăng ký nghe sự kiện này không
OnCooked?.Invoke();
}
}
```
#### 2. Lớp nhận sự kiện (Subscriber)
```csharp
public class Smartphone
{
public void Notify()
{
Console.WriteLine("Điện thoại: Reng reng! Cơm chín rồi, về ăn thôi!");
}
}
public class SmartSpeaker
{
public void Speak()
{
Console.WriteLine("Loa: Tinh ting! Cơm đã chín!");
}
}
```
#### 3. Kết nối và Chạy chương trình
```csharp
class Program
{
static void Main()
{
RiceCooker cooker = new RiceCooker();
Smartphone iphone = new Smartphone();
SmartSpeaker googleHome = new SmartSpeaker();
// 3. Đăng ký nhận sự kiện (Dùng toán tử +=)
cooker.OnCooked += iphone.Notify;
cooker.OnCooked += googleHome.Speak;
// Bắt đầu nấu
cooker.StartCooking();
// Nếu không muốn nhận thông báo nữa (Hủy đăng ký)
// cooker.OnCooked -= iphone.Notify;
}
}
```
---
### Tại sao lại dùng Event mà không gọi hàm trực tiếp?
Nếu bạn gọi trực tiếp trong lớp `RiceCooker` như thế này:
```csharp
public void StartCooking() {
// ... nấu xong
iphone.Notify();
googleHome.Speak();
}
```
=> **Nhược điểm:** Lớp `RiceCooker` bây giờ phải biết về `iphone` và `googleHome`. Nếu sau này bạn mua thêm một cái Đèn thông minh, bạn lại phải sửa code của `RiceCooker`.
**Với Event:**
* `RiceCooker` hoàn toàn độc lập. Nó chỉ "hét lên": **"Tôi nấu xong rồi!"**.
* Bất kỳ thiết bị nào (Điện thoại, Loa, Đèn...) muốn làm gì thì tự đi mà đăng ký (`+=`).
* Điều này giúp code cực kỳ linh hoạt và dễ mở rộng.
### Các thành phần chính cần nhớ:
1. **Khai báo:** `public event Action TênSựKiện;`
2. **Kích hoạt:** `TênSựKiện?.Invoke();`
3. **Đăng ký:** `đốiTượng.TênSựKiện += HàmXửLý;`
4. **Hủy đăng ký:** `đốiTượng.TênSựKiện -= HàmXửLý;`
**24. Ref / Out / In & Boxing/Unboxing**
Quản lý bộ nhớ và hiệu suất (Memory management).
Đây là những kiến thức đi sâu vào cách C# điều phối bộ nhớ (Stack và Heap). Hiểu rõ chúng sẽ giúp bạn viết code tối ưu hiệu suất, tránh lãng phí tài nguyên.
---
### 1. Ref, Out, In (Truyền tham chiếu)
Mặc định trong C#, khi bạn truyền một biến vào hàm, nó sẽ tạo ra một **bản sao** (pass by value). Nếu bạn muốn hàm tác động trực tiếp lên biến gốc ở bên ngoài, bạn dùng các từ khóa này.
#### **Ref (Reference - Truyền vào để sửa)**
Biến phải được khởi tạo giá trị trước khi truyền vào. Hàm có thể đọc và sửa giá trị đó.
```csharp
void Increase(ref int n) {
n++; // Sửa trực tiếp trên biến gốc
}
int myNum = 10;
Increase(ref myNum);
Console.WriteLine(myNum); // Kết quả: 11
```
#### **Out (Output - Lấy giá trị ra)**
Biến không cần khởi tạo trước. Hàm **bắt buộc** phải gán giá trị cho biến này trước khi kết thúc. Thường dùng khi muốn hàm trả về nhiều giá trị cùng lúc.
```csharp
void GetSize(out int width, out int height) {
width = 1920;
height = 1080;
}
int w, h;
GetSize(out w, out h); // Lấy được cả 2 giá trị ra
```
#### **In (Input - Truyền tham chiếu chỉ đọc)**
Biến được truyền vào theo kiểu tham chiếu để tăng tốc độ (không phải copy biến lớn), nhưng hàm **không được phép sửa** giá trị đó.
```csharp
void PrintLargeData(in int largeNumber) {
// largeNumber = 100; // LỖI! Không được sửa vì có từ khóa 'in'
Console.WriteLine(largeNumber);
}
```
---
### 2. Boxing và Unboxing (Đóng gói và Mở gói)
Đây là quá trình chuyển đổi giữa **Kiểu giá trị** (Value Type - nằm ở Stack) và **Kiểu tham chiếu** (Reference Type - nằm ở Heap).
* **Boxing:** Coi một giá trị (như `int`) là một đối tượng (`object`). Máy tính phải copy giá trị từ Stack lên Heap và bọc nó lại. (Tốn tài nguyên).
* **Unboxing:** Lấy giá trị từ trong "gói" `object` ở Heap đưa ngược về Stack.
```csharp
int i = 123; // Kiểu giá trị (nằm ở Stack)
// BOXING: Đưa 'i' vào một chiếc hộp đối tượng
object obj = i; // Máy tính tạo một bản sao 123 trên Heap
// UNBOXING: Mở hộp lấy giá trị ra
int j = (int)obj; // Phải ép kiểu tường minh, máy copy 123 về lại Stack
```
**Tại sao cần quan tâm?**
Nếu bạn thực hiện Boxing/Unboxing hàng triệu lần (ví dụ trong vòng lặp), chương trình sẽ bị chậm đáng kể và làm bộ nhớ (GC - Garbage Collector) phải làm việc quá tải.
*Lời khuyên: Hãy dùng **Generic (`List<int>`)** thay vì các mảng đối tượng (`ArrayList`) để tránh việc này.*
---
### Tóm tắt so sánh để dễ nhớ:
| Từ khóa | Ý nghĩa đơn giản |
| :--- | :--- |
| **Ref** | Đưa chìa khóa nhà cho thợ, thợ có thể vào xem và sửa nhà. |
| **Out** | Đưa một cái hộp rỗng, thợ bắt buộc phải bỏ đồ vào hộp đó cho bạn. |
| **In** | Đưa chìa khóa nhà, thợ chỉ được vào xem, cấm sửa bất cứ thứ gì. |
| **Boxing** | Bỏ viên kim cương (giá trị) vào một cái két sắt (đối tượng) để cất vào kho (Heap). |
| **Unboxing** | Mở két sắt trong kho để lấy viên kim cương về lại túi quần (Stack). |
Việc sử dụng đúng `ref/out/in` giúp giảm bớt việc sao chép dữ liệu vô ích, còn tránh `boxing/unboxing` giúp chương trình chạy mượt mà hơn.
**25. IEnumerable / IEnumerator / ICollection**
Hiểu sâu về cách duyệt qua các danh sách dữ liệu.
Để hiểu về 3 khái niệm này, hãy tưởng tượng bạn có một **Cuốn sách**.
1. **IEnumerable**: Là khả năng "có thể đọc được" của cuốn sách. Nó hứa rằng bạn có thể đi từ đầu đến cuối cuốn sách.
2. **IEnumerator**: Là cái "ngón tay" của bạn. Nó giữ vị trí bạn đang đọc (trang hiện tại) và giúp bạn lật sang trang tiếp theo.
3. **ICollection**: Là một "cuốn sổ tay kẹp tài liệu". Ngoài việc đọc được, bạn còn có thể biết nó có bao nhiêu trang, có thể xé bỏ một trang hoặc kẹp thêm trang mới vào.
---
### 1. IEnumerable (Khả năng duyệt)
Đây là giao diện (interface) cơ bản nhất. Nếu một danh sách là `IEnumerable`, bạn có thể dùng vòng lặp `foreach` để duyệt qua nó. Nó **chỉ cho phép đọc**, không cho sửa.
```csharp
IEnumerable<string> playlist = new List<string> { "Bài 1", "Bài 2", "Bài 3" };
foreach (var music in playlist) {
Console.WriteLine(music);
}
// Ưu điểm: Rất nhẹ, an toàn vì không cho phép xóa/sửa dữ liệu khi đang đọc.
```
---
### 2. IEnumerator (Bộ đếm - Cách thức duyệt)
`IEnumerable` thực chất không tự duyệt, nó gọi một "trình điều khiển" là `IEnumerator` để làm việc đó. Đây là cách mà `foreach` hoạt động ngầm bên dưới:
```csharp
var lister = playlist.GetEnumerator(); // Lấy "ngón tay" chỉ mục
while (lister.MoveNext()) { // Di chuyển ngón tay đến trang tiếp theo
Console.WriteLine(lister.Current); // Đọc nội dung trang hiện tại
}
```
* **MoveNext()**: Kiểm tra xem còn phần tử tiếp theo không.
* **Current**: Lấy giá trị phần tử hiện tại.
---
### 3. ICollection (Quản lý danh sách)
`ICollection` bao gồm tất cả khả năng của `IEnumerable` nhưng mạnh mẽ hơn. Nó cho phép bạn thay đổi danh sách.
```csharp
ICollection<string> myCart = new List<string> { "Laptop", "Chuột" };
// 1. Có thể biết số lượng
Console.WriteLine(myCart.Count);
// 2. Có thể thêm/xóa
myCart.Add("Bàn phím");
myCart.Remove("Laptop");
// 3. Kiểm tra tồn tại
bool hasMouse = myCart.Contains("Chuột");
```
---
### Khi nào dùng cái nào?
| Giao diện | Khi nào nên dùng? | Ví dụ thực tế |
| :--- | :--- | :--- |
| **IEnumerable** | Khi bạn **chỉ muốn duyệt qua** danh sách (để in ra, để tính tổng...) và không muốn ai sửa đổi nó. | Danh sách sản phẩm chỉ để hiển thị trên web. |
| **ICollection** | Khi bạn cần **thêm, xóa** hoặc **đếm** số lượng phần tử. | Giỏ hàng của người dùng. |
| **IList** (Cao hơn) | Khi bạn cần truy cập phần tử qua **chỉ số (Index)** như `list[5]`. | Danh sách xếp hạng (hạng 1, hạng 2...). |
### Tại sao cần phân biệt?
Trong các dự án lớn, việc chọn đúng kiểu dữ liệu giúp code của bạn **an toàn** và **nhanh** hơn.
* Nếu một hàm chỉ cần in danh sách, hãy nhận vào `IEnumerable`. Điều này ngăn chặn việc vô tình xóa mất dữ liệu trong lúc in.
* Nếu dùng `IEnumerable`, hệ thống có thể thực hiện "Lazy Loading" (chỉ lấy dữ liệu khi cần đến), giúp tiết kiệm bộ nhớ cực lớn khi làm việc với hàng triệu bản ghi.
---
### MỨC ĐỘ 5: KIẾN TRÚC VÀ TƯ DUY HỆ THỐNG (SENIOR/EXPERT)
*Tập trung vào cấu trúc, bảo mật và khả năng mở rộng mã nguồn.*
**26. Dependency Injection (DI)**
Tiêm phụ thuộc để giảm sự phụ thuộc giữa các lớp.
Để hiểu về **Dependency Injection (DI - Tiêm phụ thuộc)**, hãy tưởng tượng một ví dụ thực tế: **Cái Đèn Pin và Viên Pin.**
1. **Cách làm sai (Phụ thuộc chặt chẽ):** Bạn chế tạo một cái đèn pin mà viên pin được **hàn chết** vào mạch điện bên trong. Khi hết pin, bạn phải vứt cả cái đèn hoặc tháo tung đèn ra để hàn lại. Đèn này "phụ thuộc" hoàn toàn vào đúng viên pin đó.
2. **Cách dùng DI (Phụ thuộc lỏng lẻo):** Bạn chế tạo cái đèn pin có một **ngăn chứa pin** (đây là Interface). Bạn có thể lắp pin Con Thỏ, pin Duracell hay pin sạc vào đều được. Viên pin được "tiêm" (đưa vào) từ bên ngoài.
Trong lập trình, DI là việc bạn **không khởi tạo** các đối tượng phụ thuộc bên trong một lớp, mà **truyền chúng vào** thông qua Constructor (hàm khởi tạo).
---
### 1. Khi chưa dùng DI (Vấn đề)
Giả sử lớp `Car` (Xe) tự tạo một cái `GasEngine` (Máy xăng) bên trong nó.
```csharp
public class GasEngine {
public void Start() => Console.WriteLine("Máy xăng nổ: Bùm bùm!");
}
public class Car {
private GasEngine _engine;
public Car() {
// XE TỰ TẠO MÁY: Phụ thuộc chặt chẽ (Hard-coded)
_engine = new GasEngine();
}
public void Drive() {
_engine.Start();
Console.WriteLine("Xe đang chạy...");
}
}
```
**Vấn đề:** Nếu sau này bạn muốn làm Xe Điện (`ElectricEngine`), bạn phải mở code của lớp `Car` ra và sửa lại. Điều này vi phạm nguyên tắc "Đóng/Mở" (SOLID) và rất khó để làm Unit Test.
---
### 2. Khi dùng Dependency Injection (Giải pháp)
Chúng ta tạo ra một cái "khuôn" chung là `IEngine`.
#### Bước 1: Tạo Interface (Ngăn chứa pin)
```csharp
public interface IEngine {
void Start();
}
```
#### Bước 2: Tạo các loại Máy khác nhau
```csharp
public class GasEngine : IEngine {
public void Start() => Console.WriteLine("Máy xăng nổ!");
}
public class ElectricEngine : IEngine {
public void Start() => Console.WriteLine("Máy điện chạy êm ru...");
}
```
#### Bước 3: Lớp Car nhận máy từ bên ngoài (DI)
```csharp
public class Car {
private readonly IEngine _engine;
// DI TRONG CONSTRUCTOR: "Tiêm" cái máy vào đây
public Car(IEngine engine) {
_engine = engine;
}
public void Drive() {
_engine.Start();
Console.WriteLine("Xe đang đi chuyển.");
}
}
```
---
### 3. Cách sử dụng (Nơi thực hiện "Tiêm")
Bây giờ, lớp `Car` trở nên cực kỳ linh hoạt. Bạn muốn xe gì, bạn chỉ cần "tiêm" loại máy đó vào lúc tạo xe.
```csharp
class Program {
static void Main() {
// Tiêm máy xăng vào xe
IEngine gas = new GasEngine();
Car myGasCar = new Car(gas);
myGasCar.Drive();
// Tiêm máy điện vào xe (Không cần sửa một dòng code nào trong lớp Car)
IEngine ecoFriendly = new ElectricEngine();
Car myTesla = new Car(ecoFriendly);
myTesla.Drive();
}
}
```
---
### Tại sao DI lại quan trọng?
1. **Dễ thay đổi:** Hôm nay dùng Máy xăng, mai dùng Máy điện, mốt dùng Máy hydro... chỉ cần thay đổi ở lúc khởi tạo, không cần sửa lớp `Car`.
2. **Dễ kiểm thử (Unit Test):** Bạn có thể tạo một cái "Máy giả" (Mock Engine) để kiểm tra xem lớp `Car` có hoạt động đúng không mà không cần nổ máy thật.
3. **Code sạch và tách biệt:** Lớp `Car` chỉ lo việc chạy xe, nó không cần quan tâm cái máy được chế tạo phức tạp như thế nào.
**Tóm lại:** DI giống như việc bạn cung cấp "linh kiện" từ bên ngoài vào cho một cỗ máy, thay vì để cỗ máy đó tự sản xuất linh kiện cho chính nó. Điều này giúp hệ thống của bạn lắp ghép cực kỳ linh hoạt.
**27. SOLID Principles**
SOLID là bộ 5 nguyên tắc thiết kế phần mềm giúp code của bạn dễ đọc, dễ bảo trì và dễ mở rộng. Dưới đây là ví dụ cụ thể cho từng nguyên tắc bằng ngôn ngữ C#:
---
### 1. S - Single Responsibility Principle (Nguyên tắc đơn trách nhiệm)
**Định nghĩa:** Một lớp chỉ nên giữ một trách nhiệm duy nhất. Nếu một lớp làm quá nhiều việc, khi thay đổi một chức năng, các chức năng khác dễ bị lỗi theo.
* **Vi phạm:** Lớp `User` vừa chứa thông tin người dùng, vừa thực hiện lưu vào database, vừa gửi email.
* **Áp dụng SOLID:** Chia nhỏ thành các lớp riêng biệt.
```csharp
// Lớp này chỉ quản lý dữ liệu người dùng
public class User {
public string Email { get; set; }
}
// Lớp này chỉ lo việc lưu trữ
public class UserRepository {
public void Save(User user) { /* Lưu vào DB */ }
}
// Lớp này chỉ lo việc gửi thông báo
public class EmailService {
public void SendWelcomeEmail(User user) { /* Gửi mail */ }
}
```
---
### 2. O - Open/Closed Principle (Nguyên tắc Đóng/Mở)
**Định nghĩa:** Bạn nên thoải mái mở rộng một lớp (thêm tính năng mới), nhưng không được sửa đổi code bên trong lớp đó.
* **Vi phạm:** Dùng `if-else` hoặc `switch` trong hàm tính lương. Mỗi lần có chức năng mới lại phải vào sửa hàm đó.
* **Áp dụng SOLID:** Sử dụng kế thừa hoặc Interface.
```csharp
public abstract class Employee {
public abstract double CalculateSalary();
}
// Khi thêm loại nhân viên mới, chỉ cần tạo lớp mới, không sửa code cũ
public class FullTimeEmployee : Employee {
public override double CalculateSalary() => 5000;
}
public class PartTimeEmployee : Employee {
public override double CalculateSalary() => 2000;
}
```
---
### 3. L - Liskov Substitution Principle (Nguyên tắc thay thế Liskov)
**Định nghĩa:** Các đối tượng của lớp con phải có thể thay thế cho đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình.
* **Vi phạm:** Lớp cha là `Bird` có hàm `Fly()`. Lớp con `Penguin` (Chim cánh cụt) kế thừa `Bird` nhưng chim cánh cụt không biết bay, dẫn đến hàm `Fly()` bị lỗi.
* **Áp dụng SOLID:** Thiết kế lại cây kế thừa.
```csharp
public interface IBird { void Eat(); }
public interface IFlyingBird : IBird { void Fly(); }
public class Eagle : IFlyingBird {
public void Eat() { }
public void Fly() { }
}
public class Penguin : IBird { // Chim cánh cụt chỉ thực hiện ăn, không có bay
public void Eat() { }
}
```
---
### 4. I - Interface Segregation Principle (Nguyên tắc phân tách Interface)
**Định nghĩa:** Thay vì dùng một Interface lớn cho mọi thứ, hãy chia thành nhiều Interface nhỏ để các lớp không phải triển khai những phương thức mà chúng không dùng tới.
* **Vi phạm:** Interface `IWorker` có `Work()` và `Eat()`. Nhưng lớp `Robot` kế thừa `IWorker` thì không biết `Eat()` (ăn).
* **Áp dụng SOLID:** Chia nhỏ Interface.
```csharp
public interface IWorkable { void Work(); }
public interface IEatable { void Eat(); }
public class Human : IWorkable, IEatable {
public void Work() { }
public void Eat() { }
}
public class Robot : IWorkable {
public void Work() { } // Robot chỉ cần làm việc, không cần ăn
}
```
---
### 5. D - Dependency Inversion Principle (Nguyên tắc đảo ngược phụ thuộc)
**Định nghĩa:** Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào trừu tượng (Interface).
* **Vi phạm:** Lớp `Car` khởi tạo trực tiếp lớp `GasEngine` bên trong nó. Nếu muốn đổi sang `ElectricEngine`, bạn phải sửa lại lớp `Car`.
* **Áp dụng SOLID:** Dùng Dependency Injection.
```csharp
public interface IEngine { void Start(); }
public class GasEngine : IEngine {
public void Start() => Console.WriteLine("Máy xăng nổ");
}
public class ElectricEngine : IEngine {
public void Start() => Console.WriteLine("Máy điện chạy");
}
public class Car {
private readonly IEngine _engine;
// Car không quan tâm là loại máy gì, miễn là nó thực hiện IEngine
public Car(IEngine engine) {
_engine = engine;
}
public void Run() => _engine.Start();
}
```
### Tổng kết:
- **S:** Mỗi lớp 1 việc.
- **O:** Thêm mới, không sửa cũ.
- **L:** Con không được làm hỏng hành vi của cha.
- **I:** Đừng ép dùng cái không cần.
- **D:** Liên kết qua Interface, đừng liên kết trực tiếp.
**28. Design Patterns**
Các giải pháp mẫu cho các vấn đề lập trình phổ biến (Singleton, Factory, Observer...).
**29. Unit Test**
Viết mã để kiểm tra mã, đảm bảo chất lượng phần mềm.
Để hiểu về **Unit Test**, hãy tưởng tượng bạn là một thợ xây. Trước khi bàn giao ngôi nhà, bạn dùng thước đo lại từng viên gạch, kiểm tra từng ổ cắm điện xem có thông mạch hay không. Việc kiểm tra từng chi tiết nhỏ nhất đó chính là "Unit Test".
Trong lập trình, **Unit Test** là những đoạn code phụ trợ, được viết ra để kiểm tra xem một hàm (phương thức) cụ thể có chạy đúng như mong đợi hay không.
Dưới đây là ví dụ đơn giản sử dụng framework **xUnit** (phổ biến nhất trong C#):
### 1. Đoạn code cần kiểm tra (Code chính)
Giả sử bạn có một lớp tính toán đơn giản:
```csharp
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
```
### 2. Đoạn code Unit Test (Code kiểm tra)
Bạn sẽ viết một hàm khác để "thử nghiệm" hàm `Add` ở trên. Một Unit Test thường tuân theo quy tắc **AAA (Arrange - Act - Assert)**:
```csharp
using Xunit; // Thư viện dùng để test
public class CalculatorTests
{
[Fact] // Đánh dấu đây là một bài kiểm tra
public void Add_ShouldReturnCorrectSum()
{
// 1. Arrange (Chuẩn bị): Thiết lập các đối tượng và dữ liệu đầu vào
var calculator = new Calculator();
int a = 5;
int b = 10;
int expectedResult = 15;
// 2. Act (Thực hiện): Gọi hàm cần kiểm tra
int actualResult = calculator.Add(a, b);
// 3. Assert (Kiểm tra): So sánh kết quả thực tế với kết quả mong đợi
Assert.Equal(expectedResult, actualResult);
}
}
```
### Tại sao Unit Test lại quan trọng?
1. **Phát hiện lỗi sớm:** Nếu một ngày nào đó bạn vô tình sửa hàm `Add` thành `return a - b;`, khi bạn chạy Unit Test, hệ thống sẽ báo **"Fail"** (Thất bại) ngay lập tức. Bạn sẽ biết mình vừa làm sai trước khi gửi code cho khách hàng.
2. **Tự tin thay đổi code (Refactoring):** Bạn muốn tối ưu lại code cho chạy nhanh hơn nhưng sợ làm hỏng tính năng cũ? Nếu có Unit Test bao phủ, bạn chỉ cần sửa xong và chạy lại test. Nếu tất cả hiện màu xanh (**Pass**), bạn có thể yên tâm.
3. **Tài liệu sống:** Nhìn vào Unit Test, người khác sẽ hiểu ngay: "À, hàm này nhận vào 5 và 10 thì kết quả phải ra 15".
### Quy trình làm việc thực tế:
* Bạn viết code.
* Bạn nhấn một nút (trong Visual Studio) để chạy toàn bộ các bài Unit Test.
* Nếu tất cả bài test vượt qua (màu xanh): Code ổn.
* Nếu có bài test thất bại (màu đỏ): Có lỗi ở đâu đó, cần sửa ngay.
**30. Socket, Cryptography (Mã hóa), Hashing**
Đây là ba trụ cột quan trọng trong việc xây dựng các ứng dụng kết nối Internet và bảo vệ thông tin người dùng. Hãy cùng đi vào từng khái niệm qua các ví dụ đời thường:
---
### 1. Socket (Lập trình mạng)
**Khái niệm:** Hãy tưởng tượng **Socket** giống như một chiếc **Phích cắm điện**. Để bóng đèn sáng, phích cắm (Client) phải cắm vào ổ điện (Server). Trong lập trình, Socket là "cổng kết nối" giúp hai máy tính nói chuyện với nhau qua mạng.
**Ví dụ:** Một ứng dụng Chat đơn giản.
* **Server:** Luôn ngồi đợi ở một "Cổng" (Port) cố định.
* **Client:** Biết địa chỉ (IP) và số cổng của Server để kết nối.
```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
// --- PHÍA SERVER ---
TcpListener server = new TcpListener(IPAddress.Any, 8080);
server.Start(); // Bắt đầu mở cửa đợi khách
// --- PHÍA CLIENT ---
TcpClient client = new TcpClient("127.0.0.1", 8080); // Kết nối đến Server
NetworkStream stream = client.GetStream();
// Gửi tin nhắn
byte[] data = Encoding.UTF8.GetBytes("Xin chào Server!");
stream.Write(data, 0, data.Length);
```
---
### 2. Hashing (Băm dữ liệu - Một chiều)
**Khái niệm:** Hãy tưởng tượng bạn nghiền một quả táo thành sinh tố. Bạn không bao giờ có thể biến ly sinh tố đó trở lại thành quả táo ban đầu. Đó là **Hashing**.
* Nó biến một đoạn văn bản thành một chuỗi ký tự lạ (dấu vân tay).
* **Đặc điểm:** Cùng một dữ liệu đầu vào sẽ luôn ra một mã giống nhau, nhưng không thể dịch ngược mã đó ra dữ liệu gốc.
**Ứng dụng:** Lưu trữ mật khẩu. Admin hệ thống cũng không biết mật khẩu của bạn là gì, họ chỉ lưu "mã băm" của nó.
```csharp
using System.Security.Cryptography;
using System.Text;
string password = "MatKhauCuaToi123";
using (SHA256 sha256 = SHA256.Create())
{
byte[] inputBytes = Encoding.UTF8.GetBytes(password);
byte[] hashBytes = sha256.ComputeHash(inputBytes);
// Chuyển kết quả sang chuỗi Hex để lưu vào Database
string hashString = BitConverter.ToString(hashBytes).Replace("-", "");
Console.WriteLine($"Mã băm lưu trong DB: {hashString}");
}
```
---
### 3. Cryptography (Mã hóa - Hai chiều)
**Khái niệm:** Khác với Hashing, mã hóa giống như bạn bỏ một bức thư vào một chiếc **Hòm có khóa**.
* Người gửi dùng **Khóa** để khóa hòm lại (Mã hóa).
* Người nhận phải có **Đúng chiếc khóa đó** mới mở được hòm để đọc thư (Giải mã).
**Ví dụ:** Gửi thông tin thẻ tín dụng khi mua hàng online.
```csharp
// Đây là ví dụ về mã hóa AES (Symmetric - Dùng chung 1 khóa)
using (Aes myAes = Aes.Create())
{
// Nội dung cần giấu
string original = "Số thẻ: 1234-5678-9012";
// 1. Mã hóa (Encrypt) -> Biến thành các ký tự không đọc được
byte[] encrypted = EncryptStringToBytes_Aes(original, myAes.Key, myAes.IV);
// 2. Giải mã (Decrypt) -> Dùng đúng Key và IV để lấy lại nội dung gốc
string roundtrip = DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV);
Console.WriteLine($"Gốc: {original}");
Console.WriteLine($"Sau khi giải mã: {roundtrip}");
}
```
---
### So sánh để phân biệt rõ Hashing và Cryptography:
| Đặc điểm | Hashing (Băm) | Cryptography (Mã hóa) |
| :--- | :--- | :--- |
| **Tính chất** | **Một chiều** (Không thể dịch ngược). | **Hai chiều** (Có thể mã hóa và giải mã). |
| **Mục đích** | Kiểm tra tính toàn vẹn (Dữ liệu có bị sửa không?). | Bảo mật thông tin (Không cho người lạ đọc). |
| **Khóa** | Không cần khóa (dùng thuật toán). | **Bắt buộc phải có khóa** (Key). |
| **Ví dụ** | Lưu mật khẩu, kiểm tra file tải về. | Gửi tin nhắn bí mật, thanh toán online. |
**Tóm lại:**
- **Socket** là "đường ống" dẫn nước.
- **Cryptography** là "két sắt" để bảo vệ nước bên trong ống không bị ai nhìn thấy.
- **Hashing** là "con dấu niêm phong" trên nắp két để biết chắc chắn chưa có ai cạy két.
### Tóm tắt lộ trình học:
1. **Cú pháp cơ bản**
2. **Tư duy Hướng đối tượng**
3. **Làm chủ Dữ liệu**
4. **Xử lý Logic nâng cao**
5. **Kiến trúc phần mềm & Bảo mật**