1. Giới thiệu
Vậy là, tụi mình đã thấy interface là những hợp đồng cực mạnh. Thế lập trình ở mức interface là gì? Đó là một triết lý thiết kế nói rằng: "Phụ thuộc vào abstraction, không phải vào implementation cụ thể."
Lại ví dụ nhé. Bạn muốn pha cà phê. Bạn mua máy pha cà phê. Bạn đâu quan tâm đó là model cụ thể "Sieu-Puper-May V3000" hay "Mega-Automat Barista 5000", đúng không? Điều bạn cần là cái máy đó biết "làm cà phê" – tức là, nó implement "hợp đồng" ICoffeeMaker. Bạn chỉ cần bấm nút "Làm cà phê", nó sẽ làm. Còn nó làm kiểu gì – dùng hạt xay, capsule hay gì khác – bạn, với tư cách người dùng, cũng chẳng quan tâm lắm.
Trong code, điều này nghĩa là thay vì trong method của bạn yêu cầu kiểu cụ thể Document hay Image, bạn sẽ yêu cầu IPrintable. Code của bạn sẽ chạy với bất kỳ object nào implement interface này, không cần biết nó là loại gì cụ thể.
Hợp đồng interface — là đảm bảo rằng bất kỳ class nào implement interface đều phải định nghĩa tất cả member của interface (method, property, event). Với chương trình, bất kỳ object nào implement interface đều hỗ trợ một "bộ khả năng" nhất định, và bạn có thể tin tưởng vào điều đó, dù không biết bên trong nó làm gì.
Giả sử bạn hẹn đồng nghiệp luôn gặp nhau ở ghế sofa đỏ ngoài sảnh. Anh ấy tới đó bằng thang máy, đi bộ hay chạy trên tường cũng được – không quan trọng. Quan trọng là: anh ấy sẽ ở đó. Interface chính là "ghế sofa đỏ" cho code.
Ví dụ
public interface IPrintable
{
void Print();
}
Hợp đồng: ai implement IPrintable thì phải biết tự in ra màn hình. Còn in kiểu gì là chuyện riêng của nó.
2. Hợp đồng như điểm giao tiếp giữa các phần của hệ thống
Thực tế nó hoạt động ra sao
Một hệ thống có thể có hàng chục class, do nhiều người tạo, vào nhiều thời điểm, với nhiều mục đích khác nhau. Nhưng nếu tất cả tuân thủ một hợp đồng (tức là implement cùng một interface), thì bạn có thể dùng chúng một cách đồng nhất.
Ví dụ thực tế: tưởng tượng bạn có database khách hàng. Class Customer, class Employee, class Contractor. Mỗi class lưu thông tin theo cách riêng, nhưng nếu tất cả implement interface IContactInfo, yêu cầu method GetEmail() và GetPhoneNumber(), thì code bên ngoài không cần biết object đó là loại gì — chỉ cần lấy được email và số điện thoại là đủ.
public interface IContactInfo
{
string GetEmail();
string GetPhoneNumber();
}
public class Customer : IContactInfo
{
public string Email { get; set; }
public string Phone { get; set; }
public string GetEmail() => Email;
public string GetPhoneNumber() => Phone;
}
// Tương tự cho Employee và Contractor...
Giờ nếu bạn cần in dữ liệu của tất cả những ai công ty bạn có liên hệ (không quan trọng là ai), bạn chỉ cần duyệt qua list IContactInfo, gọi các method cần thiết — và mọi thứ chạy ngon lành.
3. Lập trình "ở mức interface"
Lập trình ở mức interface — nghĩa là viết code chỉ phụ thuộc vào interface (tức là hợp đồng), không phụ thuộc vào class cụ thể. Class implement interface có thể là gì cũng được, miễn là nó đáp ứng yêu cầu của interface.
Tại sao điều này quan trọng?
- Khả năng mở rộng: dễ thêm loại mới mà không phải sửa code cũ.
- Dễ test: dễ dàng thay object bằng mock khi test.
- Linh hoạt: implementation có thể thay đổi mỗi ngày, interface vẫn ổn định.
- Kiến trúc sạch: các module ít phụ thuộc vào nhau, dễ tái sử dụng.
Ví dụ — Chương trình quen thuộc: "Tài khoản ngân hàng"
Ở các ví dụ trước, tụi mình có abstract class BankAccount với abstract method Withdraw(), còn các loại cụ thể (SavingsAccount, CheckingAccount) thì implement chi tiết.
Giờ thử thêm interface — ví dụ, để in thông tin số dư:
public interface IBalanceReporter
{
void ReportBalance();
}
public abstract class BankAccount : IBalanceReporter
{
public double Balance { get; set; }
public abstract void Withdraw(double amount);
public void ReportBalance()
{
Console.WriteLine($"Số dư hiện tại: {Balance} euro");
}
}
Giờ ai làm việc với IBalanceReporter đều có thể gọi ReportBalance() mà không cần biết loại tài khoản cụ thể.
4. Xử lý chung và đa hình qua hợp đồng
Ví dụ: handler chung
Khi đã làm việc với interface, bạn có thể tạo method tổng quát, không phụ thuộc vào loại object:
static void PrintAllBalances(IBalanceReporter[] accounts)
{
foreach (var reporter in accounts)
{
reporter.ReportBalance();
}
}
List này có thể chứa bất kỳ object nào implement IBalanceReporter: SavingsAccount, CheckingAccount, thậm chí MockAccountForTesting. Không có phép màu gì cả, mọi thứ chạy đúng luật.
Sơ đồ: hợp đồng interface mang lại gì
+-------------------+ implement +-------------------+
| BankAccount | <---------------- | IBalanceReporter |
| (SavingsAccount) | |-------------------|
| (CheckingAccount)| | + ReportBalance() |
+-------------------+ +-------------------+
| ^
| |
+-----------+-----------------------+
|
Bất kỳ class nào khác implement hợp đồng
5. Hợp đồng, business logic và thiết kế kiến trúc
Hợp đồng interface tách rõ cái gì class phải làm, khỏi cách nó thực hiện. Vì vậy, các kiến trúc sư phần mềm rất thích thiết kế logic hệ thống qua interface. Thường thì interface (hợp đồng) được thiết kế trước, implementation sẽ có sau.
Ví dụ thực tế: hệ thống thanh toán
Ví điện tử, thẻ, PayPal, crypto... Mỗi loại có chi tiết riêng, nhưng nếu bạn trừu tượng hóa và tạo interface IPaymentProvider:
public interface IPaymentProvider
{
void Pay(decimal amount);
bool Refund(decimal amount);
}
Code làm việc với interface này chẳng quan tâm là trả bằng thẻ hay tài khoản. Rất tiện cho kiến trúc lẫn thực tế: bạn có thể thêm hệ thống thanh toán mới mà không phải đụng vào code còn lại.
6. Đưa business logic vào hợp đồng
Hợp đồng cho phép đưa các rule business chính lên interface, còn chi tiết (ví dụ kiểm tra limit, cộng cashback, v.v.) để cho class cụ thể xử lý.
Thêm ví dụ nữa
public interface ILogger
{
void LogInfo(string message);
void LogError(string message);
}
public class ConsoleLogger : ILogger
{
public void LogInfo(string message) => Console.WriteLine($"INFO: {message}");
public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}
public class FileLogger : ILogger
{
public void LogInfo(string message) => /* Ghi vào file */;
public void LogError(string message) => /* Ghi vào file */;
}
Code phía client sẽ như này (không quan trọng dùng logger nào):
void DoWork(ILogger logger)
{
logger.LogInfo("Bắt đầu làm việc.");
// ... làm gì đó ...
logger.LogError("Có lỗi khi làm việc.");
}
Đây chính là lập trình ở mức interface: code client chỉ phụ thuộc vào hợp đồng (interface), không phụ thuộc vào implementation cụ thể.
7. Lỗi và bẫy thường gặp
Người mới hay mắc lỗi: code của họ gắn chặt vào class cụ thể, không phải interface. Điều này dẫn đến nhiều vấn đề:
- Khó thay đổi hay test — phải sửa rất nhiều code.
- Thêm loại mới không được nếu không sửa nhiều chỗ.
- Các module bị "dính" chặt vào nhau.
Ngược lại — nếu ngay từ đầu xây hệ thống dựa trên interface và truyền tham số qua interface, bạn sẽ có code linh hoạt và ít phụ thuộc.
Lời khuyên chính: hãy nghĩ về giao tiếp giữa các module như giao tiếp giữa các hợp đồng, không phải giữa các implementation cụ thể.
GO TO FULL VERSION