Code Smell là một dấu hiệu bề mặt trong mã nguồn báo hiệu một vấn đề tiềm ẩn trong thiết kế hoặc kiến trúc của ứng dụng. Thuật ngữ này được đặt ra bởi Kent Beck và được Martin Fowler phổ biến trong cuốn sách “Refactoring: Improving the Design of Existing Code”. Theo Martin Fowler, mùi mã nguồn không nhất thiết có nghĩa là lỗi, nhưng hầu như luôn chỉ ra nhu cầu tái cấu trúc để cải thiện khả năng bảo trì.
Những điểm chính
Code Smell là một phép ẩn dụ cho các triệu chứng trong mã nguồn với xác suất cao chỉ ra các vấn đề sâu hơn. Bản thân thuật ngữ này không có định nghĩa chính thức — đó là một heuristic dựa trên kinh nghiệm của các nhà phát triển. Martin Fowler và Kent Beck lần đầu tiên hệ thống hóa 22 mùi vào năm 1999 trong cuốn sách “Refactoring”, và hầu hết chúng vẫn còn phù hợp sau nhiều thập kỷ.
Điều quan trọng là hiểu sự khác biệt giữa Code Smell và lỗi. Mùi không phải là lỗi: mã nguồn biên dịch, chạy và tạo ra kết quả chính xác. Vấn đề là mã nguồn như vậy khó đọc, khó sửa đổi và khó kiểm thử. Theo thời gian, chi phí của mỗi thay đổi tăng lên và niềm tin vào tính đúng đắn của việc tái cấu trúc giảm xuống. Các công cụ phân tích tĩnh (SonarQube, Detekt, SwiftLint) tự động phát hiện nhiều mùi.
Bản chất heuristic của Code Smell có nghĩa là không phải mọi phương thức dài đều cần được chia nhỏ và không phải mọi lớp lớn đều cần tái cấu trúc. Quyết định được đưa ra bởi nhà phát triển dựa trên đánh giá bối cảnh: tần suất thay đổi, mức độ quan trọng của mô-đun, kế hoạch phát triển. Các kỹ sư giàu kinh nghiệm cảm nhận mùi một cách trực quan — mã nguồn “có mùi” mặc dù tất cả các quy tắc chính thức đều được tuân thủ.
Fowler đã xác định 22 mùi được nhóm thành nhiều loại. Đối với phát triển di động, phù hợp nhất là các mùi cấu trúc, mùi thiết kế hướng đối tượng và các vấn đề cụ thể liên quan đến hạn chế của nền tảng. Hãy xem xét từng nhóm với các ví dụ thực tế.
Long Method là mùi phổ biến nhất trong các ứng dụng di động. Một màn hình biểu mẫu đăng ký thường chứa một phương thức setupUI duy nhất dài 200+ dòng, tạo tất cả View, thiết lập ràng buộc, đăng ký sự kiện và xử lý lỗi. Giải pháp: chia thành các phương thức theo khối logic — configureEmailField, configurePasswordField, setupConstraints, bindViewModel.
Large Class — một Activity hoặc ViewController chịu trách nhiệm về hiển thị, điều hướng, logic kinh doanh và tương tác mạng tất cả cùng một lúc. Lớp như vậy vi phạm Nguyên tắc Đơn trách nhiệm và chứa hàng chục trường và phương thức. Trong Android, đây thường là Fragment với 1000+ dòng chứa logic của các màn hình khác nhau. Giải pháp: trích xuất presenter/ViewModel, di chuyển mã mạng vào repository và điều hướng vào coordinator.
Duplicate Code — sao chép các khối giống hệt nhau trong các phần khác nhau của ứng dụng. Một ví dụ điển hình: hai màn hình hiển thị thẻ sản phẩm — trong danh mục và trong mục yêu thích. Nếu logic hiển thị bị sao chép, việc sửa lỗi ở một nơi sẽ không sửa được ở nơi khác. Giải pháp: trích xuất logic chung vào một thành phần hoặc phần mở rộng có thể tái sử dụng.
Feature Envy — một phương thức của lớp này sử dụng nhiều dữ liệu của lớp khác. Trong Android, điều này thể hiện khi ViewModel truy cập trực tiếp vào các trường của mô hình User thay vì gọi một phương thức của mô hình. Dấu hiệu: nếu một phương thức có thể được di chuyển vào lớp mà nó sử dụng dữ liệu — hãy di chuyển nó. Switch Statements (chuỗi điều kiện) — cấu trúc switch hoặc chuỗi if-else kiểm tra loại đối tượng. Thay vào đó, hãy sử dụng đa hình hoặc mẫu strategy.
Data Class — một lớp chỉ lưu trữ dữ liệu nhưng không chứa hành vi. Data class trong Kotlin hay cấu trúc trong Swift không phải là mùi. Vấn đề phát sinh khi logic kinh doanh làm việc với dữ liệu đó bị phân tán khắp cơ sở mã thay vì được đóng gói. Refused Bequest — một lớp con không sử dụng hầu hết các phương thức của lớp cha và ghi đè chúng bằng các stub rỗng. Dấu hiệu của kế thừa sai: thay thế kế thừa bằng hợp thành.
God Activity / God Fragment — một Activity hoặc Fragment biết mọi thứ: vòng đời, dữ liệu, điều hướng, quyền, DI. Đây là lớp tốn kém nhất để bảo trì trong một ứng dụng. Giải pháp: các mẫu kiến trúc MVVM, MVI hoặc Clean Architecture phân tách trách nhiệm. Giant ViewController — tương đương trong iOS, nơi UIViewController chứa toàn bộ logic của màn hình và thường vượt quá 500 dòng.
Hardcoded Resources — chuỗi, màu sắc, kích thước, URL API được nhúng trực tiếp vào mã nguồn. Trong Android, điều này vi phạm hệ thống tài nguyên R; trong iOS, NSLocalizedString và Asset Catalog. Khắc phục: di chuyển tất cả chuỗi vào strings.xml hoặc Localizable.strings, URL vào tệp cấu hình, kích thước vào dimens. Leaking Context — giữ tham chiếu đến Activity hoặc ViewController lâu hơn vòng đời của chính thành phần đó. Dẫn đến rò rỉ bộ nhớ và sự cố. Giải pháp: tham chiếu yếu, Jetpack Lifecycle, RxSwift DisposeBag.
| Mùi | Nơi xuất hiện | Giải pháp |
|---|---|---|
| Long Method | Android/iOS | Extract Method, chia nhỏ |
| Large Class | Activity, ViewController | MVVM, VIPER, Clean Arch |
| Duplicate Code | Bất kỳ màn hình nào | Thành phần chia sẻ, DRY |
| Feature Envy | ViewModel, Presenter | Move Method |
| Leaking Context | Android | Thành phần Lifecycle-aware |
Code review là cách đáng tin cậy nhất để phát hiện mùi. Mắt người nhận thấy các cấu trúc không tự nhiên mà các công cụ phân tích tự động bỏ sót. Hiệu quả của code review được cải thiện khi nhóm sử dụng danh sách kiểm tra các mùi điển hình. Khuyến nghị không xem xét quá 200–400 dòng mã mỗi phiên — sau ngưỡng này, sự chú ý giảm và các mùi bắt đầu bị bỏ sót.
Phân tích tĩnh tự động hóa việc tìm kiếm các mùi cấu trúc. Đối với Android, các công cụ tiêu chuẩn là Detekt (Kotlin) và Android Lint; đối với iOS, SwiftLint và SonarQube. Các công cụ này tìm thấy các phương thức dài, lớp lớn, mã trùng lặp và nhiều vấn đề khác. Điều quan trọng là điều chỉnh các quy tắc cho phù hợp với dự án — cấu hình mặc định thường quá nghiêm ngặt hoặc ngược lại bỏ sót các mùi quan trọng.
Số liệu mã nguồn cung cấp các tiêu chí khách quan: Độ phức tạp Cyclomatic (ngưỡng >10 cần chú ý), Số dòng mã trên mỗi phương thức (ngưỡng >30), Độ sâu kế thừa (>3 — lý do để suy nghĩ). Các công cụ như CodeMetrics (Xcode) và Gradle Metrics Plugin xây dựng biểu đồ thay đổi số liệu theo thời gian. Nếu độ phức tạp của một phương thức tăng từ 5 lên 15 sau lần commit cuối cùng — đó là tín hiệu để tái cấu trúc.
// Ví dụ: phương thức với Độ phức tạp Cyclomatic = 7 (trên ngưỡng 5)
fun processOrder(order: Order) {
if (order.status == Status.NEW) { /* 10 dòng */ }
else if (order.status == Status.PAID) { /* 15 dòng */ }
else if (order.status == Status.SHIPPED) { /* 20 dòng */ }
else if (order.status == Status.DELIVERED) { /* 8 dòng */ }
else if (order.status == Status.CANCELLED) { /* 5 dòng */ }
else { throw IllegalStateException() }
}
// Khắc phục: đa hình thay vì switch
interface OrderHandler {
fun handle(order: Order)
}
Phát hiện mùi tự động không thay thế code review: các công cụ phân tích tĩnh chỉ tìm thấy vấn đề cấu trúc nhưng không nắm bắt được các mùi ngữ nghĩa (Feature Envy, Inappropriate Intimacy). Sự kết hợp giữa công cụ tự động và đánh giá của con người mang lại kết quả tốt nhất. Cấu hình đường ống CI/CD của bạn để bản dựng thất bại khi vượt quá ngưỡng độ phức tạp hoặc độ dài phương thức.
Tái cấu trúc là phương pháp chính để loại bỏ mùi mã nguồn. Fowler mô tả hàng chục kỹ thuật tái cấu trúc, mỗi kỹ thuật áp dụng cho một mùi cụ thể. Extract Method — cho các phương thức dài, Extract Class — cho các lớp lớn, Move Method — cho Feature Envy. Điều quan trọng là thực hiện tái cấu trúc từng bước nhỏ, giữ cho mã nguồn hoạt động sau mỗi thay đổi.
Kiểm thử trước khi tái cấu trúc là bắt buộc. Nếu mã nguồn không được bao phủ bởi kiểm thử đơn vị, tái cấu trúc biến thành viết lại với kết quả không xác định. Đối với mã kế thừa không có kiểm thử, hãy sử dụng Characterization Tests — viết các bài kiểm thử ghi lại hành vi hiện tại, sau đó tái cấu trúc. Kiểm thử mang lại sự tự tin rằng logic kinh doanh không bị hỏng sau khi tái cấu trúc.
Tính từ từ là chìa khóa để khắc phục thành công các mùi trong phát triển di động. Đừng cố gắng viết lại toàn bộ God Activity. Đầu tiên trích xuất lớp điều hướng, sau đó lớp dữ liệu, sau đó logic hiển thị. Mỗi bước nên đi kèm với một commit và chạy kiểm thử. Sử dụng feature toggle để bật tái cấu trúc cho một phần người dùng và quay lại nếu có vấn đề.
Các công cụ IDE tự động hóa nhiều kỹ thuật tái cấu trúc. Android Studio và IntelliJ IDEA cung cấp các tái cấu trúc tích hợp: Extract Method, Extract Interface, Pull Members Up, Encapsulate Fields. Xcode (từ phiên bản 14) đã cải thiện hỗ trợ tái cấu trúc cho Swift. Sử dụng tái cấu trúc tự động giảm nguy cơ lỗi so với sao chép mã thủ công.
Phát triển di động thêm các mùi đặc thù riêng liên quan đến hạn chế của nền tảng. Trong Android, điều này bao gồm rò rỉ Context, Cursor không được đóng và sử dụng Lifecycle không đúng cách. Trong iOS, chu trình giữ tham chiếu qua closure, xử lý Auto Layout không đúng và ViewController khổng lồ. Những mùi này không chỉ làm giảm khả năng bảo trì mà còn ảnh hưởng trực tiếp đến hiệu suất và độ ổn định của ứng dụng.
Callback Hell là một mùi đặc trưng cho mã làm việc với các thao tác bất đồng bộ. Các callback lồng nhau làm cho mã khó đọc và khó gỡ lỗi. Giải pháp: coroutines (Kotlin), async/await (Swift 5.5+), RxJava/RxSwift hoặc Combine. Theo Google I/O 2023, các dự án chuyển từ kiểu callback sang coroutines đã giảm 30% số lượng lỗi và tăng tốc độ thêm tính năng mới.
Platform Coupling — sự gắn kết chặt chẽ của logic kinh doanh với các thành phần nền tảng. Kiểm thử logic như vậy yêu cầu khởi chạy trình giả lập, làm chậm vòng phản hồi. Khắc phục: Clean Architecture phân tách mã thành các lớp Domain (Kotlin/Swift thuần túy không có phụ thuộc nền tảng) và Data/UI (có phụ thuộc nền tảng). Logic kinh doanh được kiểm thử trên JVM mà không cần trình giả lập.
Câu hỏi thường gặp
Không — Code Smell không phải là lỗi. Mã nguồn có mùi hoạt động chính xác, nhưng khó bảo trì, sửa đổi và kiểm thử. Lỗi là hành vi sai; mùi là cảnh báo về các vấn đề tiềm ẩn trong tương lai.
22 mùi trong ấn bản thứ hai của “Refactoring” (2019). Trong số đó có Long Method, Large Class, Primitive Obsession, Data Clumps, Switch Statements, Speculative Generality và những mùi khác. Cộng đồng đã thêm hàng chục mùi mới cho các mô hình và nền tảng hiện đại.
Sự kết hợp cho kết quả tốt nhất: Detekt (Android/Kotlin), SwiftLint (iOS), SonarQube (cả hai) để phân tích tự động và code review cho các mùi ngữ nghĩa. Không có công cụ nào tìm thấy 100% vấn đề — kinh nghiệm con người vẫn là yếu tố quyết định.
Có, nếu mã nguồn hiếm khi thay đổi hoặc sẽ sớm được viết lại hoàn toàn. Tuy nhiên, sự tích tụ của các mùi biến thành nợ kỹ thuật: mỗi thay đổi mới trở nên khó khăn hơn và chi phí khắc phục tăng theo cấp số nhân.
Có — các framework khai báo đã tạo ra những mùi mới: khối @State khổng lồ, xử lý kết xuất lặp lại không đúng cách, tái kết hợp quá mức và thiếu trích xuất thành các View riêng biệt. Đối với SwiftUI, một mùi điển hình là Massive View với hàng chục biến @State.
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