Tính gắn kết (Cohesion) là một thước đo cho thấy các phần tử bên trong một module hay lớp liên quan chặt chẽ với nhau như thế nào. Theo Wikipedia, tính gắn kết cao là đặc điểm của một module được thiết kế tốt, nơi tất cả các phương thức và trường đều làm việc trên một nhiệm vụ duy nhất. Tính gắn kết ảnh hưởng trực tiếp đến khả năng bảo trì mã nguồn và trái ngược với tính ghép nối (coupling) — sự kết nối giữa các module.
Những điểm chính
Tính gắn kết là một thước đo đánh giá mức độ kết nối logic giữa các phương thức, trường và thuộc tính trong một lớp hoặc module. Một module có tính gắn kết cao thực hiện một tác vụ và chỉ chứa những phần tử cần thiết để hoàn thành nó. Module có tính gắn kết thấp cố gắng làm nhiều việc cùng lúc — các phương thức của nó liên quan yếu về mặt ý nghĩa.
Trong bối cảnh lập trình hướng đối tượng, tính gắn kết liên quan chặt chẽ đến Nguyên tắc Đơn trách nhiệm (S). Nếu một lớp có một trách nhiệm rõ ràng, tính gắn kết của nó thường cao. Nếu một lớp xử lý đồng thời UI, logic nghiệp vụ và mạng — tính gắn kết thấp, và lớp đó nên được chia thành nhiều lớp riêng biệt với trách nhiệm hẹp hơn.
Hiểu về tính gắn kết giúp nhà phát triển đưa ra quyết định tái cấu trúc. Khi bạn thấy một phương thức trong lớp không sử dụng bất kỳ trường nào của lớp, đó là dấu hiệu của tính gắn kết thấp. Phương thức đó hoặc bị đặt sai vị trí trong lớp, hoặc lớp được thiết kế kém. Theo đuổi tính gắn kết cao là một nỗ lực liên tục để cải thiện kiến trúc ở mọi cấp độ của mã nguồn.
Trong kỹ thuật phần mềm, bảy cấp độ gắn kết được phân biệt, xếp hạng từ tệ nhất đến tốt nhất. Hiểu thang đo này cho phép bạn đánh giá khách quan chất lượng của một module và xác định hướng tái cấu trúc. Cấp độ càng cao, mã nguồn càng dễ bảo trì và dễ hiểu.
Ngẫu nhiên — cấp độ tệ nhất, nơi các phần tử trong module được nhóm ngẫu nhiên mà không có kết nối logic. Ví dụ: một lớp Utilities chứa các phương thức định dạng ngày tháng, gửi email và tính chiết khấu. Lớp như vậy không thể hiểu được nếu không đọc tất cả các phương thức của nó, và thay đổi một phương thức có thể làm hỏng phương thức khác chỉ vì chúng ở cùng nhau.
Logic — các phần tử thực hiện các tác vụ liên quan logic nhưng khác nhau về bản chất. Một lớp với các phương thức parseJSON, parseXML và parseCSV được kết nối logic bởi chủ đề “phân tích cú pháp,” nhưng mỗi phương thức thực hiện công việc khác nhau về bản chất. Vấn đề: khi thêm định dạng mới (YAML), lớp phình to và giao diện của nó trở nên cồng kềnh.
Thời gian — các phần tử được nhóm theo thời gian thực thi. Lớp AppInitializer thiết lập cơ sở dữ liệu, tải cấu hình và khởi tạo phân tích — tất cả điều này xảy ra khi khởi động ứng dụng, nhưng bản thân các tác vụ không liên quan đến nhau. Tốt hơn nên chia chúng thành các Initializer riêng biệt cho từng lĩnh vực trách nhiệm.
Thủ tục — xảy ra khi các phần tử được kết hợp bởi một chuỗi thực thi. Module “Xử lý đơn hàng” chứa các phương thức validateCart, processPayment và sendConfirmation — mỗi phương thức được gọi theo đúng trình tự sau phương thức trước. Điều này tốt hơn gắn kết ngẫu nhiên hoặc logic, nhưng vẫn chưa lý tưởng: mỗi bước có thể được trích xuất thành một module riêng.
Giao tiếp — các phần tử làm việc với cùng một dữ liệu. Lớp UserService với các phương thức getUser, updateUser và deleteUser được kết hợp bởi thực thể User chung. Điều này tốt hơn đáng kể so với gắn kết thủ tục: lớp có một miền rõ ràng. Hầu hết các lớp Repository trong các dự án di động đều có gắn kết giao tiếp.
Chức năng — cấp độ cao nhất, nơi mọi phần tử trong module tham gia vào việc thực hiện một tác vụ duy nhất. Lớp PasswordValidator với một phương thức validate duy nhất kiểm tra độ dài, sự hiện diện của ký tự và độ phức tạp của mật khẩu là một ví dụ về gắn kết chức năng. Nếu lớp như vậy thay đổi, chỉ là do các quy tắc xác thực mật khẩu đã thay đổi.
Đạt được gắn kết chức năng là mục tiêu chính của tái cấu trúc kiến trúc. Mỗi lớp nên có chính xác một lý do để thay đổi. Trong phát triển di động, gắn kết chức năng đạt được bằng cách trích xuất các Use Cases riêng biệt, View tùy chỉnh, bộ định dạng và bộ xác thực. Mỗi lớp như vậy là một khối xây dựng hoàn chỉnh với một lĩnh vực trách nhiệm rõ ràng.
Gắn kết và ghép nối là hai mặt của cùng một chất lượng. Gắn kết trong module càng cao, ghép nối giữa các module càng thấp. Một hệ thống được thiết kế tốt đồng thời hướng tới gắn kết nội bộ cao và ghép nối bên ngoài lỏng lẻo. Nguyên tắc này được công nhận là nền tảng trong kỹ thuật phần mềm từ những năm 1970.
Mối quan hệ gắn kết-ghép nối có thể được coi như một sự cân bằng. Nếu nhà phát triển hy sinh gắn kết bằng cách kết hợp nhiều tác vụ trong một lớp, các module lân cận có nhiều phụ thuộc hơn — chúng phải truy cập vào lớp quá tải này cho các mục đích khác nhau, làm tăng ghép nối. Ngược lại, việc chia thành các lớp nhỏ, có gắn kết cao làm giảm số điểm tương tác giữa các module.
Trong thực tế, điều này có nghĩa là: khi bạn trích xuất một lớp mới với gắn kết chức năng, bạn đồng thời giải phóng các module khác khỏi việc phải biết chi tiết triển khai của nó. Ví dụ, trích xuất EncryptionManager thành một lớp riêng với gắn kết chức năng cung cấp cho các module khác một giao diện encrypt/decrypt đơn giản mà không cần hiểu chi tiết về thuật toán mã hóa.
// Gắn kết thấp — lớp làm mọi thứ cùng một lúc
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// Gắn kết cao — mỗi lớp giải quyết một tác vụ
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
Ví dụ cho thấy sự khác biệt: UserManager có gắn kết logic — tất cả các phương thức đều về người dùng, nhưng mỗi phương thức thực hiện công việc khác nhau về bản chất. Sau khi tái cấu trúc, mỗi lớp có gắn kết chức năng và ghép nối giảm vì các module khác chỉ phụ thuộc vào lớp chúng cần, không phải toàn bộ UserManager.
LCOM (Thiếu Gắn kết của Phương thức) là thước đo nổi tiếng nhất để đo tính gắn kết của lớp. LCOM đếm số cặp phương thức không chia sẻ trường chung. Giá trị 0 có nghĩa là gắn kết lý tưởng (tất cả phương thức làm việc với cùng trường), trong khi giá trị cao cho thấy gắn kết thấp. LCOM4 (phiên bản cải tiến) tính đến các kết nối bắc cầu qua các phương thức khác.
Trong phát triển Android, các thước đo gắn kết có thể được lấy qua Detekt với quy tắc TooManyFunctions. Các lớp có hàng chục phương thức sử dụng các nhóm trường khác nhau có khả năng gắn kết thấp. Trong iOS, SwiftLint có các quy tắc file_length và function_body_length — các chỉ báo gián tiếp: tệp và phương thức dài thường báo hiệu gắn kết thấp.
Một phương pháp đánh giá thủ công: hãy tự hỏi “Lớp này sẽ thay đổi vì một lý do hay nhiều lý do?” Nếu bạn có thể kể tên nhiều hơn một lý do độc lập — lớp có gắn kết thấp. Một bài kiểm tra thứ hai: “Có thể chia lớp này thành hai lớp độc lập không?” Nếu có — hãy thực hiện. Thường xuyên kiểm tra gắn kết trong quá trình đánh giá mã ngăn ngừa các lớp God và giảm nợ kỹ thuật.
Bước đầu tiên — áp dụng Nguyên tắc Đơn trách nhiệm. Mỗi lớp nên có một trách nhiệm rõ ràng. Nếu một lớp có phương thức không liên quan đến nhiệm vụ chính của nó, hãy trích xuất nó vào một lớp riêng. Kỹ thuật Extract Class hoặc Extract Delegate trong IDE tự động hóa quá trình này. Sau khi trích xuất, hãy kiểm tra xem lớp ban đầu đã trở nên tập trung hơn chưa.
Bước thứ hai — sử dụng mẫu Facade để đơn giản hóa giao diện. Nếu một lớp cung cấp 20 phương thức nhưng khách hàng chỉ sử dụng 3–4, lớp đó có thể có gắn kết thấp — nó cung cấp quá nhiều chức năng đa dạng. Nhóm các phương thức theo chủ đề, trích xuất các lớp riêng cho mỗi nhóm, và biến lớp ban đầu thành một facade hoặc xóa nó.
Bước thứ ba — chú ý đến các nhóm trường. Nếu một lớp có các trường chỉ được sử dụng bởi một tập hợp con các phương thức — đó là chỉ báo của gắn kết thấp. Chia lớp theo nhóm trường. Ví dụ, nếu một lớp chứa các trường userRepository, networkClient và analyticsTracker, nhưng nhóm phương thức đầu tiên chỉ sử dụng userRepository trong khi nhóm thứ hai sử dụng networkClient — đây là hai lớp khác nhau.
Bước thứ tư — tránh tạo các lớp “utility” với các phương thức static tùy ý. Mỗi phương thức static nằm trong lớp Utils hoặc Helpers là ứng cử viên để trích xuất thành một lớp chuyên biệt. FormatUtils.dateToString nên được chuyển sang DateFormatter, và ValidationUtils.isValidEmail sang EmailValidator. Điều này tăng gắn kết của mỗi lớp và làm cho mã nguồn tự tài liệu hóa.
Câu hỏi thường gặp
Hầu như luôn luôn. Gắn kết chức năng làm cho mã nguồn rõ ràng và có thể dự đoán. Tuy nhiên, đưa nó đến cực điểm có thể dẫn đến phân mảnh quá mức: tạo một lớp riêng cho mỗi thao tác, làm cho kiến trúc trở nên quá phức tạp. Sự cân bằng là vài lớp cho mỗi tính năng, mỗi lớp có gắn kết chức năng.
Gắn kết là thước đo nhất quán nội bộ trong một module hoặc lớp. Tính mô-đun là một nguyên tắc kiến trúc nơi ứng dụng được chia thành các module vật lý. Gắn kết cao là mục tiêu khi thiết kế cả lớp riêng lẻ lẫn toàn bộ module.
Detekt cho Android và Xcode Analyzer cho iOS làm nổi bật các lớp có số lượng phương thức hoặc trường đáng ngờ. IntelliJ IDEA và AppCode có trực quan hóa phụ thuộc — bạn có thể xem đồ thị kết nối và phát hiện các lớp có gắn kết thấp. SonarQube tự động tính toán các thước đo LCOM.
Có. Interface với các phương thức connect, disconnect và isConnected có gắn kết cao — tất cả phương thức liên quan đến quản lý kết nối. Interface với connect, parseData và renderUI có gắn kết thấp. Nguyên tắc Phân tách Interface (SOLID) yêu cầu tạo các interface tập trung hẹp với gắn kết cao.
Hãy đặt ba câu hỏi: Mục đích của lớp có thể được mô tả trong một câu không? Tất cả các phương thức có hỗ trợ mục đích này không? Có trường nào trong lớp không được sử dụng bởi một số phương thức không? Nếu câu trả lời cho bất kỳ câu hỏi nào là không — gắn kết thấp và lớp nên được chia tách.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm