Soldemy
0%

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

Connascence tĩnh

8 phút đọc

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

  • Nhận diện 5 dạng Static Connascence (Connascence tĩnh) trong mã nguồn.

  • Hiểu được lý do tại sao Static Connascence lại dễ phát hiện hơn (nhờ trình biên dịch và công cụ phân tích tĩnh).

  • Nắm vững các kỹ thuật refactoring cơ bản để "hạ cấp" từ dạng Static Connascence mạnh xuống dạng yếu hơn, nhằm tăng tính bảo trì cho hệ thống.

Static Connascence bao gồm những phụ thuộc có thể nhìn thấy ngay bằng mắt thường khi đọc code, hoặc được các trình biên dịch (compiler) như TypeScript và các công cụ linter phát hiện trước khi phần mềm thực sự chạy. Mức độ nguy hiểm tăng dần theo thứ tự.

1. Connascence of Name (CoN)

Khái niệm: Hai hay nhiều thành phần phải đồng ý với nhau về tên của một thực thể (tên biến, tên hàm, tên class, tên file).

Ví dụ: Nếu bạn có một class OrderManager và một phương thức createOrder(), bất kỳ nơi nào muốn sử dụng nó đều phải gọi chính xác tên createOrder. Nếu bạn đổi tên nó thành generateOrder(), toàn bộ các nơi gọi nó đều bị lỗi và phải đổi theo.

Đánh giá: Đây là dạng Connascence yếu nhất và tốt nhất. Bạn không thể và cũng không nên tránh né CoN. Nhờ các công cụ IDE hiện đại, việc đổi tên diễn ra an toàn và tức thời trên toàn bộ dự án.

2. Connascence of Type (CoT)

Khái niệm: Hai hay nhiều thành phần phải đồng ý với nhau về kiểu dữ liệu của một thực thể.

Ví dụ:

TypeScript
function calculateDiscount(price: number): number {
  return price * 0.1;
}
// Bắt buộc phải truyền `number`, truyền `string` sẽ lỗi
calculateDiscount(100);

Đánh giá: CoT mạnh hơn CoN một chút, nhưng vẫn là một dạng Connascence tốt. Trước đây, trong các ngôn ngữ dynamic typing như JavaScript thuần, việc vi phạm CoT chỉ bộc lộ ở runtime. Tuy nhiên, sự ra đời của TypeScript đã biến những lỗi này thành tĩnh. Việc dựa vào CoT là cốt lõi của tính an toàn kiểu dữ liệu.

3. Connascence of Meaning (CoM)

Khái niệm: Hai hay nhiều thành phần phải thống nhất với nhau về ý nghĩa (semantic meaning) của một giá trị cụ thể. Đây thường là các Magic Strings hoặc Magic Numbers, nhưng CoM không chỉ giới hạn ở các giá trị này.

Ví dụ Problematic (CoM):

TypeScript
// Component A (Lưu DB)
const userStatus = 2; // Mặc ngầm hiểu 2 là 'Bị khóa'
// Component B (Hiển thị UI)
if (user.status === 2) {
  renderBadge("Banned");
}

Ở đây, giá trị 2 mang một ý nghĩa nghiệp vụ mà cả hai component phải ngầm thống nhất. Nếu hệ thống thay đổi quy ước 2 = "Chờ duyệt", 3 = "Bị khóa" nhưng Component B không được cập nhật, code vẫn có thể chạy bình thường nhưng cho kết quả sai.

Cách hạ cấp (Downgrade) về CoN / CoT:

Đưa giá trị đó vào một hằng số (Constant) hoặc Enum. Lúc này, các thành phần chia sẻ với nhau cái Tên (Name), không phải là cái Ý nghĩa (Meaning) tự suy diễn.

TypeScript
enum UserStatus {
  ACTIVE = 1,
  BANNED = 2,
  PENDING = 3
}
if (user.status === UserStatus.BANNED) { ... }

4. Connascence of Position (CoP)

Khái niệm: Hai hay nhiều thành phần trong mã nguồn bị ràng buộc với nhau bởi vị trí hoặc thứ tự của các giá trị dữ liệu.

Ví dụ Problematic (CoP):

CoP thường gặp nhất ở danh sách tham số của hàm, hoặc khi trả về một mảng thay vì một object.

TypeScript
function registerUser(name: string, email: string, age: number, isAdmin: boolean) { ... }

