Clean Architecture
04 Phần II · SOLID Chương 9–11

LSP · ISP · DIP

Liskov Substitution · Interface Segregation · Dependency Inversion

Ba chữ cái cuối của SOLID đi từ tính thay thế (subtype phải thế chỗ được base) qua tính tối giản phụ thuộc (đừng dính vào thứ bạn không dùng) tới tính đảo chiều (phụ thuộc vào abstraction, không vào concretion). Cả ba hội tụ về một ý: dựng ranh giới đúng chỗ để hệ thống còn mềm. Riêng DIP sẽ là nguyên lý tổ chức xuất hiện đi xuất hiện lại trong mọi sơ đồ kiến trúc về sau.

01 Ba nguyên lý còn lại của SOLID

SRP và OCP (chủ đề 03) lo việc chia trách nhiệmmở rộng không sửa. Ba nguyên lý còn lại bàn về các phần ghép lại với nhau ra sao — và đặc biệt là phụ thuộc giữa chúng nên chạy theo hướng nào.

LSP · Thay thế

Liskov Substitution

Các phần thay thế cho nhau (interchangeable parts) phải tuân thủ một hợp đồng (contract) chung — subtype thế chỗ base mà chương trình vẫn đúng.

ISP · Tách giao diện

Interface Segregation

Đừng phụ thuộc vào thứ bạn không dùng. Interface béo kéo bạn dính vào những thay đổi chẳng liên quan.

DIP · Đảo phụ thuộc

Dependency Inversion

Code high-level policy không phụ thuộc low-level detail; ngược lại, detail phụ thuộc policy qua các abstraction.

Sợi chỉ đỏ xuyên cả ba: vi phạm ở mức class (kế thừa sai, interface béo, gọi thẳng lớp cụ thể) rò rỉ lên kiến trúc — biến thành if/else đặc biệt, redeploy dây chuyền, và những ranh giới đặt sai chỗ.

02 LSP · Tính thay thế

Barbara Liskov (1988) định nghĩa subtype như sau: nếu mọi chương trình P viết theo kiểu T vẫn cư xử y hệt khi ta thay một đối tượng kiểu S vào chỗ của T, thì S là một subtype của T. Nói gọn: phần thay thế phải tôn trọng hợp đồng của phần bị thay.

Để dựng phần mềm từ các phần thay thế cho nhau, những phần đó phải tuân thủ một hợp đồng cho phép chúng thế chỗ lẫn nhau.

LSP · Clean Architecture

Ví dụ kinh điển: Square / Rectangle

Cho Square kế thừa Rectangle nghe rất "toán học", nhưng vi phạm LSP. Rectangle cho phép đổi width và height độc lập; Square thì buộc hai cạnh luôn đổi cùng nhau. Một User tưởng mình cầm Rectangle sẽ vỡ trận:

Pseudo · assertion gãy khi Rectangle thực ra là Square
Rectangle r = makeRectangle();   // ...nhưng thực ra trả về một Square
r.setW(5);
r.setH(2);
assert(r.area() == 10);          // ✗ Square sẽ cho 4 hoặc 25 → assertion thất bại
Cách "vá" duy nhất là thêm if trong User để dò xem Rectangle có thật là Square không. Một khi hành vi của User phụ thuộc vào kiểu cụ thể nó dùng, các kiểu đó không còn substitutable.

LSP leo lên kiến trúc: vụ taxi dispatch

LSP không chỉ về inheritance. Nó áp cho mọi interface & implementation: interface kiểu Java, các lớp Ruby cùng method signature, hay nhiều dịch vụ cùng đáp một REST interface chung. Hãy nhìn điều gì xảy ra khi vi phạm:

Một hệ tổng hợp nhiều taxi dispatch service gửi lệnh điều xe qua REST, lấy URI từ hồ sơ tài xế. Mọi hãng phải tuân cùng một interface — cùng các trường pickupAddress, pickupTime, destination. Rồi lập trình viên của hãng Acme không đọc kỹ spec, viết tắt destination thành dest:

Module dựng lệnh dispatch · vết rò LSP
// Acme không substitutable → buộc phải có nhánh đặc biệt
if (driver.getDispatchUri().startsWith("acme.com")) {
    // ...dựng URI bằng /dest/ thay vì /destination/
}
Nhét chữ "acme" vào code mở cửa cho hàng loạt lỗi kỳ quái và lỗ hổng bảo mật. Acme mua thêm hãng khác? Lại thêm một if nữa cho từng thương hiệu.

Vi phạm LSP

  • Square extends Rectangle: phá vỡ hợp đồng "đổi cạnh độc lập".
  • Acme bẻ cong REST interface chung (destdestination).
  • Hệ quả: if/else đặc biệt rải vào kiến trúc, kiến trúc bị "ô nhiễm".

Tuân thủ LSP

  • Subtype tôn trọng đúng contract của base.
  • Cách ly khác biệt bằng module dựng lệnh dẫn bởi configuration database keyed theo URI.
  • Mọi implementation thật sự thay thế được nhau ⟶ không nhánh đặc biệt.

Một vi phạm nhỏ về tính thay thế có thể khiến kiến trúc bị bơm thêm cả đống cơ chế thừa chỉ để dọn hậu quả. LSP nên được mở rộng tới tận mức kiến trúc.

03 ISP · Đừng phụ thuộc thứ bạn không dùng

Xét lớp OPS có ba thao tác op1, op2, op3. Ba user: User1 chỉ cần op1, User2 chỉ cần op2, User3 chỉ cần op3. Trong ngôn ngữ statically typed (Java), mã của User1 phụ thuộc cả OPS — nên nếu op2 đổi (thứ User1 chẳng quan tâm), User1 vẫn bị recompile & redeploy oan.

✗ Interface béo User1 User2 User3 OPS op1 op2 op3 ✓ Tách interface User1 User2 User3 U1Ops U2Ops U3Ops OPS
Tách OPS thành U1Ops·U2Ops·U3Ops; OPS implement cả ba. Giờ User1 chỉ phụ thuộc U1Ops — đổi op2 không còn buộc User1 recompile/redeploy.

ISP là vấn đề kiến trúc, không chỉ ngôn ngữ

Ngôn ngữ dynamically typed (Ruby, Python) suy ra phụ thuộc lúc runtime, không có source dependency cứng ⟶ dễ tưởng ISP chỉ là chuyện ngôn ngữ. Nhưng gốc rễ sâu hơn: phụ thuộc vào thứ mang theo "hành lý" (baggage) thừa luôn có hại.

Kiến trúc sư muốn dùng framework F trong hệ thống S, nhưng tác giả F trói nó vào database D: S → F → D. Nếu D có tính năng mà F không dùng, thì một thay đổi — hay một lỗi (failure) — trong phần thừa của D vẫn có thể buộc FS redeploy, thậm chí gãy dây chuyền.

Gói cả ISP về một câu: "Đừng phụ thuộc vào những thứ bạn không cần." (Ở mức component, đây chính là Common Reuse Principle — bản tổng quát của ISP.)

04 DIP · Đảo ngược phụ thuộc

Hệ thống linh hoạt nhất là hệ mà mọi source code dependency chỉ trỏ tới abstraction, không tới concretion. Trong Java: import/use chỉ nên trỏ tới interface, abstract class — không gì cụ thể bị phụ thuộc cả.

Code hiện thực high-level policy không nên phụ thuộc vào code hiện thực low-level detail. Ngược lại mới đúng: detail phải phụ thuộc policy.

DIP · Clean Architecture

Chỉ tránh concretion volatile

Coi mọi thứ cụ thể là cấm kỵ thì phi thực tế: String trong Java cũng cụ thể nhưng cực kỳ stable — không cần trừu tượng hóa. Thứ cần tránh là các volatile concrete element: những module đang phát triển tích cực, thay đổi liên tục.

Stable abstractions: mỗi thay đổi của interface kéo theo thay đổi ở implementation, nhưng đổi implementation thường không đụng tới interface. Vậy nên interface luôn ít biến động hơn — kiến trúc sư giỏi cố thêm tính năng vào implementation mà không sửa interface.

Phụ thuộc concretion volatile

  • Tham chiếu thẳng lớp cụ thể hay thay đổi.
  • Kế thừa từ lớp cụ thể volatile (quan hệ source cứng nhất).
  • Override hàm cụ thể — bạn không bỏ được phụ thuộc của nó, mà thừa kế luôn chúng.

Bốn quy tắc DIP

  • Trỏ tới abstract interface, không tới lớp cụ thể volatile.
  • Đừng kế thừa từ lớp cụ thể volatile.
  • Thay vì override: làm hàm abstract rồi tạo nhiều implementation.
  • Không bao giờ nhắc tên thứ vừa cụ thể vừa volatile.

Tạo đối tượng cụ thể bằng Abstract Factory

Khởi tạo một concrete object lại buộc có source dependency tới định nghĩa cụ thể của nó. Lối thoát: Abstract Factory. Application gọi makeSvc() trên interface ServiceFactory; phần ServiceFactoryImpl mới thật sự new ConcreteImpl rồi trả về dưới dạng Service trừu tượng — policy không bao giờ phải gọi tên lớp chi tiết.

Ranh giới kiến trúc & sự đảo chiều

Đây là ý cốt lõi nối sang phần Kiến trúc: interface trừu tượng vẽ nên một đường ranh giới kiến trúc. Mọi source dependency cắt qua ranh giới chỉ theo một hướng — về phía abstract. Nhưng flow of control lại đi ngược lại. Source dependency bị đảo so với luồng điều khiển — đó là lý do gọi là Dependency Inversion.

ranh giới kiến trúc Abstract · policy Concrete · detail High-level Policy «interface» ConcreteImpl source dependency → flow of control →
Hai mũi tên ngược chiều nhau: source dependency trỏ về vùng abstract, còn flow of control chạy sang vùng concrete. Đường cong này về sau chính là architectural boundary — và quy tắc "phụ thuộc luôn hướng về abstract" sẽ thành Dependency Rule.

Không thể xóa sạch vi phạm DIP — luôn cần ai đó new ra lớp cụ thể. Hãy gom các vi phạm ấy vào vài module cấp thấp, thường là module main, tách khỏi phần còn lại của hệ thống.

05 Ba nguyên lý qua lăng kính RAG

Cùng hệ "Hỏi–đáp tài liệu": pipeline Python (FastAPI) lo chunk→embed→retrieve→generate; PHP (Laravel) chỉ chạm ở ranh giới service. Ba nguyên lý soi rõ ba quyết định khác nhau.

DIP · pipeline phụ thuộc abstraction

RAG  Pipeline là high-level policy. Nó không được nhắc tên OpenAI hay FAISS (concretion volatile) — chỉ phụ thuộc interface VectorStore/LLM (stable abstraction). Một Abstract Factory dựng concrete; mọi vi phạm DIP gom về module main:

Python · policy ↔ abstraction, factory dựng concrete
class VectorStore(Protocol):                 # stable abstraction
    def search(self, q: list[float], k: int) -> list[Chunk]: ...

class LLM(Protocol):
    def generate(self, prompt: str) -> str: ...

def answer(q, store: VectorStore, llm: LLM):  # high-level policy, 0 concretion
    ctx = store.search(embed(q), k=5)
    return llm.generate(build(ctx, q))

# module `main` (vùng "bẩn") — nơi DUY NHẤT gọi tên lớp volatile
def make_store() -> VectorStore: return FaissStore(...)   # Abstract Factory
def make_llm()   -> LLM:        return OpenAILLM(...)
Source dependency trỏ vào VectorStore/LLM; flow of control chạy ra FaissStore/OpenAILLM — đảo chiều đúng tinh thần DIP.

ISP · client chỉ cần search()

RAG  Web client (PHP) chỉ truy hồi — đừng bắt nó phụ thuộc cả interface admin-index (upsert, reindex, drop). Tách Retriever khỏi IndexAdmin để đổi pipeline nạp dữ liệu không buộc client redeploy:

PHP · ranh giới service — client chỉ thấy phần mình dùng
interface Retriever {            // U1Ops của RAG: client chỉ cần đúng cái này
    public function search(string $q, int $k): array;
}
// interface IndexAdmin { upsert(); reindex(); drop(); }  ← client KHÔNG phụ thuộc

class AskController {
    public function __construct(private Retriever $rag) {}   // không dính "baggage" admin
    public function ask(string $q): array { return $this->rag->search($q, 5); }
}
"Đừng phụ thuộc vào thứ bạn không cần": đổi logic reindex phía Python không lan tới web layer.

LSP · mọi VectorStore thay thế được nhau

RAG  FaissStorePgVectorStore phải thật sự substitutable: cùng nhận vector, trả top-k cùng ngữ nghĩa. Nếu một store lén đổi nghĩa tham số k (vd "ngưỡng điểm" thay vì "số kết quả"), pipeline buộc phải có nhánh if isinstance(...) — chính là vết rò Square/Rectangle.

Substitutable

  • store = FaissStore() hay PgVectorStore()answer() không đổi một dòng.
  • Hợp đồng search() giữ nguyên ngữ nghĩa.

Vi phạm LSP

  • Một store đổi nghĩa k hoặc ném lỗi khác kiểu.
  • Pipeline phải thêm if theo loại store ⟶ kiến trúc nhiễm.

06 Ghi nhớ nhanh

LSP: subtype phải thế chỗ base mà chương trình vẫn đúng. Square/Rectangle vi phạm vì phá hợp đồng; vi phạm rò lên kiến trúc thành if/else đặc biệt.

ISP: đừng phụ thuộc vào thứ bạn không dùng. Tách interface béo; phụ thuộc module mang baggage thừa gây recompile/redeploy oan và lỗi dây chuyền.

DIP: phụ thuộc vào abstraction, tránh volatile concretion. Dùng Abstract Factory để dựng concrete; ưu tiên stable abstractions.

Đảo chiều: qua ranh giới kiến trúc, source dependency hướng về abstract còn flow of control đi ngược lại. Đây sẽ là Dependency Rule của mọi sơ đồ về sau.

Một câu chung cho ISP & DIP: chỉ phụ thuộc thứ ổn định & cần thiết; gom phần "bẩn" (vi phạm DIP không tránh được) vào module main.

NguồnChương 9 (Liskov Substitution Principle), 10 (Interface Segregation Principle) & 11 (Dependency Inversion Principle), Clean Architecture — Robert C. Martin, Prentice Hall 2017.