Sau khi hoàn thành chương này, người đọc có khả năng:
Định nghĩa Cohesion và phân biệt rõ đơn vị phân tích của nó với Coupling. Coupling quan sát từ bên ngoài một thành phần, Cohesion quan sát từ bên trong.
Phân loại các dạng Cohesion phổ biến, từ Coincidental đến Functional, và xác định chúng trong code TypeScript.
Đánh giá một class/module có Cohesion cao hay thấp dựa trên các tín hiệu cụ thể, không chỉ dựa trên cảm giác "class này to quá".
Nhận biết mối liên hệ ban đầu giữa Cohesion thấp và Coupling cao
1. Cohesion là gì?
Cohesion (tính cố kết) đo mức độ mà các phần tử bên trong một thành phần (các thuộc tính, phương thức của một class; các function trong một module) liên quan chặt chẽ với nhau, cùng phục vụ một mục đích hoặc một trách nhiệm duy nhất.
Cũng như Coupling, khái niệm Cohesion bắt nguồn từ công trình Structured Design của Constantine và Yourdon. Ở đó, Cohesion được mô tả là mức độ mà các thành phần bên trong một module "gắn kết" với nhau, càng gắn kết chặt vào một mục đích duy nhất, Cohesion càng cao.
Điểm khác biệt căn bản so với Coupling: Coupling là quan hệ giữa hai thành phần, còn Cohesion là thuộc tính nội tại của một thành phần. Một class có thể có Coupling thấp with mọi class khác (không phụ thuộc ai) nhưng vẫn có Cohesion thấp, nếu bản thân nó gộp nhiều trách nhiệm không liên quan
2. Vì sao Cohesion quan trọng
Cohesion cao mang lại một số lợi ích trực tiếp:
Khả năng định vị: Khi một yêu cầu thay đổi liên quan đến một trách nhiệm cụ thể, nếu Cohesion cao, toàn bộ code liên quan tập trung ở một nơi không phải tìm kiếm rải rác.
Khả năng đặt tên chính xác: Một thành phần chỉ làm một việc có thể được đặt tên phản ánh đúng việc đó. Khi không thể đặt một cái tên rõ ràng, mô tả chính xác cho một class (OrderManager, Utils, Helper, Common), đó thường là dấu hiệu Cohesion thấp.
Khả năng tái sử dụng có ý nghĩa: Một thành phần Cohesion cao có thể được tái sử dụng nguyên vẹn ở context khác, vì nó đóng gói đúng một khái niệm trọn vẹn. Một thành phần Cohesion thấp khi tái sử dụng sẽ kéo theo cả những trách nhiệm không liên quan.
Cần lưu ý: Cohesion cao không tự động dẫn đến Coupling thấp và ngược lại. Mối quan hệ giữa hai khái niệm này phức tạp hơn một phép suy diễn trực tiếp.
3. Phân loại và nhận diện Cohesion
3.1. Các dạng Cohesion
Structured Design phân loại Cohesion theo một thang từ thấp đến cao. Khác với Coupling (thang càng lỏng càng tốt), thang Cohesion có chiều ngược lại,càng cao càng tốt. Dưới đây là các dạng chính, sắp xếp từ thấp đến cao:
Coincidental Cohesion: Các phần tử bên trong một thành phần không có mối liên hệ nghiệp vụ nào với nhau, chỉ tình cờ được gộp chung thường thấy ở các class Utils hoặc Helpers chứa các hàm không liên quan.
class Utils {
formatDate(date: Date): string { /* ... */ }
calculateShipping(order: Order): number { /* ... */ }
hashPassword(password: string): string { /* ... */ }
}
Logical Cohesion: Các phần tử được gộp vì chúng thuộc cùng một phạm trù logic bề ngoài (ví dụ: đều là hàm xử lý input), nhưng thực thi các hành vi khác nhau, thường được chọn qua một flag điều khiển đây cũng là nơi Logical Cohesion thường đi kèm Control Coupling.
class InputHandler {
handle(type: "keyboard" | "mouse" | "touch", event: unknown) {
if (type === "keyboard") { /* ... */ }
else if (type === "mouse") { /* ... */ }
else { /* ... */ }
}
}
Temporal Cohesion. Các phần tử được gộp vì chúng xảy ra tại cùng một thời điểm trong vòng đời hệ thống (ví dụ: mọi thứ cần chạy lúc khởi động ứng dụng), không phải vì chúng liên quan về mặt nghiệp vụ.
function initializeApplication() {
loadConfig();
connectDatabase();
warmUpCache();
registerCronJobs();
}
Procedural Cohesion: Các phần tử được gộp vì chúng theo cùng một trình tự thực thi (thực hiện bước 1 rồi bước 2 rồi bước 3), nhưng bản thân các bước đó xử lý dữ liệu và mục đích khác nhau.
Communicational Cohesion: Các phần tử cùng thao tác trên cùng một tập dữ liệu, dù mục đích của từng thao tác có thể khác nhau. Đây đã là mức Cohesion tương đối tốt trong thực hành.
class OrderReportBuilder {
constructor(private readonly order: Order) {}
getSubtotal(): number { /* dùng this.order */ }
getTaxAmount(): number { /* dùng this.order */ }
getFormattedSummary(): string { /* dùng this.order */ }
}
Sequential Cohesion: Đầu ra của phần tử này là đầu vào trực tiếp của phần tử kế tiếp, tạo thành một pipeline xử lý dữ liệu có thứ tự tự nhiên và cần thiết.
Functional Cohesion: Mọi phần tử bên trong thành phần cùng phục vụ chính xác một mục đích nghiệp vụ duy nhất, không thừa không thiếu. Đây là mức lý tưởng thường được nhắm tới trong thiết kế hướng đối tượng.
class ShippingFeeCalculator {
calculate(order: Order, region: Region): Money {
const base = this.getBaseRate(region);
const weightSurcharge = this.getWeightSurcharge(order);
return base.add(weightSurcharge);
}
private getBaseRate(region: Region): Money { /* ... */ }
private getWeightSurcharge(order: Order): Money { /* ... */ }
}
Ở ShippingFeeCalculator, mọi phương thức, kể cả các phương thức private hỗ trợ đều tồn tại vì một lý do duy nhất: tính phí vận chuyển. Không có phương thức nào có thể bị loại bỏ mà không làm mất khả năng của class, và không có phương thức nào phục vụ một mục đích khác.
3.2. Phân biệt High và Low Cohesion
Thang bảy cấp độ ở trên hữu ích để phân tích học thuật, nhưng trong thực hành, một số tín hiệu trực tiếp hơn thường đủ để đánh giá:
LCOM (Lack of Cohesion of Methods): là một nhóm metric định lượng, đo bằng cách xem các phương thức trong một class có cùng sử dụng chung các thuộc tính hay không. Một class mà các phương thức chia thành nhiều nhóm không chia sẻ thuộc tính nào với nhau là tín hiệu Cohesion thấp, dấu hiệu class nên được tách.
Khả năng đặt tên: nếu không thể đặt tên một class bằng một danh từ cụ thể, đơn nghĩa (mà phải dùng từ chung chung như Manager, Processor, Handler, Service không có định ngữ rõ ràng), đó thường là Cohesion thấp.
Lý do thay đổi (reason to change): nếu một class có nhiều hơn một lý do nghiệp vụ độc lập khiến nó cần được sửa đổi, đó là dấu hiệu của Cohesion thấp. Đây cũng chính là trọng tâm của Single Responsibility Principle (SRP). Một class nên tập trung vào một trách nhiệm nghiệp vụ duy nhất.
Ngoài các tín hiệu ở trên, một cách kiểm tra thực dụng: thử mô tả trách nhiệm của một class bằng một câu, không dùng liên từ “và”, “hoặc”. Nếu không thể mô tả mà không dùng liên từ nối hai trách nhiệm khác nhau ("class này tạo đơn hàng và xuất báo cáo doanh thu"), đó là dấu hiệu khá đáng tin cậy của Cohesion thấp.
4. Trade-offs
Tách quá mức (over-fragmentation). Đẩy Functional Cohesion đến cực đoan, tách mỗi class chỉ còn một phương thức. Điều này có thể khiến việc hiểu một luồng nghiệp vụ hoàn chỉnh đòi hỏi phải nhảy qua rất nhiều class nhỏ. Cohesion cao ở từng đơn vị không đảm bảo hệ thống dễ hiểu ở mức tổng thể.
Chi phí phối hợp (coordination cost): Khi trách nhiệm được phân tách thành các class/service có Cohesion cao, một luồng nghiệp vụ đơn lẻ (ví dụ: “đăng ký tài khoản”) thường cần một thành phần điều phối (orchestrator) để phối hợp và gọi các thành phần đó theo đúng trình tự. Tuy nhiên, bản thân orchestrator cũng cần được thiết kế hợp lý để tránh trở thành một class có Cohesion thấp.
Cohesion phụ thuộc vào context nghiệp vụ: Không có một ranh giới cố định cho việc “một trách nhiệm” nên nằm trong một class hay được tách thành nhiều class. Trong một hệ thống nhỏ, việc đặt sendWelcomeEmail cùng class với register có thể hoàn toàn hợp lý nếu hai hoạt động này luôn thuộc cùng một luồng nghiệp vụ và không có nhu cầu thay đổi độc lập trong tương lai gần. Điểm quan trọng không phải là tách class càng nhỏ càng tốt, mà là xác định ranh giới dựa trên khả năng thay đổi độc lập trong thực tế.
5. Practical Guidelines
Khi một class có nhiều nhóm phương thức không chia sẻ thuộc tính/trạng thái với nhau, đó là tín hiệu cụ thể để tách, không cần chờ đến khi class "quá dài" mới xem xét.
Đặt tên là công cụ chẩn đoán hữu ích và rẻ: nếu không đặt được một cái tên chính xác, đơn nghĩa cho một thành phần, nên coi đó là tín hiệu cần xem lại ranh giới trách nhiệm.
Không nhắm đến Functional Cohesion tuyệt đối cho mọi thành phần trong hệ thống bằng mọi giá, cân nhắc chi phí phối hợp phát sinh khi tách, đặc biệt với các luồng nghiệp vụ có tính "trọn gói" hiếm khi thay đổi độc lập.
Khi tách một class Cohesion thấp thành nhiều class Cohesion cao, luôn dự trù một thành phần điều phối rõ ràng cho các luồng cần phối hợp nhiều class đó, tránh để logic điều phối rơi rớt một cách ngẫu nhiên vào bất kỳ đâu.
6. Summary
Cohesion đo mức độ các phần tử bên trong một thành phần cùng hướng đến một mục đích chung. Đây là thuộc tính nội tại của chính thành phần, khác với Coupling, vốn phản ánh mức độ phụ thuộc giữa các thành phần.
Thang đo Cohesion đi từ Coincidental (thấp nhất, các phần tử gần như không có mối liên hệ) đến Functional (cao nhất, mọi phần tử cùng phục vụ một mục đích duy nhất).
Một số tín hiệu thực tế cho thấy Cohesion thấp: các nhóm phương thức không chia sẻ trạng thái hoặc mục đích chung, khó đặt tên class một cách chính xác, hoặc class có nhiều lý do nghiệp vụ độc lập để thay đổi.
Tăng Cohesion không phải là một chiến thắng miễn phí. Việc tách một thành phần thành nhiều class có thể làm tăng số lượng Coupling và chi phí phối hợp giữa chúng. Vì vậy, không nên tách class một cách cực đoan chỉ để đạt Cohesion cao hơn.
Khi cân nhắc việc tách một class, hãy đặt câu hỏi: Những nơi nào trong codebase sẽ phải phụ thuộc vào nhiều class thay vì chỉ một class như trước? Chi phí phối hợp phát sinh có thực sự xứng đáng với lợi ích từ việc tăng Cohesion hay không? Ranh giới phù hợp luôn phụ thuộc vào context và nhu cầu thay đổi thực tế của hệ thống.