Danh sách bài viết

Bài 116: pub use — Re-export

Bài 116 của series Rust Cơ Bản — pub use vừa bring item vào scope hiện tại (như use thường), vừa re-export item đó như thành viên public của module hiện tại. Bài này phân tích vấn đề API public phân tán, cú pháp pub use cơ bản, facade pattern trong lib.rs, re-export với alias, glob re-export và lý do tránh, cách serde, tokio, axum dùng kỹ thuật này, cùng cảnh báo về ổn định API khi re-export type từ internal module.

09/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ẽ:

  • 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, serde tổ 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.
2

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

3

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 đủ.

4

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.

5

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};
6

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.

7

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.

8

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 SerializeDeserialize được định nghĩa trong submodule serde, nhưng serde::Serialize / serde::Deserialize hoạ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ới ser/de.
  • tokio: re-export nhiều type từ futures (như Future, Stream) qua module tokio::stream để consumer không phải thêm dependency futures nếu chỉ cần basic. Cũng re-export Pin, Context, Poll từ std qua tokio::macros::support cho code macro-generated.
  • axum: re-export một số type của tower (Service, Layer) và http (Request, Response, StatusCode) để user xài axum không cần import tower hay http trực tiếp — giảm số dependency phải gõ trong Cargo.toml và 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.

9

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 unstable feature 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òng pub use để rustdoc hiển thị item như nó được khai báo tại module hiện tại.
10

Tổng Kết

  • pub use foo::bar; = use foo::bar; + đồng thời phơi bar như item public của module hiện tại.
  • Facade pattern: lib.rs re-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.rs rấ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.
11

Bài Tập Củng Cố

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

  1. Viết lib.rs cho crate my_db có internal layout pool/connection.rs chứa struct Connection, error/kind.rs chứa enum DbError. Re-export cả hai để consumer chỉ cần use my_db::{Connection, DbError};.
  2. Phân biệt hai dòng: use crate::user::User;pub use crate::user::User; đặt trong lib.rs. Consumer bên ngoài thấy khác biệt gì?
  3. Vừa đổi tên module nội bộ từ kind sang types. Trong lib.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?
  4. 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ể.
  5. Crate có module prelude chứa 5 type quan trọng. Viết pub use prelude::*;lib.rs. Hệ quả tốt và xấu là gì?
  6. Một consumer phàn nàn rằng họ phải import cả tower để dùng trait Service với axum. 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
  1. pub use crate::pool::connection::Connection;pub use crate::error::kind::DbError; trong lib.rs. Lưu ý các module trung gian (pool, connection, error, kind) phải pub theo path từ chỗ viết — hoặc dùng pub(crate) nếu chỉ muốn nội bộ thấy, miễn item đích public ở chỗ re-export.
  2. use (không pub) chỉ bring User vào scope của lib.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 use phơi ra ngoài; consumer dùng use my_lib::User.
  3. Sửa lib.rs thành pub use error::types::DbError;. Consumer không cần sửa gì vì path public my_db::DbError không đổi — đây chính là giá trị lớn nhất của facade pattern.
  4. (a) Tên nội bộ dài, ví dụ InternalUserModel cần phơi ngoài ngắn gọn là User. (b) Hai module re-export type trùng tên (http::Requestgrpc::Request) cần phân biệt — alias thành HttpRequest/GrpcRequest.
  5. Tốt: consumer dùng use my_crate::*; hoặc use my_crate::prelude::*; rất gọn. Xấu: thêm bất kỳ item nào vào prelude đề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-export prelude::* ra root.
  6. Re-export trait Service từ tower ngay tại axum::Service (hoặc một module phù hợp): pub use tower::Service;. Consumer chỉ cần use axum::Service; mà không phải khai báo tower trong Cargo.toml.
12

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.locktarget/, 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.