Soldemy
0%

Coupling, Cohesion và Connascence trong thiết kế phần mềm

Cân bằng Coupling và Cohesion

9 phút đọc

Sau khi hoàn thành chương này, người đọc có khả năng:

  • Xem các nguyên lý thiết kế kinh điển (SOLID, DRY) như những công cụ để điều tiết Coupling và Cohesion, thay vì coi chúng là các quy tắc bắt buộc phải tuân theo.

  • Phân biệt giữa "DRY đúng nghĩa" (loại bỏ sự trùng lặp của logic nghiệp vụ) với "DRY giả hiệu" (vô tình tạo ra Coupling không cần thiết).

  • Vận dụng YAGNI và KISS như cơ chế kiểm soát, ngăn thiết kế rơi vào tình trạng over-engineering.

1. SOLID

SOLID không phải là một tập hợp quy tắc xuất hiện ngẫu nhiên. Trên thực tế, năm nguyên lý này là các chỉ dẫn cụ thể nhằm đạt được trạng thái "High Cohesion, Low Coupling" trong lập trình hướng đối tượng.

  • S - Single Responsibility Principle (SRP): Thể hiện trực tiếp khái niệm Functional Cohesion. Việc một class chỉ có duy nhất một lý do để thay đổi đồng nghĩa với việc các thành phần bên trong nó cùng hướng tới một mục tiêu chung.

  • O - Open/Closed Principle (OCP): Hướng đến mục tiêu cao nhất của Low Coupling. Tính năng mới có thể được bổ sung (mở rộng) mà không cần chỉnh sửa code hiện có (đóng lại với sửa đổi), điều này có nghĩa là module cũ không bị ràng buộc chặt với các yêu cầu phát sinh sau này.

  • L - Liskov Substitution Principle (LSP): Bảo đảm tính an toàn cho Message/Interface Coupling. Khi module A phụ thuộc vào interface B, mọi class hiện thực B đều cần hoạt động chính xác mà A không phải nắm rõ chi tiết bên trong.

  • I - Interface Segregation Principle (ISP): Có chức năng loại bỏ Stamp Coupling (sự phụ thuộc vào cấu trúc dữ liệu thừa thãi). Thay vì buộc client phải phụ thuộc vào một interface cồng kềnh chứa nhiều method không sử dụng, nguyên lý này khuyến khích chia nhỏ interface.

  • D - Dependency Inversion Principle (DIP): Biến đổi Content/Control Coupling thành Message Coupling. Các module ở tầng cao không truy cập trực tiếp các chi tiết triển khai (concrete classes) mà dựa vào các Abstractions.

2. DRY

