Danh sách bài viết

Bài 310: Common Anti-Patterns: Over-Clone, Over-Arc, unwrap Everywhere

Bài 310 của series Rust Cơ Bản — bài cuối Group 38 (Best Practices) tổng hợp 6 anti-pattern phổ biến nhất mà developer chuyển từ Java, Python, JavaScript, Go sang Rust hay mắc phải: .clone() khắp nơi để né borrow checker, bọc Arc<Mutex<T>> dù chỉ chạy single-thread, rải .unwrap() trong production, lạm dụng Box<dyn Trait> ở chỗ generic phù hợp hơn, dựng module tree 5 tầng kiểu Java enterprise, và reinvent Option/Result của stdlib. Mỗi anti-pattern đều có ví dụ before/after side-by-side để bạn nhận diện và refactor được ngay trong codebase của mình.

11/06/2026
10 phút đọc
1 lượt xem
1

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 &mut thay Arc<Mutex>, ? + thiserror thay unwrap, generic <T: Trait> thay dyn Trait, flat module thay nested 5 tầng, dùng Option/Result stdlib thay enum tự định nghĩa.
  • Có checklist self-review trước khi merge PR Rust.
2

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.

3

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).

4

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.

5

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.

6

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.

7

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.

8

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 State hoặc Rc<RefCell<State>>.
  • unwrap everywhere: .unwrap()? + enum error qua thiserror; bật clippy unwrap_used warn.
  • dyn Trait quá mức: Box<dyn Trait> param → impl Trait hoặc <T: Trait>. Giữ dyn cho 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/Maybestd::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.

9

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, reinvent Option/Result.
  • Fix tương ứng: borrow thay clone, &mut/Rc<RefCell> thay Arc<Mutex> khi single-thread, ? + thiserror thay unwrap, generic thay dyn 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ệt clippy::pedantic), cargo machete, cargo bloat giú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.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 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?
  2. 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?
  3. Bạn viết fn render(widgets: Vec<Box<dyn Widget>>) — đây có phải anti-pattern không? Vì sao?
  4. Đồ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?
  5. Project có pub enum AppResult<T> { Ok(T), Err(String) } dùng khắp nơi. Migration sang stdlib Result nên làm theo bước nào để tránh break consumer?
  6. Khi nào .clone()chấp nhận được chứ không phải anti-pattern? Cho 2 ví dụ cụ thể.
Đáp án
  1. fn search(query: &str, db: &HashMap<String, User>) -> Vec<User>. Lý do: &str nhận cả String và 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.
  2. Refactor mỗi callsite về ? + custom error ConfigError bằng thiserror với variant Missing(&'static str), InvalidPort(std::num::ParseIntError). Bật clippy lint unwrap_usedexpect_used ở mức deny hoặc warn trong Cargo.toml hoặc clippy.toml; CI chạy cargo clippy -- -D warnings chặn PR mới thêm unwrap.
  3. Không, đây là use case đúng của dyn Trait. Vec cầ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ùng Box<dyn Trait> ở chỗ generic monomorphization đủ (vd: function param chỉ 1 type cụ thể tại mỗi callsite).
  4. Đề xuất flatten: module sâu quá kiểu Java enterprise. Có thể đổi thành src/user.rs chứa cả struct + handler, hoặc nếu code lớn thì src/user/mod.rs gom handler.rs, service.rs. Bỏ tầng api/v1/handlers — version hóa qua route prefix trong code, không qua thư mục. Tên file get_user_handler.rs cũng redundant, đặt handler.rs gom các handler là đủ.
  5. Bước 1: định nghĩa type AppResult<T> = std::result::Result<T, AppError> với AppError enum thiserror — vẫn dùng tên cũ nhưng bản chất là stdlib Result. Bước 2: tìm callsite dùng AppResult::Ok → đổi Ok(..), AppResult::ErrErr(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.
  6. (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.
11

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.