Mục lục
- Mục Tiêu Bài Học
- Anti-Pattern 1: Clone Khắp Nơi Để Tránh Borrow Checker
- Anti-Pattern 2: Arc<Mutex<T>> Khi Không Cần Concurrency
- Anti-Pattern 3: unwrap() Trong Production Code
- Anti-Pattern 4: Over-Use of dyn Trait Khi Generic Hợp Hơn
- Anti-Pattern 5: Module Tree Quá Sâu (Java-like 5 Level Nesting)
- Anti-Pattern 6: Reinvent stdlib (Custom Result, Custom Option)
- Fix Mỗi Cái: Bảng Before vs After
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Nhận diện 6 anti-pattern thường xuất hiện trong code Rust của người mới: over-clone, over-Arc, unwrap everywhere, lạm dụng
dyn Trait, module quá sâu, tái phát minh stdlib. - Hiểu vì sao mỗi anti-pattern là dấu hiệu xấu (perf, an toàn, ergonomics, hay maintainability).
- Áp dụng được pattern fix cho từng cái: borrow thay clone,
Rc<RefCell>hoặc plain&mutthayArc<Mutex>,?+ thiserror thay unwrap, generic<T: Trait>thaydyn Trait, flat module thay nested 5 tầng, dùngOption/Resultstdlib thay enum tự định nghĩa. - Có checklist self-review trước khi merge PR Rust.
Anti-Pattern 1: Clone Khắp Nơi Để Tránh Borrow Checker
Triệu chứng: developer mới gặp lỗi "value borrowed after move" hoặc "cannot borrow as mutable" rồi gõ .clone() cho compile. Codebase ngập .clone() không cần thiết, mỗi request copy String/Vec hàng MB, GC pressure giả lập ngay trong Rust.
// BAD: clone mọi nơi để né borrow checker
fn process(users: Vec<User>) -> Vec<String> {
let names = users.clone().into_iter().map(|u| u.name.clone()).collect();
log_count(users.clone().len());
names
}
Fix: dùng reference. Vec đã có sẵn .iter() trả &T, .len() chỉ cần &self.
// GOOD: borrow thay clone
fn process(users: &[User]) -> Vec<String> {
log_count(users.len());
users.iter().map(|u| u.name.clone()).collect()
// chỉ clone name vì cần own trong Vec trả về
}
Quy tắc: chỉ .clone() khi thực sự cần value độc lập (giữ trong struct, gửi sang thread, return ownership). Nếu chỉ đọc, dùng &T. Nếu chỉ đếm/check, dùng &self method.
Anti-Pattern 2: Arc<Mutex<T>> Khi Không Cần Concurrency
Triệu chứng: code single-thread nhưng bọc state trong Arc<Mutex<T>> vì "đọc tutorial thấy người ta dùng". Mỗi lần truy cập phải .lock().unwrap(), có overhead atomic + system call (futex/srwlock), code lủng củng.
// BAD: Arc<Mutex> trong code single-thread
use std::sync::{Arc, Mutex};
fn build_counter() -> Arc<Mutex<i32>> {
let counter = Arc::new(Mutex::new(0));
for _ in 0..10 {
*counter.lock().unwrap() += 1;
}
counter
}
Fix: chỉ dùng Arc<Mutex> khi thật sự share giữa nhiều thread. Single-thread mutation thường chỉ cần &mut T:
// GOOD: plain &mut, không atomic
fn build_counter() -> i32 {
let mut counter = 0;
for _ in 0..10 { counter += 1; }
counter
}
Trường hợp cần shared mutable trong single-thread (graph có nhiều owner đọc/ghi), dùng Rc<RefCell<T>> — không atomic, không khoá kernel, runtime borrow check:
// GOOD single-thread shared mutable
use std::{rc::Rc, cell::RefCell};
let shared = Rc::new(RefCell::new(Vec::new()));
shared.borrow_mut().push(1);
Quy tắc: cây quyết định là "có cần gửi sang thread khác không?" — không → &mut hoặc Rc<RefCell>. Có → Arc<Mutex> (hoặc Arc<RwLock> nếu đa số là read).
Anti-Pattern 3: unwrap() Trong Production Code
Triệu chứng: gặp Result/Option → reflex .unwrap() cho qua. Service production panic vì I/O error, parse error, missing config. Không có log context, không có graceful fallback.
// BAD: unwrap khắp nơi
fn load_user(id: u64) -> User {
let bytes = std::fs::read(format!("users/{id}.json")).unwrap();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
User {
id,
name: json["name"].as_str().unwrap().to_string(),
}
}
Fix: propagate error bằng ?, định nghĩa enum error bằng thiserror (xem Bài 305):
// GOOD: ? + thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserError {
#[error("đọc file user {0}")]
Io(#[from] std::io::Error),
#[error("parse JSON user")]
Parse(#[from] serde_json::Error),
#[error("thiếu field name cho user {0}")]
MissingName(u64),
}
fn load_user(id: u64) -> Result<User, UserError> {
let bytes = std::fs::read(format!("users/{id}.json"))?;
let json: serde_json::Value = serde_json::from_slice(&bytes)?;
let name = json["name"].as_str().ok_or(UserError::MissingName(id))?;
Ok(User { id, name: name.to_string() })
}
Khi nào .unwrap() chấp nhận được: test code, ví dụ docs, prototype, và những invariant chứng minh được luôn đúng (vd: regex literal hardcode). Production hot path tuyệt đối tránh — bật clippy lint unwrap_used, expect_used ở mức warn để CI bắt.
Anti-Pattern 4: Over-Use of dyn Trait Khi Generic Hợp Hơn
Triệu chứng: developer quen OOP dùng Box<dyn Trait> ở mọi function signature vì "Java polymorphism quen tay". Mất zero-cost abstraction, mỗi method call qua vtable, không inline được.
// BAD: dyn Trait khi monomorphization đủ
trait Logger { fn log(&self, msg: &str); }
fn run_job(logger: Box<dyn Logger>, n: u64) {
for i in 0..n { logger.log(&format!("step {i}")); }
}
Fix: dùng generic — compiler monomorphize, inline call, zero overhead:
// GOOD: generic, static dispatch
fn run_job<L: Logger>(logger: &L, n: u64) {
for i in 0..n { logger.log(&format!("step {i}")); }
}
// Hoặc impl Trait syntax: fn run_job(logger: &impl Logger, n: u64)
Khi nào dyn Trait đúng: cần lưu collection heterogeneous (Vec<Box<dyn Widget>>), plugin system load runtime, giảm code size khi quá nhiều type param dẫn tới binary phồng. Ngoài ra mặc định ưu tiên generic.
Anti-Pattern 5: Module Tree Quá Sâu (Java-like 5 Level Nesting)
Triệu chứng: project Rust có cấu trúc src/domain/users/services/impl/default_user_service.rs 5-6 tầng kiểu com.company.app.module.feature.impl của Java. Đọc code phải nhớ path dài, use statement loằng ngoằng, refactor đau.
# BAD: nested kiểu Java enterprise
src/
domain/
users/
services/
impl/
default_user_service.rs
traits/
user_service.rs
repositories/
impl/
postgres_user_repo.rs
Fix: flatten theo phong cách Rust — module chỉ sâu khi code thật sự cần đóng gói. Stdlib Rust hiếm khi sâu quá 2 tầng (std::collections::HashMap).
# GOOD: flat, feature-oriented
src/
user.rs # struct User + impl methods
user_repo.rs # trait UserRepo + PostgresUserRepo
user_service.rs # business logic
main.rs
Hoặc tổ chức theo feature folder nếu lớn (xem Bài 306): src/user/mod.rs gom repo.rs, service.rs, handler.rs — vẫn flat 2 tầng. Quy tắc: nếu use statement có 4+ dấu ::, hãy nghi ngờ module quá sâu.
Anti-Pattern 6: Reinvent stdlib (Custom Result, Custom Option)
Triệu chứng: developer định nghĩa lại Option, Result, hoặc Either riêng vì "muốn customize message" hay không biết stdlib đã có. Mất khả năng dùng ? operator, mất hàng trăm combinator built-in (map, and_then, or_else, unwrap_or_default...), interop với ecosystem chết.
// BAD: tự định nghĩa Result
pub enum MyResult<T> {
Success(T),
Failure(String),
}
fn divide(a: i32, b: i32) -> MyResult<i32> {
if b == 0 { MyResult::Failure("div by zero".into()) }
else { MyResult::Success(a / b) }
}
Fix: dùng std::result::Result<T, E>. Nếu cần error type riêng, define E bằng thiserror, không tự dựng lại Ok/Err:
// GOOD: stdlib Result + custom E
#[derive(Debug, thiserror::Error)]
pub enum MathError {
#[error("chia cho 0")]
DivByZero,
}
fn divide(a: i32, b: i32) -> Result<i32, MathError> {
if b == 0 { Err(MathError::DivByZero) } else { Ok(a / b) }
}
Tương tự với Option — không bao giờ tạo enum Maybe<T> { Just(T), Nothing }. Stdlib đã ổn định, có hàng trăm combinator, mọi crate ecosystem nhận và trả Option/Result standard.
Ngoại lệ chính đáng duy nhất: type alias để tiện gõ — pub type Result<T> = std::result::Result<T, MyError>. Đây không phải reinvent mà là shortcut.
Fix Mỗi Cái: Bảng Before vs After
Bảng tổng hợp để dán lên monitor khi review code:
- Over-clone:
foo(x.clone())→foo(&x). Chỉ clone khi cần own giá trị mới. - Over-Arc<Mutex>:
Arc<Mutex<State>>single-thread →&mut StatehoặcRc<RefCell<State>>. - unwrap everywhere:
.unwrap()→?+ enum error qua thiserror; bật clippyunwrap_usedwarn. - dyn Trait quá mức:
Box<dyn Trait>param →impl Traithoặc<T: Trait>. Giữdyncho collection heterogeneous. - Module sâu:
a::b::c::d::e::Foo→ flat 2 tầng theo feature, nhìn stdlib làm mẫu. - Reinvent stdlib:
enum MyResult/Maybe→std::result::Result+std::option::Option.
Workflow self-review trước PR: chạy cargo clippy -- -W clippy::pedantic -W clippy::unwrap_used -W clippy::expect_used, đọc warning, sửa từng cái. cargo machete bắt unused dep. cargo bloat --release phát hiện binary phồng do quá nhiều generic monomorphization (dấu hiệu cần đổi sang dyn Trait).
Anti-pattern không phải lỗi compiler — code vẫn build, vẫn chạy. Đó là code smell: dấu hiệu thiết kế chưa đúng tinh thần Rust. Đọc Rust API Guidelines, đọc source stdlib, đọc code các crate hạng A (tokio, serde, reqwest) — sẽ thấy idiom rõ dần.
Tổng Kết
- 6 anti-pattern phổ biến nhất của developer mới sang Rust: over-clone, over-Arc<Mutex>, unwrap everywhere, lạm dụng
dyn Trait, module quá sâu kiểu Java, reinventOption/Result. - Fix tương ứng: borrow thay clone,
&mut/Rc<RefCell>thayArc<Mutex>khi single-thread,?+ thiserror thay unwrap, generic thaydyn Trait, flat module 2 tầng, dùng stdlib types. - Code vẫn compile và chạy được với anti-pattern — đây là code smell chứ không phải bug. Chỉ ảnh hưởng performance, maintainability, hoặc reliability.
- Tool hỗ trợ:
cargo clippy(đặc biệtclippy::pedantic),cargo machete,cargo bloatgiúp bắt nhiều dấu hiệu xấu. - Mental model: học idiom Rust bằng cách đọc stdlib + top-tier crate (
tokio,serde), không bê nguyên pattern từ Java/Python/Go.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Bạn thấy function
fn search(query: String, db: Arc<Mutex<HashMap<String, User>>>) -> Vec<User>. Cả 2 param đều có dấu hiệu anti-pattern. Refactor signature thế nào, biết function chỉ chạy single-thread và không modify db? - Codebase có pattern
config.get("port").unwrap().parse::<u16>().unwrap()ở 30+ chỗ. Fix tổng quát ra sao? Đề xuất tool/lint nào để CI bắt sớm? - Bạn viết
fn render(widgets: Vec<Box<dyn Widget>>)— đây có phải anti-pattern không? Vì sao? - Đồng nghiệp PR thêm file
src/api/v1/handlers/user/get_user_handler.rs. Bạn nên góp ý thế nào? - Project có
pub enum AppResult<T> { Ok(T), Err(String) }dùng khắp nơi. Migration sang stdlibResultnên làm theo bước nào để tránh break consumer? - Khi nào
.clone()là chấp nhận được chứ không phải anti-pattern? Cho 2 ví dụ cụ thể.
Đáp án
fn search(query: &str, db: &HashMap<String, User>) -> Vec<User>. Lý do:&strnhận cảStringvà literal qua deref coercion, không cần own;Arc<Mutex>không cần vì single-thread + read-only — chỉ cần&HashMap. Nếu cần modify, thêm&mut.- Refactor mỗi callsite về
?+ custom errorConfigErrorbằng thiserror với variantMissing(&'static str),InvalidPort(std::num::ParseIntError). Bật clippy lintunwrap_usedvàexpect_usedở mứcdenyhoặcwarntrongCargo.tomlhoặcclippy.toml; CI chạycargo clippy -- -D warningschặn PR mới thêm unwrap. - Không, đây là use case đúng của
dyn Trait.Veccần phần tử cùng size, mà các widget khác type có size khác nhau — buộc phải box và dùng dynamic dispatch. Anti-pattern chỉ là khi dùngBox<dyn Trait>ở chỗ generic monomorphization đủ (vd: function param chỉ 1 type cụ thể tại mỗi callsite). - Đề xuất flatten: module sâu quá kiểu Java enterprise. Có thể đổi thành
src/user.rschứa cả struct + handler, hoặc nếu code lớn thìsrc/user/mod.rsgomhandler.rs,service.rs. Bỏ tầngapi/v1/handlers— version hóa qua route prefix trong code, không qua thư mục. Tên fileget_user_handler.rscũng redundant, đặthandler.rsgom các handler là đủ. - Bước 1: định nghĩa
type AppResult<T> = std::result::Result<T, AppError>vớiAppErrorenum thiserror — vẫn dùng tên cũ nhưng bản chất là stdlib Result. Bước 2: tìm callsite dùngAppResult::Ok→ đổiOk(..),AppResult::Err→Err(AppError::..)dần. Bước 3: enable?operator (không hoạt động với enum cũ). Bước 4: xóa enum cũ. Nếu là public API, deprecate qua#[deprecated]1-2 release rồi mới remove. - (a) Spawn thread/task cần own copy:
let cfg = config.clone(); tokio::spawn(async move { use_cfg(&cfg) });. (b) Lưu vào struct lifetime khác (vd cache theo Vec):self.cached.push(item.clone());. Trong cả 2 case, clone là requirement do ownership semantics, không phải để né borrow checker.
Bài Tiếp Theo
Bài 311: Rust 2024 Edition — Gì Mới — kết thúc Group Best Practices và mở đầu Group cuối: tổng hợp các thay đổi edition 2024 (RPIT capture rule mới, gen blocks, never type fallback), cách migrate dự án 2021 → 2024 bằng cargo fix --edition, khi nào nên upgrade.