// Người gọi phải nhớ chính xác thứ tự: name, email, age, isAdmin

registerUser("[email protected]", "Alice", false, 25); // lỗi logic do nhầm vị trí

Dù có TypeScript (báo lỗi việc đưa boolean vào age), nếu hai tham số liền nhau có cùng type (như name và email), trình biên dịch sẽ bất lực.

Cách hạ cấp (Downgrade) về CoN:

Thay vì dựa vào vị trí, hãy dựa vào tên (sử dụng Object argument / DTO).

TypeScript
interface RegisterParams {

  name: string;

  email: string;

  age: number;

  isAdmin: boolean;

}

function registerUser(params: RegisterParams) { ... }

registerUser({

  email: "[email protected]", // Đảo thứ tự thoải mái

  name: "Alice",

  age: 25,

  isAdmin: false

});

5. Connascence of Algorithm (CoA)

Khái niệm: Hai hay nhiều thành phần phải chia sẻ và đồng ý chính xác về một thuật toán xử lý dữ liệu.

Ví dụ Problematic (CoA):

Giả sử hệ thống tạo ra một file cache. Service A ghi file bằng cách kết nối các trường bằng dấu phẩy.

TypeScript
// Service A: Ghi cache
const cacheStr = `${user.id},${user.role},${Date.now()}`;

Service B đọc file cache đó:

TypeScript
// Service B: Đọc cache
const cacheParts = cacheStr.split(',');
const userId = cacheParts[0];
const userRole = cacheParts[1];

Hai Service này bị trói buộc bởi thuật toán phân tách chuỗi bằng dấu phẩy (Comma-separated). Nếu Service A đổi dấu phẩy thành dấu gạch đứng |, Service B sẽ sụp đổ.

Cách hạ cấp:

Đóng gói thuật toán đó vào một module dùng chung (Utility / Shared Library), hoặc sử dụng các chuẩn công nghiệp (như JSON) thay vì tự viết thuật toán riêng.

TypeScript
// Cả hai sử dụng chung chuẩn JSON thay vì tự chế thuật toán
// Service A
const cacheStr = JSON.stringify(user);
// Service B
const parsed = JSON.parse(cacheStr);

6. Summary

Static Connascence là nhóm các dạng phụ thuộc có thể được nhận diện ngay trong quá trình biên dịch hoặc phân tích tĩnh, trước khi chương trình thực sự vận hành. Nhờ đặc tính này, chúng thường được xem là an toàn hơn so với Dynamic Connascence, bởi lẽ trình biên dịch, hệ thống kiểu dữ liệu và các công cụ linter có thể chủ động cảnh báo lập trình viên ngay khi có sai sót, thay vì để lỗi âm thầm tồn tại đến tận môi trường thực thi.

Tuy nhiên, không phải mọi dạng Static Connascence đều mang mức độ rủi ro như nhau. Mức độ nguy hiểm gia tăng dần theo thứ tự từ Connascence of Name, Connascence of Type, Connascence of Meaning, Connascence of Position cho đến Connascence of Algorithm. Càng về sau, sự phụ thuộc giữa các thành phần càng trở nên ngầm định và khó kiểm soát, đồng thời khả năng phát hiện lỗi của công cụ tự động cũng giảm dần.

Nguyên tắc cốt lõi trong thiết kế phần mềm là luôn tìm cách hạ cấp các dạng Connascence mạnh về những dạng yếu hơn. Một giá trị mang ý nghĩa ngầm định nên được chuyển thành hằng số hoặc enum để trở thành Connascence of Name. Một danh sách tham số phụ thuộc vào vị trí nên được thay thế bằng đối tượng có tên trường rõ ràng. Một thuật toán tự phát triển và chia sẻ ngầm giữa các thành phần nên được đóng gói thành module dùng chung hoặc thay thế bằng chuẩn công nghiệp đã được kiểm chứng.

Việc chủ động nhận diện và quản lý Static Connascence không chỉ giúp giảm thiểu lỗi trong quá trình phát triển, mà còn góp phần nâng cao khả năng bảo trì, mở rộng và tái sử dụng mã nguồn về lâu dài. Đây là nền tảng quan trọng để xây dựng một hệ thống phần mềm có kiến trúc bền vững, dễ dàng thích ứng với những thay đổi trong tương lai.

Hỏi đáp

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