Mã nguồn, giống như entropy, nếu không có tác động bên ngoài sẽ dần xuống cấp và trở nên hỗn loạn. Chúng ta thường nhận ra vấn đề nhưng khó thay đổi hiện trạng. Tuy nhiên, khi cần nâng cấp hoặc viết lại mã, việc đảm bảo chất lượng mã mới trở nên vô cùng quan trọng. Bài viết này tổng hợp các khía cạnh then chốt về code review, dựa trên kinh nghiệm từ nhiều nguồn đáng tin cậy, nhằm giúp nhóm của bạn cải thiện quy trình này.
Tại sao cần Code Review?
Tác hại của việc không review
Chất lượng mã nguồn phản ánh trực tiếp chất lượng sản phẩm. Sản phẩm không ổn định, nhiều lỗi sẽ ảnh hưởng đến sự hài lòng và uy tín của khách hàng. Mã nguồn kém chất lượng cũng làm tăng chi phí bảo trì. Nếu vì lý do nào đó mà mã được viết cẩu thả, không kịp sửa chữa, lại có sự thay đổi nhân sự, hậu quả sẽ rất nghiêm trọng.
Mã khó đọc, không chuẩn mực khiến người tiếp nhận có tâm lý phản kháng, dẫn đến việc sửa chữa một cách tùy tiện, chỉ cần chạy được là được, bất chấp quy tắc. Điều này khiến mã nguồn ngày càng trở nên tồi tệ hơn. Khó có thể tưởng tượng một sản phẩm tồn tại sau 5-10 năm duy trì sự hỗn loạn như vậy. Nợ kỹ thuật có thể gây ra lỗi cục bộ, khó sửa chữa, hoặc thậm chí phải xây dựng lại từ đầu.
Theo quan điểm vật lý, entropy cho thấy mọi thứ đều có xu hướng hỗn loạn nếu không có ngoại lực tác động. Vì vậy, quy tắc và review là vô giá. Trong thiết kế phần mềm, cần chú ý đến sự thay đổi lâu dài. Một mã nguồn mục nát không thể hỗ trợ các thay đổi lâu dài. Làm sản phẩm mà không review thì khó có thể tồn tại lâu dài.
Lợi ích của Code Review
Trong công việc hàng ngày của kỹ sư phần mềm, code review gần như là hoạt động quan trọng nhất nhưng dễ bị bỏ qua nhất. Đây là một hoạt động đầu tư thấp, sinh lợi cao, mang lại nhiều lợi ích cho cá nhân như thói quen, phương pháp và kiến thức. Cụ thể, code review mang lại 6 lợi ích sau:
- Phát hiện sớm lỗi và vấn đề thiết kế: Phát hiện vấn đề càng muộn, chi phí sửa chữa càng cao. Code review giúp phát hiện lỗi sớm, tăng hiệu quả.
- Nâng cao năng lực cá nhân: Nhận góp ý từ đồng nghiệp giúp cải thiện kỹ năng. Chỉ cần biết mã sẽ được review, bạn đã có xu hướng viết tốt hơn.
- Chia sẻ kiến thức trong nhóm: Khi mã được đưa vào kho, nó trở thành tài sản chung. Code review giúp các thành viên khác hiểu về thiết kế, cách triển khai. Các thảo luận cũng trở thành tài liệu tham khảo hữu ích.
- Cải thiện chất lượng ở các lĩnh vực cụ thể: Có thể mời chuyên gia về bảo mật, hiệu suất, UI... để review chuyên sâu. Các mã lõi hoặc rủi ro cao có thể được review tập thể.
- Thống nhất phong cách code: Đây là chức năng phổ biến, nhưng nên tự động hóa bằng công cụ.
- Lợi ích xã hội: Khi biết đồng nghiệp sẽ review, cách bạn lập trình và tư duy sẽ thay đổi hoàn toàn, vì sẽ có phản hồi và đánh giá về mã của bạn.
Khó khăn và Tranh cãi
Hầu hết các tranh cãi không phủ nhận lợi ích của code review, mà tập trung vào những "lý do" hoặc "cái cớ" để không thực hiện.
- Tốn thời gian và công sức: Deadline đã ấn định, thời gian eo hẹp, ai muốn dành thời gian cho review? Đây là rào cản khách quan phổ biến.
- Ảnh hưởng đến tinh thần đồng đội: Các lập trình viên thường cãi nhau vì bất đồng quan điểm trong review.
- Chủ quan: Người review áp đặt sở thích cá nhân, cho rằng code của người khác không đủ tốt.
- Cơ cấu nhân sự không phù hợp: Nhiều người mới, ít người có kinh nghiệm. Review chéo giữa người mới có thể không hiệu quả, trong khi người có kinh nghiệm lại phải review quá nhiều.
- Tâm lý không thích bị review: Một số lập trình viên quá tự tin, cho rằng code của mình hoàn hảo, không cần ai review.
- Hình thức: Nếu làm không tốt, code review chỉ mang tính hình thức, qua loa.
Các Hình thức Code Review
Trước khi triển khai, cần hiểu rõ các hình thức:
- Kiểm tra máy (Machine check): Do công cụ thực hiện.
- Review thủ công (Manual review): Do con người thực hiện.
- Review trực tuyến (Online review): Trên các nền tảng như GitHub, GitLab.
- Review offline qua máy chiếu (Offline projection review): Được khuyến nghị vì hiệu quả cao, dù có vẻ truyền thống nhưng lợi ích lớn hơn chi phí.
Đối tượng cần Review
Cần quy ước review cho các thay đổi sau: thay đổi lớn cần nhiều người review, thay đổi thông thường chỉ cần một đồng nghiệp kiểm tra lại.
Thành viên tham gia Review
Nguyên tắc là những người có liên quan mật thiết đến nghiệp vụ đều nên tham gia, ví dụ: người hướng dẫn của lập trình viên, đồng nghiệp phụ trách bước tiếp theo trong quy trình. Việc này giúp đồng bộ thông tin kịp thời, tránh thiếu sót. Tuy nhiên, chỉ nên mời những người thực sự quan tâm và hiểu vấn đề, vì đa số dễ mất tập trung khi nói về việc họ không hứng thú.
Nội dung cần tập trung khi Review
Cần phân biệt rõ mục đích và trọng tâm của code review:
- Sai lầm thường gặp: Người review áp đặt quan điểm cá nhân. Nếu muốn thống nhất một phong cách, hãy đề xuất đưa vào tài liệu quy tắc của nhóm.
- Mục đích: Tối đa hóa lợi ích của nhóm, giúp code của mọi người trông giống nhau để dễ dàng sửa đổi.
- Trọng tâm nên tập trung:
- Lỗi logic rõ ràng
- Tuân thủ code convention
- Tính dễ đọc và khả năng bảo trì
- Vi phạm nguyên lý thiết kế cơ bản
- Không nên tập trung quá nhiều:
- Mã có chạy được không (do người viết và tester đảm bảo)
- Mã có đáp ứng yêu cầu nghiệp vụ không
- Có bao phủ đủ kịch bản không
Quy trình Review
Một quy trình rõ ràng giúp tránh gián đoạn công việc:
- Xây dựng tài liệu quy tắc: Tránh sở thích cá nhân, tạo cơ sở chung.
- Lên lịch: Ví dụ, review vào thứ Hai hàng tuần. Người gửi cần ước lượng thời gian, kết hợp với thời gian test và release để có lịch phù hợp.
- Bổ sung tài liệu: Người gửi cần cung cấp bối cảnh, nội dung, phạm vi ảnh hưởng. Dựa vào đó, người review sẽ tập trung vào các nguy cơ tiềm ẩn (ví dụ: liên quan đến tiền bạc).
- Tiến hành Review: Dựa trên mức độ nghiêm trọng của vấn đề phát hiện, quyết định có review lại không.
- Hoàn thiện các yếu tố cần thiết: Đảm bảo các yếu tố như monitoring, comment, test đã đầy đủ. Thiếu sót nào cũng có thể bị từ chối.
- Cập nhật quy tắc: Định kỳ xem xét lại các review trước để cải tiến quy tắc chung.
Các bước triển khai Code Review
Để áp dụng thành công, cần đạt được sự đồng thuận trong nhóm, chọn nhóm thí điểm, sau đó chọn công cụ và quy trình phù hợp. Không phải nhóm nào cũng phù hợp, đặc biệt là các nhóm dự án, nhóm "chữa cháy", nhóm nhỏ ưu tiên nghiệp vụ.
- Tính review vào khối lượng công việc: Nếu không dành thời gian, review sẽ thất bại. Cần ước tính khoảng 30-60 phút mỗi ngày cho review, tương đương 1/5 thời gian viết code. Tốc độ review khoảng 150 dòng/giờ. Kết quả review nên là một tiêu chí đánh giá hiệu suất. Cần có phản hồi thường xuyên cho người review về chất lượng của họ. Người mới nên review nhiều hơn để quen, tập trung vào cấu trúc hơn là chi tiết cú pháp.
- Chọn nhóm thí điểm: Giảm rủi ro, thu thập phản hồi tiêu cực để cải tiến trước khi áp dụng đại trà.
- Chọn công cụ: Kết hợp kiểm tra máy và review thủ công. Các công cụ như GitLab, GitHub, Gerrit, Phabricator đều hỗ trợ code review. Quy trình cơ bản: commit code lên nhánh, gửi lên công cụ review, thực hiện kiểm tra máy và thủ công, nếu không đạt thì sửa lại và gửi lại. Một bộ công cụ phổ biến là GitLab, Jenkins và SonarQube.
Các thao tác quan trọng khi Review
- Tăng tính nguyên tử của commit: Mỗi commit nên là một đơn vị thay đổi độc lập, nhỏ gọn. Không nên commit nửa chừng hoặc quá nhiều code trong một lần.
- Cải thiện chất lượng mô tả commit: Mô tả commit nên gồm:
- Tiêu đề: Ngắn gọn, dưới 70 ký tự.
- Mô tả chi tiết: Mục đích, lý do chọn giải pháp, tổng quan về triển khai.
- Kết quả test: Mô tả các test đã thực hiện (thành công, thất bại, hiệu suất...).
- Thông tin liên quan: ID task, link sprint... Có thể dùng template cho commit message.
Nguyên tắc vàng khi Review
- Tôn trọng lẫn nhau: Người gửi code nên tạo điều kiện cho người review (mô tả tốt, test kỹ, commit nhỏ). Người review nên tránh áp đặt tiêu chuẩn chủ quan.
- Thảo luận, không phán xét: Mục đích là trao đổi, không phải đánh giá. Tâm lý thảo luận giúp giảm cái tôi, tăng hiệu quả. Người review nên đưa ra ý kiến, không nhất thiết phải đưa ra giải pháp, và tránh thuyết giáo.
Các vấn đề thường gặp
Thiếu comment và mô tả thay đổi
// Ví dụ về code bị trùng lặp do thiếu comment
public Article GetArticleById(long id) { return null; }
public MaterialPO GetMaterialById(long id) { return null; }
public ArticleVO GetArticleByIdWithoutStatus(long id) { return null; }
Quá tin tưởng vào bên thứ ba
// Sai: Thiếu kiểm tra đầu vào
Boolean SaveError(List<ShareDetail> list) {
if(list.IsEmpty()) return false; // Không đảm bảo phần tử trong list khác null
int id = list.get(0).getId();
return Save(id, list);
}
// Đúng: Kiểm tra kỹ lưỡng và lấy id từ tham số
Boolean SaveRight(int id, List<ShareDetail> list) {
if(id == null || CollectionUtil.hasNull(list)) return false;
return Save(id, list);
}
Phạm vi biến quá rộng
// Sai: Biến được tham chiếu qua nhiều phương thức
public static void main(String[] args) {
ShareDetail detail = new ShareDetail();
varErrorStep1(detail);
}
public static void varErrorStep1(ShareDetail detail) {
detail.setId(111);
varErrorStep2(detail);
}
public static void varErrorStep2(ShareDetail detail) {
detail.setName("hello");
}
// Đúng: Trả về kết quả từ phương thức
public static ShareDetail varRight() {
ShareDetail detail = new ShareDetail();
detail.setId(111);
detail.setName("hello");
return detail;
}
Thiếu kết quả theo từng bước
// Sai: Dùng cờ hiệu cho logic thoát sớm
public void StepError() {
boolean flag = false;
List<Integer> list = new ArrayList<>();
for(int detail : list) {
if(detail > 10) {
flag = true;
}
}
if(flag) return;
}
// Đúng: Thoát sớm, có cấu trúc rõ ràng
public void StepRight() {
// Bước 1: Kiểm tra tham số
List<Integer> list = new ArrayList<>();
for(int detail : list) {
if(detail > 10) {
return; // Thoát ngay, không cần cờ hiệu
}
}
// Bước 2: ...
// Bước 3: ...
}
Vấn đề về log
// Sai: Log không đủ thông tin
public Boolean logError(Integer id, List<ShareDetail> list) {
if(id == null || CollectionUtil.hasNull(list)) return false;
logger.info("logError run");
try {
return shareDao.save(id, list);
} catch(Exception e) {
logger.error("save error"); // Không ghi context
logger.error("save error e: {0}", e.getMessage()); // Ghi không đầy đủ
}
return false;
}
// Đúng: Log đầy đủ, tránh lỗi
public Boolean logError(Integer id, List<ShareDetail> list) {
if(id == null || CollectionUtil.hasNull(list)) return false;
logger.info("logError run");
try {
return shareDao.save(id, list);
} catch(Exception e) {
logger.error("save error id: {}, e: {}, list: {}", id, e, list == null ? null : JSONObject.toJSON(list));
}
return false;
}
Để code review thực sự hiệu quả, cần có quy trình rõ ràng, tập trung vào lợi ích nhóm và đảm bảo mọi người đều nhận được phản hồi tích cực. Nếu có thể, hãy mời tester tham gia để họ hiểu rõ các điểm thay đổi, giúp kiểm soát biên kịch bản test tốt hơn.