BÀI 2: Thiết Kế "Hợp Đồng" (Contracts & Interfaces) – Ranh Giới Giữa Các Tầng
Ở Bài 1, chúng ta đã hiểu trừu tượng hóa giúp giấu đi sự phức tạp thế nào. Nhưng câu hỏi đặt ra là: Làm thế nào để các tầng giao tiếp với nhau mà không bị phụ thuộc cứng vào nhau?
Câu trả lời nằm ở khái niệm Interface (Giao diện) hay còn gọi là Hợp đồng (Contract).
1. Bản chất của "Hợp đồng" trong Lập trình
Trong thế giới thực, một bản hợp đồng xây dựng quy định rõ: Chủ đầu tư yêu cầu cái gì (nhà có mấy phòng, sơn màu gì), còn nhà thầu tự lo cách thi công bên trong miễn là bàn giao đúng cam kết. Chủ đầu tư không cần quan tâm nhà thầu dùng gạch hãng nào hay thợ nào làm.
Trong lập trình, Interface chính là bản hợp đồng đó:
- Nó chỉ định nghĩa tên hàm, tham số đầu vào và kiểu dữ liệu trả về (What).
- Nó tuyệt đối không chứa code xử lý logic bên trong (How).
- Khi một lớp (
Class)implementsmột Interface, nó đang ký vào bản cam kết: "Tôi hứa sẽ thực hiện đầy đủ tất cả các hành vi có trong bản hợp đồng này".
2. Tại sao Interface lại giải quyết được bài toán "Tightly Coupled"?
Hãy nhìn lại đoạn code ở Bài 1. Nếu Controller của bạn gọi thẳng một Class cụ thể (Concrete Class) như ElasticSearchRepository:
// ❌ VI PHẠM NGUYÊN TẮC: Controller dính chặt vào Elasticsearch
public function search(Request $request, ElasticSearchRepository $repo) {
$results = $repo->searchName($request->keyword);
return response()->json($results);
}
Vấn đề: Điều gì xảy ra nếu sau này sếp bảo: "Thôi, Elasticsearch đắt đỏ quá, chuyển sang dùng Meilisearch hoặc đơn giản là MySQL Full-text Search đi"?
Bạn sẽ phải sửa lại toàn bộ Controller, đổi kiểu dữ liệu từ ElasticSearchRepository sang MeiliSearchRepository. Nếu hệ thống có 50 Controller gọi đến nó, bạn sẽ phải sửa ở 50 nơi. Đứa nào lười test sót là hệ thống sập.
✅ Giải pháp dùng Contract (Interface):
Thay vì phụ thuộc vào một công nghệ cụ thể, hãy bắt Controller phải phụ thuộc vào Interface:
// Controller chỉ quan tâm đến Hợp đồng (EsRepositoryInterface)
public function search(Request $request, EsRepositoryInterface $repo) {
$results = $repo->newEsBuilder()->where('name', $request->keyword)->get();
return response()->json($results);
}
Bây giờ, bạn muốn đổi từ Elasticsearch sang Meilisearch hay thậm chí là MySQL? Bạn chỉ cần tạo một class mới (ví dụ MeiliSearchRepository) và cho nó implements EsRepositoryInterface.
Bên trong Service Container của Laravel (hoặc framework bạn dùng), bạn chỉ cần cấu hình ánh xạ: "Khi ai gọi EsRepositoryInterface, hãy trả về instance của MeiliSearchRepository". Controller hoàn toàn không phải sửa một dòng code nào cả! Đó chính là sức mạnh của tính Đa hình (Polymorphism) nhờ Abstraction Layer.
3. Nguyên tắc vàng khi thiết kế Interface
Một Interface kém chất lượng thường trông giống như một cái thùng rác: chứa hàng chục hàm không liên quan đến nhau. Để thiết kế một Interface chuẩn mực, hãy tuân thủ các quy tắc sau:
- Tuân thủ Interface Segregation Principle (ISP): Đừng gom tất cả mọi thứ vào một Interface khổng lồ (Fat Interface). Hãy tách nhỏ chúng ra theo từng hành vi cụ thể (ví dụ:
SearchableInterface,CacheableInterface,StorableInterface). Người dùng chỉ cần ký vào bản hợp đồng nào họ thực sự sử dụng. - Tên Interface phải mang tính hành vi hoặc danh từ chung: Thường tận cùng bằng đuôi
Interfacehoặc các tính từ nhưRepository,Service,Contract,-able(ví dụ:Loggable,Renderable). - Chỉ định hình "Cái gì", không bao giờ định hình "Bằng cách nào": Không đưa các biến trạng thái (
public $connection) vào Interface. Interface chỉ thuần túy là bộ khung hành động.
Tổng kết
- Interface (Contract) là biên giới kỹ thuật giúp ngắt đứt sự phụ thuộc trực tiếp giữa các tầng trong ứng dụng.
- Nhờ có Interface, bạn có thể thay đổi công nghệ bên dưới (Low-level details) mà không làm ảnh hưởng đến tầng nghiệp vụ bên trên (High-level business logic).
Sẵn sàng cho Bài 3 chưa? Trong bài tiếp theo, chúng ta sẽ bóc tách kỹ thuật Dependency Injection (DI) – chiếc cầu nối biến các lý thuyết trừu tượng này thành hiện thực chạy mượt mà trong ứng dụng thực tế.
All rights reserved