Soldemy
0%

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

Axes của Connascence

10 phút đọc

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

  • Hiểu và giải thích được ba trục (axis) dùng để đo lường và đánh giá Connascence: Strength (Độ mạnh), Locality (Khoảng cách), và Degree (Quy mô).

  • Nhận thức được rằng một dependency không chỉ được đánh giá bằng việc nó "tốt" hay "xấu" một cách tuyệt đối, mà phụ thuộc vào sự kết hợp của cả ba yếu tố trên.

  • Sử dụng ba trục này như một framework tư duy để quyết định xem có nên dành thời gian refactor một đoạn code ngay lập tức hay không.

Mọi hệ thống đều phải có sự phụ thuộc (Connascence) mới có thể hoạt động. Mục tiêu của chúng ta không phải là đưa Connascence về 0. Meilir Page-Jones đã đề xuất ba trục để giúp lập trình viên đánh giá xem một dependency đang ở mức độ an toàn hay nguy hiểm, từ đó đưa ra quyết định thiết kế hợp lý.

1. Strength (Mức độ mạnh / yếu của dependency)

Khái niệm: Strength chỉ ra loại Connascence mà hai thành phần đang mắc phải. Trục này được sắp xếp dựa trên độ khó để phát hiện và độ khó để refactor khi có sự thay đổi. Về Strength, mức độ Connascence được đánh giá từ yếu (tốt) đến mạnh (xấu):

  • Yếu nhất (Tốt nhất): Name (Tên), Type (Kiểu dữ liệu).

  • Trung bình: Meaning (Ý nghĩa giá trị), Position (Vị trí/Thứ tự).

  • Mạnh (Xấu): Algorithm (Thuật toán), Execution (Thứ tự chạy), Timing (Thời gian), Identity (Định danh).

Nguyên tắc thiết kế:

Luôn hướng tới việc hạ cấp (downgrade) Strength của Connascence. Đẩy các Connascence Dynamic thành Static, và đẩy các Connascence Static từ mạnh (Algorithm, Position) xuống yếu (Type, Name).

Ví dụ: Việc đổi từ truyền ba tham số (string, string, string) (Connascence of Position - trung bình) sang truyền một Object { name: string, email: string, phone: string } (Connascence of Name - yếu) chính là một bước tiến về Strength.

2. Locality (Khoảng cách giữa các thành phần liên đới)

Khái niệm: Locality đánh giá xem hai thành phần đang phụ thuộc vào nhau nằm cách xa nhau bao nhiêu về mặt vật lý (kiến trúc mã nguồn).

Khoảng cách này có thể được chia theo các cấp độ (từ gần đến xa):

  1. Nằm trong cùng một hàm (Function).

  2. Nằm trong cùng một class / file.

  3. Nằm trong cùng một module / package.

  4. Nằm ở hai module hoàn toàn khác nhau trong cùng một codebase (Monolith).

  5. Nằm ở hai codebase/microservice hoàn toàn khác nhau, giao tiếp qua network.

Nguyên tắc thiết kế (Quy tắc tỷ lệ nghịch):

Khoảng cách (Locality) càng xa, thì độ mạnh (Strength) của Connascence bắt buộc phải càng yếu.

Ngược lại, nếu khoảng cách rất gần, bạn có thể chấp nhận những Connascence rất mạnh.

Ví dụ:

Nếu bạn viết một hàm private nhỏ phục vụ tính toán ngay trong cùng một class, hàm đó nhận 4 tham số không có tên (Connascence of Position). Điều này hoàn toàn có thể chấp nhận được vì Locality cực kỳ gần. Người đọc chỉ cần cuộn chuột vài dòng là thấy luồng chạy.

Nhưng nếu hai Microservices giao tiếp với nhau mà đòi hỏi phải truyền mảng 4 phần tử không tên qua REST API (cũng là Connascence of Position), đó là một thảm họa kiến trúc, vì Locality quá xa, một bên đổi thứ tự sẽ làm chết hệ thống bên kia mà không có cách nào debug dễ dàng.

3. Degree (Số lượng thành phần bị ảnh hưởng)

Khái niệm: Degree đo lường quy mô của sự phụ thuộc. Nó trả lời câu hỏi: "Có bao nhiêu thực thể (class, function, module) đang dính dáng đến Connascence này?"

Nguyên tắc thiết kế:

Một Connascence dù mạnh (xấu) đến đâu, nhưng nếu Degree của nó nhỏ (chỉ ảnh hưởng tới 2 file), thì nó ít gây hại hơn một Connascence mức trung bình nhưng có Degree khổng lồ (ảnh hưởng tới 500 file).

Ví dụ:

Giả sử hệ thống của bạn sử dụng một Magic String "PENDING" (Connascence of Meaning) để đánh dấu trạng thái đơn hàng.

  • Nếu chuỗi "PENDING" này chỉ được dùng trong 2 file (OrderProcessor và OrderRepository), Degree = 2. Rủi ro ở mức thấp.

  • Nếu chuỗi "PENDING" này xuất hiện ở 50 file rải rác khắp hệ thống (UI, Email Service, PDF Generator, Analytics...), Degree = 50. Lúc này nó trở thành một "bãi mìn". Việc đổi "PENDING" thành "AWAITING_PAYMENT" sẽ khiến toàn bộ dự án sụp đổ.

Giải pháp cho trường hợp Degree cao là tạo ra một ranh giới đóng gói (Encapsulation) hoặc sử dụng Constants/Enums để quy tụ chúng về một điểm duy nhất, làm giảm mức độ phân tán.

4. Dùng ba Axis để đánh giá một dependency cụ thể

Để thấy sức mạnh của việc kết hợp cả 3 trục, hãy cùng phân tích một ví dụ thực tế.

Giả định bạn đang xem xét một hệ thống E-commerce. Backend trả về một chuỗi ID ghép (Composite ID) cho Frontend bằng cách nối userId và orderId theo thuật toán: 1099_8821 (ngăn cách bởi dấu gạch dưới).

Frontend tự động dùng hàm split(“_”) để bóc tách lấy userId và hiển thị.

Phân tích qua 3 Axis:

  1. Strength (Độ mạnh): Cực kỳ cao (Xấu).

Backend và Frontend đang mắc phải Connascence of Algorithm (thuật toán nối và cắt chuỗi bằng dấu “_”) và Connascence of Position (mặc định tin rằng index 0 là userId, index 1 là orderId).

  1. Locality (Khoảng cách): Rất xa (Xấu).

Backend và Frontend nằm ở 2 repository khác nhau, viết bằng 2 ngôn ngữ/framework khác nhau, triển khai ở 2 nơi khác nhau. Khoảng cách vật lý là xa nhất có thể.

  1. Degree (Quy mô): Rất lớn (Xấu)

Giả sử cái Composite ID này được trả về ở 20 API endpoints khác nhau, và Frontend gọi hàm split("_") ở 30 components UI khác nhau.

Kết luận đánh giá:

Dependency này đang thất bại ở cả 3 trục: Quá mạnh, quá xa, và quá nhiều nơi bị ảnh hưởng. Đây là một mã độc kiến trúc cần phải được ưu tiên refactor ngay lập tức. Nếu Backend quyết định đổi format ghép ID (ví dụ: dùng dấu “-” hoặc thêm storeId vào giữa), hệ thống Frontend sẽ gãy toàn tập.

Hành động Refactoring (Dựa trên 3 Axis):

Mục tiêu là hạ cấp Strength (xuống Name/Type).

Backend thay vì trả về một string ngầm định, hãy trả về một DTO rõ ràng:

{
  "id": "1099_8821",
  "userId": 1099,
  "orderId": 8821
}

Lúc này, Frontend chỉ cần đọc thẳng giá trị response.userId.

  • Strength giảm xuống chỉ còn Connascence of Name (cùng phải thống nhất tên field là userId).

  • Dù Locality vẫn xa và Degree vẫn lớn, nhưng vì chúng ta đã hạ cấp Strength xuống mức an toàn nhất (Name), dependency này không còn là rủi ro gây vỡ hệ thống nữa.

Từ phân tích trên, có thể thấy mô hình ba trục mang lại một cơ sở lý luận vững chắc trong phát triển phần mềm. Đây là thước đo khách quan giúp đội ngũ kỹ thuật biện luận một cách logic trước các bên liên quan (stakeholders, quản lý dự án) về những khiếm khuyết kiến trúc tiềm ẩn. Phương pháp này minh chứng rõ ràng lý do vì sao một đoạn mã dù vẫn đáp ứng tốt yêu cầu chức năng trước mắt nhưng lại đang tích lũy khoản nợ kỹ thuật nghiêm trọng, qua đó khẳng định tính thiết yếu của việc tiến hành tái cấu trúc.

Hỏi đáp

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