DRY (Don't Repeat Yourself) có thể là nguyên lý bị áp dụng sai nhiều nhất và hệ quả là một dạng Coupling gây hại False Coupling (Coupling giả tạo).

Xét ví dụ một hệ thống lưu trữ hồ sơ Người dùng (User) và thông tin Đơn vị thuê bao (Tenant), trong đó cả hai đều chứa các trường Tên, Địa chỉ, Email. Một lập trình viên áp dụng DRY một cách máy móc có thể dựng lên một class ProfileManager dùng chung để xử lý validation cho cả hai đối tượng.

TypeScript
// DRY sai lầm: Gom chung vì code giống nhau, dẫn đến Control Coupling
function validateProfile(entity: any, type: "user" | "tenant") {
  if (!entity.email) throw new Error("Email required");
  if (type === "tenant") {
    if (!entity.taxCode) throw new Error("Tax code required for tenant");
  } else {
    if (!entity.dob) throw new Error("DOB required for user");
  }
}

Điểm mấu chốt là: sự trùng lặp về hình thức mã nguồn (nhìn giống nhau) không đồng nghĩa với trùng lặp về bản chất nghiệp vụ (cùng một lý do tồn tại). User và Tenant thực chất sẽ phát triển theo hai hướng riêng biệt. Việc gộp chung chúng vào một hàm chỉ vì vài dòng code tương tự nhau vô tình tạo ra mối liên kết giữa hai Domain vốn không liên quan, đồng thời làm suy giảm Cohesion (do hàm này gánh cả logic của User lẫn Tenant).

Nguyên tắc: Chỉ nên áp dụng DRY khi hai đoạn code cùng chia sẻ một lý do thay đổi (reason to change). Nếu sự tương đồng chỉ là ngẫu nhiên, tốt hơn là chấp nhận để chúng tồn tại độc lập (Duplication is cheaper than the wrong abstraction).

3. YAGNI

YAGNI (You Aren't Gonna Need It) khuyến nghị không nên xây dựng những tính năng hay lớp abstraction chưa thực sự cần thiết ở thời điểm hiện tại.

Khi tiếp cận Design Patterns, người viết code rất dễ có xu hướng tạo ra abstract class, generic repository, hoặc nhiều tầng interface cho những module còn đơn giản.

Mỗi interface được sinh thêm đồng nghĩa với việc "indirection cost" (chi phí truy cập gián tiếp) tăng lên.

Việc tạo interface quá sớm khi mới chỉ có một class hiện thực duy nhất không thực sự giúp giảm Coupling, mà chỉ dịch chuyển sự phụ thuộc từ "phụ thuộc class" sang "phụ thuộc interface", đi kèm với chi phí phức tạp gia tăng.

YAGNI có vai trò giữ Coupling ở mức phù hợp với thực tế: Dependency Inversion hay các Design Pattern chỉ nên được áp dụng khi rủi ro thay đổi (change volatility) là điều có căn cứ và đang thực sự diễn ra.

4. KISS và Accidental Complexity

KISS (Keep It Simple, Stupid) nhấn mạnh rằng thiết kế phần mềm cần ưu tiên tính rõ ràng. Bất kỳ nỗ lực nào nhằm nâng cao Cohesion và hạ thấp Coupling đều phải đánh đổi bằng "Accidental Complexity" sự phức tạp phát sinh từ công nghệ hay kiến trúc, chứ không xuất phát từ bản chất nghiệp vụ.

Chẳng hạn, việc sử dụng Data Coupling (truyền một object đơn giản) rõ ràng ưu việt hơn Control Coupling (truyền một flag điều khiển). Tuy nhiên, triển khai toàn bộ kiến trúc CQRS (Command Query Responsibility Segregation) chỉ để xử lý một form cập nhật tên người dùng lại là một sự vi phạm nguyên lý KISS. Khi hệ thống bị đẩy vào mức độ phức tạp quá mức, mọi lợi ích mà Low Coupling mang lại sẽ bị triệt tiêu, bởi lúc này không thành viên nào trong team còn nắm được cách hệ thống thực sự vận hành.

5. Thứ tự ưu tiên theo context

Trên thực tế, xung đột giữa các nguyên lý là điều khó tránh khỏi: SOLID thúc đẩy việc bổ sung thêm abstraction, trong khi YAGNI và KISS lại đòi hỏi sự tối giản. Dưới đây là một hướng tiếp cận mang tính thực dụng:

  • Xuất phát từ YAGNI và KISS: Viết code trực diện, đơn giản, tập trung giải quyết đúng vấn đề đặt ra. Ở giai đoạn đầu, có thể chấp nhận mức Coupling cao hơn nếu module đó chỉ hoạt động trong một phạm vi hẹp.

  • Ưu tiên đánh giá Cohesion trước (SRP): Khi module dần phình to, không nên vội vàng tách rời (decouple). Thay vào đó, hãy nhóm các phương thức có chung trạng thái lại với nhau, sao cho mỗi class phản ánh đúng một thực thể nghiệp vụ.

  • Tách biệt ranh giới (áp dụng DRY hợp lý): Xác định những phần logic trùng lặp do xuất phát từ cùng một nguyên nhân nghiệp vụ, rồi tách chúng thành các component riêng biệt.

  • Áp dụng Dependency Inversion (SOLID) tại những điểm dễ biến động: Cuối cùng, chỉ nên xây dựng Interface/Abstraction (nhằm giảm Coupling) tại những vị trí thường xuyên có khả năng thay đổi như kết nối Database, cấu hình Third-party API, hay quy trình thanh toán hoặc những nơi cần thiết lập Unit Test giả lập. Hợp đồng chặt chẽ (hard contract) chỉ thực sự cần thiết ở ranh giới giữa các module cốt lõi.

Hỏi đáp

Đăng nhập để đặt câu hỏi và tham gia thảo luận.