Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu khác biệt giữa
use(chỉ bring vào scope local) vàpub use(re-export, tức cũng phơi ra ngoài). - Biết viết facade pattern trong
lib.rs— flatten public API cho consumer. - Biết re-export với
asđể đổi tên public, và biết khi nào không nên đổi. - Biết vì sao glob
pub use foo::*;ít được khuyến nghị, và khi nào nó vẫn hữu ích (prelude). - Đọc hiểu cách
axum,tokio,serdetổ chức public surface bằng re-export. - Cảnh giác rằng re-export một type từ internal đồng nghĩa với commit ổn định type đó như public API.
Vấn Đề: API Public Phân Tán
Khi crate lớn dần, code thường được tách thành nhiều module. Cấu trúc nội bộ đẹp với developer, nhưng consumer bên ngoài lại chỉ thấy một loạt path dài và không nhất quán:
// src/lib.rs
pub mod user {
pub mod model {
pub struct User { pub id: u64, pub name: String }
}
}
pub mod error {
pub mod kind {
pub enum Error { NotFound, Invalid }
}
}
Consumer phải viết:
use my_lib::user::model::User;
use my_lib::error::kind::Error;
Path quá dài và phơi cấu trúc nội bộ (user::model, error::kind). Tệ hơn, nếu sau này refactor đổi tên kind thành types, mọi consumer đều break. Đây là bài học đắt giá khi xuất bản crate lên crates.io — cấu trúc thư mục chọn cho lập trình viên đọc source không nên bị ràng buộc với hình dáng API mà người dùng phải gõ ra. Cần một cách để giấu cấu trúc nội bộ mà vẫn phơi các item quan trọng ở path ngắn, ổn định, không phụ thuộc vị trí file vật lý.
pub use Re-export Cơ Bản
use thường chỉ bring path vào scope của module hiện tại — symbol đó là private bên trong module. pub use làm thêm một việc: phơi symbol đó như item public của module hiện tại.
// src/lib.rs
mod user {
pub mod model {
pub struct User { pub id: u64, pub name: String }
}
}
pub use user::model::User; // re-export tại crate root
Bây giờ consumer chỉ cần:
use my_lib::User; // ngắn, ổn định, không lộ user::model
Hai phía cùng có lợi: cấu trúc nội bộ vẫn rõ ràng (mod user; mod user::model), còn consumer thấy một mặt phẳng phẳng và không phụ thuộc vào nó. Nếu mai mốt đổi user::model thành user::entity, chỉ cần sửa duy nhất dòng pub use — consumer không cần đổi gì. Cùng một pub use còn có thể đặt ở bất kỳ module nào, không chỉ crate root: ví dụ re-export một helper từ submodule sâu lên module trung gian để các module cùng tầng dùng thuận tiện mà không cần biết tới đường dẫn đầy đủ.
Facade Pattern Trong lib.rs
Pattern phổ biến nhất: lib.rs đóng vai trò facade — re-export những key type cho consumer, để tất cả API public xuất hiện ở crate root.
// src/lib.rs
mod user;
mod error;
mod db;
pub use crate::user::User;
pub use crate::user::UserRepo;
pub use crate::error::Error;
pub use crate::db::Pool;
Consumer dùng:
use my_lib::{User, UserRepo, Error, Pool};
Lợi ích: (1) một dòng use phẳng cho mọi thứ; (2) maintainer quyết định chính xác những gì là public — item nào không re-export ra lib.rs dù đánh dấu pub trong module con vẫn có thể coi như "semi-public" mà chỉ ai đọc kỹ source mới tìm thấy; (3) đổi cấu trúc nội bộ thoải mái mà không break consumer.
Đây cũng là lý do nhìn vào lib.rs của một crate là cách nhanh nhất để xem "API chính thức" của crate đó. Khi review crate của người khác trên crates.io, mở thẳng src/lib.rs trong tab Source và đọc các dòng pub use ở đầu file — bạn sẽ hình dung được ngay danh sách type/function mà crate phơi ra cho người dùng. Nếu lib.rs chỉ toàn pub mod mà không có dòng pub use nào, đó là dấu hiệu API chưa được tổ chức facade — mọi đường dẫn người dùng gõ sẽ phản chiếu y nguyên cấu trúc thư mục.
Re-export Gom Items Từ Nhiều Module
Có thể re-export các item cùng tên hoặc khác tên từ nhiều submodule khác nhau và gom chúng vào một mặt phẳng duy nhất. Ví dụ một crate domain-driven có nhiều bounded context:
// src/lib.rs
mod orders { pub struct Order { pub id: u64 } }
mod billing { pub struct Invoice { pub id: u64 } }
mod shipping { pub struct Shipment { pub id: u64 } }
pub use orders::Order;
pub use billing::Invoice;
pub use shipping::Shipment;
Consumer chỉ thấy 3 type Order, Invoice, Shipment nằm trực tiếp ở my_lib:: — không phải gõ my_lib::orders::Order. Mặt nội bộ vẫn được chia theo bounded context để code đẹp.
Nhóm theo nested {} cũng được nếu nhiều item cùng module:
pub use orders::{Order, OrderStatus, OrderError};
Re-export Với Alias
Giống use ... as ..., pub use cũng dùng được as để đổi tên public:
mod internal {
pub struct InternalUserModel { /* ... */ }
}
pub use internal::InternalUserModel as User;
Consumer thấy my_lib::User; tên nội bộ dài vẫn được giữ riêng. Một use case khác là tránh trùng tên khi re-export từ hai module có cùng tên type:
pub use http::Request as HttpRequest;
pub use grpc::Request as GrpcRequest;
Lưu ý: đổi tên public là quyết định API. Một khi đã release với tên User thì sau này không đổi sang tên khác mà không phá compatibility. Vì vậy chỉ đổi tên khi chắc chắn tên public sẽ ổn định lâu dài. Một quy tắc kinh nghiệm: ưu tiên đổi tên nội bộ cho khớp với tên public mong muốn, thay vì giữ tên cũ và dùng alias dài hạn — alias làm rustdoc hiển thị hai tên dễ gây nhầm lẫn cho người mới đọc source.
Re-export Module Toàn Bộ (Glob)
pub use foo::*; re-export tất cả item public của module foo ra module hiện tại.
// src/lib.rs
mod prelude {
pub use crate::user::User;
pub use crate::error::Error;
pub use crate::Result;
}
pub use prelude::*; // bung tất cả ra crate root
Khuyến nghị chung: tránh glob re-export trong public API. Lý do: khi thêm item mới vào module gốc, item đó tự động trở thành public ở mức re-export — có thể tạo xung đột tên với crate consumer khác hoặc làm "lộ" thứ chưa muốn phơi ra. Đọc lib.rs với hàng loạt pub use foo::*; cũng khó biết chính xác API gồm những gì.
Ngoại lệ chấp nhận được: module prelude được thiết kế chuyên để consumer use my_lib::prelude::*; ở đầu file — nội dung prelude là một danh sách được chọn cẩn thận, không phải toàn bộ trait/struct. Style này phổ biến trong diesel, bevy, rayon.
Use Case Thực Tế: serde, tokio, axum
Các crate Rust phổ biến đều dùng pub use rất nhiều — đọc lib.rs của chúng là một bài học sống động.
- serde: trait
SerializevàDeserializeđược định nghĩa trong submoduleservàde, nhưngserde::Serialize/serde::Deserializehoạt động nhờ re-export ởlib.rs:pub use ser::Serialize; pub use de::Deserialize;. Người dùng không cần biết tớiser/de. - tokio: re-export nhiều type từ
futures(nhưFuture,Stream) qua moduletokio::streamđể consumer không phải thêm dependencyfuturesnếu chỉ cần basic. Cũng re-exportPin,Context,Polltừstdquatokio::macros::supportcho code macro-generated. - axum: re-export một số type của
tower(Service,Layer) vàhttp(Request,Response,StatusCode) để user xàiaxumkhông cần importtowerhayhttptrực tiếp — giảm số dependency phải gõ trongCargo.tomlvà rút ngắn import statement.
Pattern chung: thư viện cao tầng re-export type quan trọng từ dependency tầng thấp để consumer không phải biết tới cấu trúc nội bộ. Đây là facade pattern áp dụng ở quy mô ecosystem. Lưu ý mặt trái: khi crate dependency tầng dưới ra major version mới với breaking change, crate cao tầng buộc phải bump major theo nếu type đã re-export — chi phí phải tính khi thiết kế facade rộng.
Tránh: Re-export Type Chưa Sẵn Sàng
Quy tắc đắt giá: re-export một item ra public path = commit item đó là API ổn định. Dù module gốc đặt tên là internal hay private, ngay khi viết pub use internal::Foo;, Foo đã trở thành public — đổi tên, đổi shape, xoá field đều là breaking change theo semver.
// Cẩn thận: cam kết Foo là public API stable
mod internal {
pub struct Foo { pub buf: Vec<u8>, pub state: u32 }
}
pub use internal::Foo;
Một số tip thực tế:
- Chỉ re-export type đã đủ ổn định. Type còn đang thử nghiệm — giữ private hoặc đưa vào
unstablefeature flag. - Cân nhắc viết wrapper public ổn định bao quanh type internal:
pub struct PublicFoo(internal::Foo);— giúp đổi internal không break API. - Re-export trait kèm theo type associated phải chú ý: consumer sẽ dùng method và associated type của trait — đổi trait cũng là breaking change.
- Document re-export bằng
///hoặc#[doc(inline)]trên dòngpub useđể rustdoc hiển thị item như nó được khai báo tại module hiện tại.
Tổng Kết
pub use foo::bar;=use foo::bar;+ đồng thời phơibarnhư item public của module hiện tại.- Facade pattern:
lib.rsre-export key type từ submodule để consumer chỉuse my_lib::X;, không cần biết cấu trúc nội bộ. - Alias
pub use foo::Bar as MyBar;đổi tên public — quyết định API, cần ổn định. - Glob
pub use foo::*;tránh trong public API trừ trường hợp prelude được chọn cẩn thận. - serde, tokio, axum dùng re-export ở
lib.rsrất nhiều — phẳng API và giảm dependency consumer phải khai báo. - Cảnh báo ổn định: re-export tức cam kết item là public API; đổi shape là breaking change theo semver.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết
lib.rscho cratemy_dbcó internal layoutpool/connection.rschứa structConnection,error/kind.rschứa enumDbError. Re-export cả hai để consumer chỉ cầnuse my_db::{Connection, DbError};. - Phân biệt hai dòng:
use crate::user::User;vàpub use crate::user::User;đặt tronglib.rs. Consumer bên ngoài thấy khác biệt gì? - Vừa đổi tên module nội bộ từ
kindsangtypes. Tronglib.rsđang cópub use error::kind::DbError;. Cần sửa gì? Consumer của crate có cần sửa gì không? - Khi nào nên dùng
pub use foo::Bar as MyBar;thay vì giữ nguyên tên? Cho 2 tình huống cụ thể. - Crate có module
preludechứa 5 type quan trọng. Viếtpub use prelude::*;ởlib.rs. Hệ quả tốt và xấu là gì? - Một consumer phàn nàn rằng họ phải import cả
towerđể dùng traitServicevớiaxum. Maintainer axum (tưởng tượng) sẽ giải quyết bằng kỹ thuật nào trong bài này?
Đáp án
pub use crate::pool::connection::Connection;vàpub use crate::error::kind::DbError;tronglib.rs. Lưu ý các module trung gian (pool,connection,error,kind) phảipubtheo path từ chỗ viết — hoặc dùngpub(crate)nếu chỉ muốn nội bộ thấy, miễn item đích public ở chỗ re-export.use(không pub) chỉ bringUservào scope củalib.rsđể code trong file đó dùng — không phơi ra ngoài; consumer vẫn phải gõuse my_lib::user::User.pub usephơi ra ngoài; consumer dùnguse my_lib::User.- Sửa
lib.rsthànhpub use error::types::DbError;. Consumer không cần sửa gì vì path publicmy_db::DbErrorkhông đổi — đây chính là giá trị lớn nhất của facade pattern. - (a) Tên nội bộ dài, ví dụ
InternalUserModelcần phơi ngoài ngắn gọn làUser. (b) Hai module re-export type trùng tên (http::Requestvàgrpc::Request) cần phân biệt — alias thànhHttpRequest/GrpcRequest. - Tốt: consumer dùng
use my_crate::*;hoặcuse my_crate::prelude::*;rất gọn. Xấu: thêm bất kỳ item nào vàopreludeđều "ngầm" trở thành public ở crate root, có thể conflict tên với crate consumer khác hoặc gây surprise. Nên giữ prelude là module riêng (my_crate::prelude) — không re-exportprelude::*ra root. - Re-export trait
Servicetừtowerngay tạiaxum::Service(hoặc một module phù hợp):pub use tower::Service;. Consumer chỉ cầnuse axum::Service;mà không phải khai báotowertrongCargo.toml.
Bài Tiếp Theo
Bài 117: Workspace Preview — Multi-Crate Project — đến đây đã đi hết module bên trong một crate. Khi project lớn hơn, một crate không đủ — cần nhiều crate trong cùng repository, share Cargo.lock và target/, dependency lẫn nhau qua path. Đó là Cargo workspace, và bài 117 là cái nhìn preview trước khi chương Cargo Nâng Cao đào sâu.
