Hôm qua, khi đang fix lỗi của SonarQube, “cánh tay phải” của mình có gặp 1 issue với Cyclomatic Complexity
Bạn thử nhìn xem nó có thân quen chút nào không?!

Minh họa lỗi Cyclomatic Complexity trên SonarQube
Nói thì bảo là khoe, chứ mình chả thấy quen 1 chút nào hết 😀
Cyclomatic Complexity nghĩa là gì?SonarQube đang đếm cái gì mà toàn thấy hiển thị ở mấy câu lệnh if vậy?Tại sao lại là con số 10 mà không phải là số khác?
Cyclomatic Complexity nghĩa là gì?
“Cyclomatic Complexity” dịch Anh-Việt nôm na là “độ phức tạp theo chu kỳ”.
Đang xem: Complexity là gì
Tuy nhiên, trên SonarQube (hoặc với các tool khác) thì bạn sẽ tiếp xúc với từ tiếng Anh nhiều hơn nên trong bài này mình sẽ giữ từ gốc.
Theo Wikipedia, Cyclomatic Complexity là một số liệu (dùng trong nghành phần mềm) được sử dụng để chỉ mức độ phức tạp của một chương trình. Đây là thước đo định lượng số lượng đường dẫn độc lập tuyến tính thông qua mã nguồn của chương trình. Nó được phát triển bởi Thomas J. McCabe, Sr. vào năm 1976. ( ref (1) )
Theo tài liệu của SonarQube, Cyclomatic Complexity được tính dựa trên số lượng đường dẫn thông qua mã nguồn. Bất cứ khi nào luồng điều khiển của hàm chia tách, bộ đếm độ phức tạp sẽ được tăng thêm một. Mỗi hàm có độ phức tạp tối thiểu là 1. Tính toán này thay đổi theo ngôn ngữ vì từ khóa và chức năng thực hiện sẽ có khác nhau. ( ref (2) )

Minh họa luồng điều khiển trong 1 hàm
Dựa theo thông tin trên thì câu hỏi số 2 của mình cũng đã rõ ràng luôn rồi.
SonarQube đang đếm gì mà toàn thấy hiển thị ở câu lệnh if vậy?
iflà câu lệnh rẽ nhánh nên mỗi lần lệnh này được sử dụng bộ đếm độ phức tạp của SonarQube sẽ tăng lên.
Trong lệnh if có n điều kiện thì chỉ số Cyclomatic Complexitycũng + n điểmVới mỗi lệnh if lồng trong 1 lệnh if khác thì chỉ số Cyclomatic Complexitysẽ được tăng thêm (số điều kiện * 2) điểmSonarQube cũng định nghĩa việc tối đa không quá 3 lệnh if lồng nhau. Bạn nên lưu ý việc này.
SonarQube hiển thị thông tin này rất minh bạch & dễ hiểu.
Câu lệnh rẽ nhánh. VD:

Bộ đếm độ phức tạp được thay đổi như thế nào. VD:

C# cung cấp các loại lệnh rẽ nhánh sau ( ref (3) ):
Lệnh | Mô tả |
if |
Một lệnh if bao gồm một biểu thức logic theo sau bởi một hoặc nhiều lệnh khác. |
if … else |
Một lệnh if có thể theo sau bởi một lệnh else (tùy ý: có hoặc không), mà có thể được thực hiện khi biểu thức logic có giá trị false. |
if … else lồng nhau |
Bạn có thể sử dụng lệnh if hoặc lệnh else if bên trong lệnh if hoặc else if khác. |
switch () case |
Lệnh switch case cho phép kiểm tra điều kiện của một biến trước khi thực thi các lệnh |
switch lồng nhau |
Bạn có thể sử dụng một lệnh switch bên trong một lệnh switch khác |
Vậy cách tính Cyclomatic Complexity như thế nào?
Để có thể tính được Cyclomatic Complexity thì cần biết đến nhiều khái niệm liên quan khác nữa. Nên nếu bạn muốn biết chi tiết thì hãy Vote bài này & comment bên dưới để mình có thể biết được mức độ quan tâm của các bạn.
Dựa trên định nghĩa thì chúng ta có thể thấy, khi chỉ số Cyclomatic Complexity của 1 hàm càng cao thì
Mức độ phân nhánh của mã nguồn càng lớn. Dần dần chính tác giả của mã nguồn sẽ mất kiểm soát các luồng của hàm (trong đầu). Những người maintain hàm đó sau này cũng sẽ không dễ gì hình dung được các luồng chạy của hàm.Số lượng unit test case sẽ tăng theo cấp số nhân.
Xem thêm: Đâu Là Sự Khác Biệt Giữa ” If Possible Là Gì, Nghĩa Của Từ Possible
Một hàm có chỉ số Cyclomatic Complexity là 30 sẽ tốt hơn, hay 3 hàm có chỉ số Cyclomatic Complexity là 10 thì tốt hơn?
Nếu bạn là người bảo trì mã nguồn của người khác thì bạn sẽ có câu trả lời: khi có 3 hàm chỉ số Cyclomatic Complexity là 10 thì khả năng cao là chương trình sẽ dễ bảo trì hơn, với sự phân tách hợp lý hơn. Kết quả là, bạn cũng giảm rủi ro làm phát sinh thêm bug.
Ở cấp độ class, bạn có thể theo cùng 1 logic: chỉ số Cyclomatic Complexity cao hơn có thể là dẫn chứng của mức độ tách rời, đóng gói thấp.
Tại sao tiêu chuẩn chỉ số decoupling Cyclomatic Complexity
Mình cũng chưa điều tra ra nguyên nhân sâu xa của giá trị này. Nếu bạn nào có kiến thức về điểm này thì nhờ các bạn chia sẻ ở mục comment để mình có thể học hỏi thêm nhé.
Các trường hợp thường gặp
Trường hợp 1: đối với tác giả của mã nguồn
Chỉ số Cyclomatic Complexity của 1 hàm sẽ tăng lên rất nhanh khi bạn viết quá nhiều chức năng xử lý trong đó.
Theo thời gian 1 hàm có thể đã bao gồm mã nguồn xử lý cho cả 1 business case luôn.
Trường hợp này hay xuất hiện khi:
Dev mới nhận đầu bài; đang liên tục viết code để xử lý cho chạy được. Khi đó, mỗi lần 1 trường hợp nảy ra trong đầu thì Dev sẽ thêm những câu lệnh rẽ nhánh (thường là if) để xử lý vấn đề.Khi đang liên tục code, Dev nghĩ ra được trường hợp cần xử lý. Tuy nhiên, để xử lý trường hợp đó thì chỉ cần khoảng 3 dòng code. Do đó, Dev nghĩ rằng không cần tách hàm ra làm gì cho nó dài dòng. Lâu dần, nhiều trường hợp nhỏ như vậy sẽ khiến chỉ số Cyclomatic Complexity tăng cao.Dev nghĩ ra được trường hợp cần xử lý. Tuy nhiên, không thể nghĩ ra được tên hàm mới cần đặt như thế nào cho phù hợp với Coding Convention của dự án mà vẫn ngắn gọn. Dẫn đến quyết định thường gặp là: thôi không tách hàm nữa cho nhanh.Giải pháp:
Đề xuất đưa ra giải pháp ở mức dự án:
Dự án nên làm theo định hướng Test-driven development.Sử dụng tool SonarQube và CICD pipeline trong dự án để với mỗi commit bạn sẽ nhận được thông tin đánh giá luônSau khi Dev implement xong thì cần thiết có giai đoạn PTL review.
Khi Dev đã xác định được input là gì, output mong muốn là gì, các trường hợp cần đối ứng là gì thì
Các trường hợp cần đối ứng của hàm đã được nhìn nhận từ đầu. Số lượng trường hợp phát sinh trong quá trình làm rất ít. Do đó chỉ số Cyclomatic Complexity có thể est ngay từ đầu để có thể đưa ra quyết định là nên viết 1 hàm hay là tách ra nhiều hàm.Do phải viết unit test case trước nên việc thêm các câu lệnh rẽ nhánh trong quá trình phát triển sẽ khiến Dev thấy số lượng unit test case tăng nhanh. Effort bỏ ra để viết unit test case lớn. Dev sẽ dễ dàng đưa ra quyết định là tách hàm hơn. Bởi vì việc tách hàm sẽ khiến việc viết unit test case dễ dàng hơn nhiều.
Trường hợp 2: đối với người bảo trì mã nguồn của người khác
Khi nhận thông tin về 1 defect, để đảm bảo an toàn (không phá vỡ cấu trúc mã nguồn hiện tại), người bảo trì thường cố gắng thêm các câu lệnh check case by case để fix defect.
Trường hợp này hay xuất hiện khi:
Defect là 1 trường hợp nhỏ, mức độ ảnh hưởng đến các hàm khác cũng nhỏ. Người bảo trì hoàn toàn không có unit test case của mã nguồn cũ. Do đó thêm 1 câu lệnh bắt đúng điều kiện của trường hợp hiện tại để fix là cách làm thường gặp.Dự án nhận làm theo ticket, khi thực hiện est đã làm báo giá rất chặt. Do đó, để đảm bảo thời gian cam kết (VD như xử lý 1 ticket sau 12h), đảm bảo effort bảo trì nằm trong báo giá thì thường dự án cũng sẽ quyết định bổ sung thêm mã nguồn bắt đúng trường hợp bị lỗi vào thay vì đánh giá chỉ số Cyclomatic Complexity hoặc ảnh hưởng đến việc bảo trì tương lai. (tâm lý mặc kệ thằng sau này bảo trì là thằng nào)
Giải pháp:
Bạn cần dùng SonarQube hoặc các tool tương tự khác để đánh giá chỉ số Cyclomatic Complexitytrước khi tiến hành bảo trì.
Xem thêm: Toàn Tập Cách Dùng As Well As Là Gì ? Cấu Trúc Và Một Số Cách Dùng
Nếu chỉ số Cyclomatic Complexitycủa hàm nằm trong giới hạn cho phép Nếu chỉ số Cyclomatic Complexitycủa hàm >= 10 rồi thì nên study đầy đủ hàm đó & tiến hành refactor code cho hợp lý. Có thể tách hàm ra được là việc tốt nhất.
Tạm kết
Trên đây là những hiểu biết của mình vềCyclomatic Complexity và cách xử lý những vấn đề trong quá trình làm việc gặp phải. Hi vọng có ích cho các bạn và mong nhận được ý kiến của các bạn