Mô-đun phần mềm — Độ gắn kết, Độ ghép nối, Fan-in, Fan-out
1. Tổng quan
A. Định nghĩa
Mô-đun (Module) là đơn vị chức năng của phần mềm có thể biên dịch và tái sử dụng độc lập, và hai trục đánh giá định lượng, định tính chất lượng thiết kế của nó là độ gắn kết (Cohesion, mức liên quan giữa các phần tử bên trong mô-đun) và độ ghép nối (Coupling, mức phụ thuộc giữa các mô-đun). Thiết kế tốt không có ngoại lệ đều hướng tới 'độ gắn kết cao (Strong Cohesion), độ ghép nối thấp (Loose Coupling)'.
Lý do thiết kế mô-đun là nền tảng của chất lượng phần mềm là vì nó quyết định 'phạm vi ảnh hưởng mà thay đổi của một mô-đun lan ra toàn hệ thống (Ripple Effect)'. Chi phí sửa để dùng tiếp của phần mềm lớn hơn nhiều so với chi phí xây dựng (60~80% tổng chi phí vòng đời là bảo trì), và phần lớn chi phí bảo trì phụ thuộc vào 'một thay đổi khiến phải động đến cùng lúc bao nhiêu mô-đun'. Nếu mô-đun được chia tốt, mỗi mô-đun độc lập nên việc hiểu, sửa, tái sử dụng, kiểm thử đơn vị kết thúc cục bộ; nếu chia sai, một thay đổi yêu cầu nhỏ cũng kéo theo sửa đổi dây chuyền và lỗi hồi quy (Regression). Hai thước đo để phán định 'chia tốt hay xấu' này chính là độ gắn kết và độ ghép nối.
Hai thước đo không phải là khái niệm độc lập mà liên động như hai mặt của đồng xu. Nếu gom đúng các chức năng liên quan vào một mô-đun (gắn kết↑), mô-đun đó tự hoàn chỉnh nên ít tham chiếu mô-đun khác (ghép nối↓) và tính độc lập tự nhiên tăng lên. Ngược lại, nếu nhồi các chức năng tạp nham khác tính chất vào một mô-đun (gắn kết↓), mỗi chức năng đó đều kéo dữ liệu, trạng thái từ bên ngoài vào dùng nên tham chiếu vươn ra khắp nơi (ghép nối↑) và mô-đun bị cuốn chặt vào hệ thống. Tức là hành động nâng độ gắn kết thường cũng chính là hành động giảm độ ghép nối. Đây là lý do trong thiết kế có cấu trúc (Structured Design), Constantine (L. Constantine) và Yourdon (E. Yourdon) đưa ra hai khái niệm như một cặp.
B. Bối cảnh ra đời và sự cần thiết
Trước khi phương pháp luận có cấu trúc xuất hiện vào thập niên 1970, chương trình được viết thành một luồng khổng lồ (thủ tục nguyên khối), và 'mã spaghetti' — không thể dự đoán sửa chỗ này sẽ làm hỏng chỗ nào — là phổ biến. Để khắc phục, Constantine và Yourdon phân rã (Decomposition) chương trình thành đơn vị chức năng, đồng thời đề xuất độ gắn kết và độ ghép nối làm thước đo đánh giá khách quan chất lượng phân rã. Phần mềm càng lớn và được duy trì càng lâu thì tần suất thay đổi càng cao, và hai thước đo này giống như định luật vật lý của thiết kế để tạo ra 'cấu trúc bền vững trước thay đổi'. Ngày nay, SRP (nguyên tắc đơn trách nhiệm) của hướng đối tượng, việc xác lập ranh giới dịch vụ của vi dịch vụ, che giấu thông tin và đóng gói rốt cuộc đều là cách nói lại 'gắn kết cao, ghép nối thấp' bằng ngôn ngữ khác.
2. Hai trục của tính độc lập mô-đun — Độ gắn kết và độ ghép nối
Gộp mức tốt xấu của mô-đun thành một khái niệm thì được tính độc lập mô-đun (Module Independence), và đó là hàm của độ gắn kết và độ ghép nối. Hình dưới thể hiện trạng thái lý tưởng — hai mô-đun mỗi bên gắn chặt bên trong (gắn kết cao), và chỉ trao đổi với nhau lượng dữ liệu tối thiểu (ghép nối thấp).
flowchart LR
subgraph A["Mô-đun A (gắn kết chức năng)"]
a1["Phần tử 1"] --- a2["Phần tử 2"] --- a3["Phần tử 3"]
end
subgraph B["Mô-đun B (gắn kết chức năng)"]
b1["Phần tử 1"] --- b2["Phần tử 2"]
end
A -.->|"Ghép nối dữ liệu (chỉ dữ liệu cần thiết)"| B
style A fill:#e8f0fe,stroke:#2f6fed,stroke-width:2px
style B fill:#e8f0fe,stroke:#2f6fed,stroke-width:2px
A. Độ gắn kết — "Một mô-đun chỉ làm tốt một việc"
Độ gắn kết thể hiện "các phần tử (câu lệnh, hàm, dữ liệu) bên trong một mô-đun gắn với nhau vì một mục đích duy nhất đến mức nào". Độ gắn kết cao nghĩa là có thể giải thích mô-đun đó 'làm gì' trong một câu. Ví dụ, nếu mục đích đơn nhất rõ ràng như calculateAccountBalance() thì là gắn kết chức năng. Ngược lại, nếu trong commonUtil() trộn lẫn log, mã hóa, chuyển đổi ngày tháng, thì ngay cái tên cũng chỉ có nghĩa là 'thứ này thứ kia', nên độ gắn kết thấp.
Độ gắn kết được chia thành 7 mức từ ngẫu nhiên (tệ nhất) đến chức năng (tốt nhất). Điều quan trọng hơn việc thuộc thứ tự là nguyên lý 'vì sao càng lên cao càng tốt'. Càng xuống thấp, căn cứ gắn các phần tử lại với nhau càng yếu, ở mức 'tình cờ nằm cùng một tệp'; càng lên cao, tính tất yếu logic càng mạnh, như 'vì gia công tuần tự cùng một dữ liệu' hoặc 'vì cùng tạo nên một chức năng duy nhất'. Tính tất yếu càng mạnh thì mô-đun đó là một khối có lý do nên không cần chia nhỏ, và lý do thay đổi cũng chỉ có một (khớp chính xác với SRP).
| Độ gắn kết (thấp→cao) | Căn cứ gắn kết | Ví dụ |
|---|---|---|
| Ngẫu nhiên (Coincidental) | Không liên quan gì | Lớp Util gom các hàm tạp nham |
| Logic (Logical) | Chỉ tương tự về tính chất, thực thi chọn bằng cờ | Rẽ nhánh theo type trong process(type) |
| Thời gian (Temporal) | Thực thi cùng thời điểm | Thiết lập log, CSDL, bộ nhớ đệm trong initialize() |
| Thủ tục (Procedural) | Thực thi theo thứ tự định sẵn | Chỉ chung thứ tự, dữ liệu không liên quan |
| Giao tiếp (Communicational) | Dùng cùng dữ liệu | Từ cùng đầu vào tạo ra nhiều kết quả |
| Tuần tự (Sequential) | Đầu ra trước là đầu vào sau | Pipeline phân tích cú pháp→kiểm tra→chuyển đổi |
| Chức năng (Functional, tốt nhất) | Hoàn chỉnh một chức năng duy nhất | calculateInterest() |
B. Độ ghép nối — "Giữa các mô-đun chỉ vướng víu ở mức tối thiểu"
Độ ghép nối là "các mô-đun phụ thuộc lẫn nhau sâu đến mức nào". Ghép nối càng mạnh thì thay đổi bên trong của một bên càng buộc bên kia phải sửa theo. Tệ nhất là ghép nối nội dung (Content Coupling), khi một mô-đun trực tiếp động vào mã bên trong hoặc biến cục bộ của mô-đun khác. Khi đó, ngay lúc bên kia thay đổi hiện thực, bên này sẽ hỏng một cách âm thầm. Tốt nhất là ghép nối dữ liệu (Data Coupling), khi chỉ trao đổi giá trị cần thiết qua tham số. Chỉ cần giữ giao diện (trao đổi cái gì) thì hiện thực bên trong có thể thay đổi tự do, thay đổi được cục bộ hóa.
Độ ghép nối có 6 mức từ mạnh (nội dung) đến yếu (dữ liệu). Đặc biệt, trong thực tế hay gây vấn đề là ghép nối điều khiển (Control Coupling) — truyền cờ chi phối logic bên trong của mô-đun kia (true trong sort(data, true) chỉ thị tăng/giảm dần) — và ghép nối chung (Common Coupling) — nhiều mô-đun dùng chung biến toàn cục khiến không thể truy vết ai đã thay đổi giá trị. Đây là lý do lạm dụng trạng thái toàn cục làm xấu đi việc bảo trì nhiều nhất.
| Độ ghép nối (mạnh→yếu) | Hình thức phụ thuộc | Vấn đề |
|---|---|---|
| Nội dung (Content) | Truy cập trực tiếp bên trong, biến cục bộ của bên kia | Phá vỡ đóng gói, thay đổi là chắc chắn hỏng |
| Chung (Common) | Dùng chung biến toàn cục | Không truy vết được thay đổi, tác dụng phụ lan rộng |
| Ngoài (External) | Dùng chung định dạng, giao thức, thiết bị bên ngoài | Dễ tổn thương theo thay đổi bên ngoài |
| Điều khiển (Control) | Truyền cờ điều khiển | Bên gọi biết nội bộ bên được gọi |
| Tem (Stamp) | Truyền cả cấu trúc (chỉ dùng một phần) | Bị ảnh hưởng khi trường không cần thiết thay đổi |
| Dữ liệu (Data, tốt nhất) | Chỉ truyền giá trị cần thiết qua tham số | Phụ thuộc tối thiểu, thay đổi cục bộ |
Tóm lại một dòng, độ gắn kết theo thứ tự ngẫu nhiên < logic < thời gian < thủ tục < giao tiếp < tuần tự < chức năng (tốt nhất), độ ghép nối theo thứ tự nội dung > chung > ngoài > điều khiển > tem > dữ liệu (tốt nhất), và mục tiêu thiết kế là đẩy độ gắn kết từ dưới lên trên, độ ghép nối từ trái sang phải.
C. Kỹ thuật thực tiễn giảm độ ghép nối
Độ ghép nối không tự giảm mà được giảm bằng các kỹ thuật thiết kế có chủ đích. Thứ nhất là tối thiểu hóa giao diện. Thay vì ghép nối tem truyền cả cấu trúc, chỉ truyền đúng giá trị cần thiết qua tham số sẽ hạ xuống ghép nối dữ liệu. Ví dụ, đổi calculateDiscount(entireOrderObject) thành calculateDiscount(amount, tier) thì dù các trường không liên quan của đối tượng đơn hàng thay đổi, hàm này không bị ảnh hưởng. Thứ hai là loại bỏ cờ điều khiển. Thay vì truyền cờ chi phối rẽ nhánh bên trong như process(data, isAdmin), tách thành processAdmin(data), processRegular(data) (đa hình, mẫu chiến lược) thì bên gọi không cần biết nội bộ bên được gọi, ghép nối điều khiển biến mất.
Thứ ba là loại trừ trạng thái toàn cục. Biến toàn cục mà nhiều mô-đun dùng chung là nguồn gốc của ghép nối chung, nên cục bộ hóa trạng thái hoặc trao đổi qua tiêm phụ thuộc (DI) tường minh để có thể truy vết 'ai đã thay đổi giá trị khi nào'. Thứ tư là che giấu thông tin (Information Hiding). Giấu cấu trúc dữ liệu và thuật toán bên trong mô-đun và chỉ giao tiếp qua giao diện công khai, thì dù hiện thực bên trong thay đổi, chừng nào giao diện được giữ, các mô-đun khác không bị ảnh hưởng, ghép nối yếu đi về căn bản. Bốn kỹ thuật này đều hội tụ về một nguyên lý: 'giảm những gì một mô-đun biết về nội bộ của mô-đun khác'.
3. Fan-in và Fan-out — Đo khả năng tái sử dụng và độ phức tạp của cấu trúc
Nếu độ gắn kết, độ ghép nối nhìn 'chất lượng của một mô-đun', thì fan-in, fan-out chẩn đoán hình dạng cấu trúc tổng thể (Structure Chart) qua quan hệ gọi giữa các mô-đun. Fan-in là số mô-đun cấp trên gọi một mô-đun cụ thể (= được tái sử dụng rộng rãi đến mức nào), còn Fan-out là số mô-đun cấp dưới mà một mô-đun cụ thể gọi (= phụ thuộc vào bao nhiêu thứ).
flowchart TB
U1["Mô-đun trên 1"] --> M["Mô-đun M"]
U2["Mô-đun trên 2"] --> M
U3["Mô-đun trên 3"] --> M
M --> D1["Mô-đun dưới 1"]
M --> D2["Mô-đun dưới 2"]
M --> D3["Mô-đun dưới 3"]
style M fill:#e8f0fe,stroke:#2f6fed,stroke-width:2px
Trong hình trên, mô-đun M có Fan-in = 3, Fan-out = 3. Fan-in cao nghĩa là mô-đun đó được nhiều cấp trên dùng chung, là chỉ số khả năng tái sử dụng giúp giảm mã trùng lặp. Các hàm thư viện dùng chung được thiết kế tốt (ví dụ formatDate(), writeLog()) tự nhiên có Fan-in cao. Ngược lại, Fan-out cao nghĩa là mô-đun đó phụ thuộc vào quá nhiều cấp dưới để làm việc của mình, cho thấy trách nhiệm quá mức (tín hiệu vi phạm SRP), độ phức tạp cao và dễ tổn thương trước thay đổi. Do đó, cấu trúc tốt hướng tới Fan-in cao, Fan-out thấp.
| Chỉ số | Ý nghĩa | Hướng mong muốn | Hàm ý thiết kế |
|---|---|---|---|
| Fan-in | Số mô-đun trên gọi tôi | Càng cao càng tốt | Tái sử dụng↑, nhưng bắt buộc quản lý độ ổn định |
| Fan-out | Số mô-đun dưới tôi gọi | Càng thấp càng tốt | Phụ thuộc, độ phức tạp↓, thường khuyến nghị ≤7 |
Tuy nhiên, mô-đun có Fan-in cao là 'điểm yếu chí mạng mà nhiều nơi phụ thuộc', nên có tính chất hai lưỡi: thay đổi sai mô-đun đó thì ảnh hưởng lan rộng. Vì vậy, mô-đun dùng chung có Fan-in càng cao càng phải cố định giao diện ổn định (không thay đổi thường xuyên) và kiểm thử kỹ lưỡng. Ngược lại, mô-đun có Fan-out quá lớn (ví dụ Fan-out từ 10 trở lên) nhiều khả năng là 'mô-đun quá tải trách nhiệm kiểu tháp điều khiển', nên trở thành đối tượng tái cấu trúc bằng cách đặt tầng trung gian để phân tán trách nhiệm (Factoring).
4. Áp dụng qua trường hợp — Từ thiết kế xấu đến thiết kế tốt
Hiểu qua trường hợp cụ thể sẽ rõ ràng. Giả sử mô-đun processOrder() ban đầu của một hệ thống thương mại điện tử thực hiện toàn bộ trừ tồn kho, phê duyệt thanh toán, gửi email, ghi log, cập nhật thống kê trong một hàm. Mô-đun này có năm lý do thay đổi khác nhau (thay đổi chính sách tồn kho, thay cổng thanh toán PG, sửa mẫu email, đổi định dạng log, thêm hạng mục thống kê) nên độ gắn kết thấp (mức logic~thời gian), và mỗi lần đều phải động đến toàn bộ hàm nên rủi ro hồi quy lớn. Thực tế, việc sửa một dòng mẫu email đã dẫn tới sự cố làm hỏng logic thanh toán.
Tách nó thành các đơn vị gắn kết chức năng deductStock(), approvePayment(), sendNotification(), recordHistory(), và đổi để processOrder() cấp trên gọi chúng bằng ghép nối dữ liệu (chỉ truyền dữ liệu đơn hàng cần thiết qua tham số), thì lý do thay đổi của mỗi mô-đun được thu hẹp về một. Khi đó, các chức năng dùng chung như recordHistory(), sendNotification() được nhiều cấp trên (đặt hàng, hoàn tiền, giao hàng) tái sử dụng nên Fan-in tăng lên 3~4, còn Fan-out của processOrder() dừng ở mức 4 có thể quản lý. Kết quả, theo kinh nghiệm của một đội, lỗi hồi quy sau triển khai giảm rõ rệt, việc viết kiểm thử đơn vị có thể thực hiện độc lập theo mô-đun nên dễ nâng độ bao phủ kiểm thử. Như vậy, độ gắn kết, độ ghép nối, fan-in/fan-out không tách rời mà cùng chuyển động theo hướng một phân rã tốt đồng thời cải thiện cả ba chỉ số.
5. Chuyên sâu — Mở rộng sang hướng đối tượng, MSA và đo lường định lượng
Các khái niệm sinh ra từ thiết kế có cấu trúc truyền thống này ngày nay được diễn giải lại trong bối cảnh rộng hơn. Trong hướng đối tượng (OOP), độ gắn kết thể hiện ở đơn trách nhiệm (SRP) của lớp và mức liên quan giữa phương thức và trường, được định lượng bằng các chỉ số như LCOM (Lack of Cohesion of Methods) — nếu các phương thức của một lớp hầu như không dùng trường chung thì LCOM cao, là tín hiệu 'hãy tách ra'. Độ ghép nối được quản lý bằng định luật Demeter (Law of Demeter) và nguyên tắc đảo ngược phụ thuộc (DIP), hạ xuống gần ghép nối dữ liệu bằng giao diện và tiêm phụ thuộc (DI). Các công cụ phân tích tĩnh phổ biến (ví dụ họ SonarQube) tự động đo độ phức tạp chu trình (Cyclomatic Complexity) và chỉ số ghép nối (ví dụ ghép nối vào/ra CE, CA) để sớm lộ ra 'mô-đun xấu'.
Đến kiến trúc vi dịch vụ (MSA), nguyên lý này trở thành chính việc xác lập ranh giới dịch vụ. Bounded Context của thiết kế hướng miền (DDD) là công việc vạch ranh giới 'gắn kết cao', còn nối lỏng các dịch vụ bằng REST/gRPC/hàng đợi thông điệp là hạ độ ghép nối về mức ghép nối dữ liệu. Ngược lại, nếu nhiều dịch vụ trực tiếp tham chiếu một cơ sở dữ liệu dùng chung thì đó là 'ghép nối chung' trong môi trường phân tán, một phản mẫu tiêu biểu (Shared Database) phá vỡ lợi ích của MSA. Tức là 'gắn kết cao, ghép nối thấp' học được ở cấp mô-đun được lặp lại nguyên vẹn ở cấp dịch vụ, chỉ là quy mô lớn hơn.
6. Những điều cần cân nhắc và hàm ý (góc nhìn Kỹ sư chuyên nghiệp)
Gắn kết cao, ghép nối thấp là quy tắc vàng của thiết kế và là tên gọi khác của SRP, che giấu thông tin. Gom chức năng liên quan vào một mô-đun để nâng độ gắn kết thì tham chiếu giảm và độ ghép nối cùng giảm theo. Thay vì cố quản lý riêng hai thước đo, hãy xác lập trước 'mô-đun này làm gì trong một câu' thì cả hai chỉ số cùng được cải thiện.
Phải tận dụng Fan-in/out làm cảnh báo sớm trong chẩn đoán cấu trúc. Với mô-đun có Fan-out quá mức (vượt ngưỡng khuyến nghị 7) thì chia trách nhiệm (Factoring), với mô-đun dùng chung có Fan-in cao thì cố định giao diện ổn định và tăng cường kiểm thử hồi quy để kiểm soát lan truyền thay đổi. Đo định kỳ bằng phân tích tĩnh và sơ đồ cấu trúc để xác định thứ tự ưu tiên tái cấu trúc.
Cần cảnh giác với chia quá nhỏ, có nhận thức về đánh đổi. Nếu chỉ chạy theo độ gắn kết mà chia mô-đun quá vụn, số mô-đun và đường gọi bùng nổ khiến việc hiểu tổng thể lại khó hơn (tăng ghép nối nhận thức) và sinh chi phí hiệu năng. Đặc biệt trong MSA, phân rã dịch vụ quá mức làm tăng giao dịch phân tán, độ trễ mạng và độ phức tạp vận hành. 'Lớn nhất có thể, nhỏ chỉ khi cần' là điểm cân bằng thực tiễn.
Không được quên chỉ số định lượng là phương tiện chứ không phải mục đích. Con số LCOM, độ ghép nối chỉ là tín hiệu chỉ ra "mùi mã xấu"; tái cấu trúc chỉ để khớp con số mà không có ngữ cảnh miền là vô nghĩa. Phán đoán cùng với chiến lược kiến trúc, tần suất thay đổi, cấu trúc tổ chức (định luật Conway), hội tụ về nguyên tắc 'thứ hay thay đổi cùng nhau thì để cùng nhau, thứ thay đổi riêng thì để riêng' — đó là phán đoán thiết kế từ góc nhìn Kỹ sư chuyên nghiệp (Professional Engineer).
Tóm tắt một câu: Thiết kế mô-đun tốt hướng tới độ gắn kết cao (chức năng), độ ghép nối thấp (dữ liệu) và cục bộ hóa ảnh hưởng thay đổi bằng cấu trúc Fan-in cao (tái sử dụng), Fan-out thấp (phụ thuộc tối thiểu), và nguyên lý này xuyên suốt qua SRP, che giấu thông tin đến hướng đối tượng và việc xác lập ranh giới dịch vụ của MSA, chỉ khác về quy mô.