Privacy for Extensibility
Một trong những idioms quan trọng nhất trong Rust API design là sử dụng privacy (tính riêng tư) để đảm bảo API có thể được mở rộng trong tương lai mà không breaking changes.
Nguyên tắc cơ bản
Mặc định, hãy giữ mọi thứ private, chỉ expose những gì cần thiết.
Điều này cho phép bạn thay đổi implementation details trong tương lai mà không ảnh hưởng đến users của library.
Ví dụ: Struct Fields
❌ Không nên: Public fields
pub struct User {
pub username: String,
pub email: String,
pub age: u32,
}
fn main() {
let user = User {
username: "duyet".to_string(),
email: "duyet@example.com".to_string(),
age: 25,
};
// Users có thể truy cập và modify trực tiếp
println!("{}", user.age);
}
Vấn đề:
- Không thể thêm validation
- Không thể track changes
- Không thể thay đổi internal representation
- Breaking change nếu rename hoặc xóa field
✅ Nên: Private fields với accessor methods
pub struct User {
username: String,
email: String,
age: u32,
}
impl User {
pub fn new(username: String, email: String, age: u32) -> Result<Self, String> {
if age > 150 {
return Err("Invalid age".to_string());
}
if !email.contains('@') {
return Err("Invalid email".to_string());
}
Ok(User { username, email, age })
}
pub fn username(&self) -> &str {
&self.username
}
pub fn email(&self) -> &str {
&self.email
}
pub fn age(&self) -> u32 {
self.age
}
pub fn set_email(&mut self, email: String) -> Result<(), String> {
if !email.contains('@') {
return Err("Invalid email".to_string());
}
self.email = email;
Ok(())
}
}
fn main() {
let user = User::new(
"duyet".to_string(),
"duyet@example.com".to_string(),
25,
).unwrap();
println!("{}", user.age());
}
Ưu điểm:
- Có thể thêm validation logic
- Có thể thay đổi internal representation
- Có thể track changes, logging
- API ổn định hơn
Non-exhaustive Enums
Sử dụng #[non_exhaustive] để cho phép thêm variants trong tương lai:
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum Error {
NotFound,
PermissionDenied,
Timeout,
}
// Trong tương lai có thể thêm:
// NetworkError,
// DatabaseError,
// ...mà không breaking existing code
}
Users buộc phải có default case khi match:
#![allow(unused)]
fn main() {
fn handle_error(error: Error) {
match error {
Error::NotFound => println!("Not found"),
Error::PermissionDenied => println!("Permission denied"),
Error::Timeout => println!("Timeout"),
// Bắt buộc phải có _ case
_ => println!("Other error"),
}
}
}
Non-exhaustive Structs
Tương tự với structs, #[non_exhaustive] prevent users khởi tạo trực tiếp:
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub struct Config {
pub host: String,
pub port: u16,
}
impl Config {
pub fn new(host: String, port: u16) -> Self {
Config { host, port }
}
}
}
Users không thể sử dụng struct literal syntax:
#![allow(unused)]
fn main() {
// ❌ Compile error!
// let config = Config {
// host: "localhost".to_string(),
// port: 8080,
// };
// ✅ Must use constructor
let config = Config::new("localhost".to_string(), 8080);
}
Builder Pattern cho Extensibility
Builder pattern giúp thêm fields mới mà không breaking changes:
pub struct HttpClient {
timeout: u64,
retry_count: u32,
user_agent: String,
}
pub struct HttpClientBuilder {
timeout: u64,
retry_count: u32,
user_agent: String,
}
impl HttpClient {
pub fn builder() -> HttpClientBuilder {
HttpClientBuilder {
timeout: 30,
retry_count: 3,
user_agent: "MyClient/1.0".to_string(),
}
}
}
impl HttpClientBuilder {
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
pub fn retry_count(mut self, count: u32) -> Self {
self.retry_count = count;
self
}
pub fn user_agent(mut self, agent: String) -> Self {
self.user_agent = agent;
self
}
pub fn build(self) -> HttpClient {
HttpClient {
timeout: self.timeout,
retry_count: self.retry_count,
user_agent: self.user_agent,
}
}
}
fn main() {
let client = HttpClient::builder()
.timeout(60)
.retry_count(5)
.build();
}
Trong tương lai có thể thêm field mới:
#![allow(unused)]
fn main() {
pub struct HttpClientBuilder {
timeout: u64,
retry_count: u32,
user_agent: String,
// Thêm mới - không breaking change!
max_redirects: u32,
}
impl HttpClientBuilder {
// Thêm method mới
pub fn max_redirects(mut self, max: u32) -> Self {
self.max_redirects = max;
self
}
}
// Existing code vẫn hoạt động bình thường!
let client = HttpClient::builder()
.timeout(60)
.build();
}
Private modules
Sử dụng private modules để ẩn implementation details:
#![allow(unused)]
fn main() {
// lib.rs
mod internal {
// Private helper functions
pub(crate) fn helper() {
// ...
}
}
pub mod api {
// Public API
pub fn public_function() {
crate::internal::helper();
}
}
}
Sealed Trait Pattern
Prevent users implement trait của bạn:
#![allow(unused)]
fn main() {
mod sealed {
pub trait Sealed {}
}
pub trait MyTrait: sealed::Sealed {
fn do_something(&self);
}
// Chỉ types trong module này có thể implement MyTrait
impl sealed::Sealed for MyType {}
impl MyTrait for MyType {
fn do_something(&self) {
// ...
}
}
// Users không thể implement MyTrait cho types của họ
// vì không thể access sealed::Sealed
}
Visibility Modifiers
Rust cung cấp nhiều levels của visibility:
#![allow(unused)]
fn main() {
pub struct Container {
pub public_field: i32, // Accessible everywhere
pub(crate) crate_field: i32, // Accessible within crate
pub(super) parent_field: i32, // Accessible in parent module
private_field: i32, // Accessible in same module
}
}
Real-world Example: Database Client
#![allow(unused)]
fn main() {
pub struct Database {
// Private: có thể thay đổi từ String sang URL type
connection_string: String,
// Private: có thể switch sang connection pool
max_connections: u32,
}
impl Database {
pub fn connect(url: &str) -> Result<Self, String> {
// Validation
if url.is_empty() {
return Err("Empty URL".to_string());
}
Ok(Database {
connection_string: url.to_string(),
max_connections: 10,
})
}
pub fn with_max_connections(url: &str, max: u32) -> Result<Self, String> {
let mut db = Self::connect(url)?;
db.max_connections = max;
Ok(db)
}
pub fn execute(&self, query: &str) -> Result<Vec<String>, String> {
// Implementation có thể thay đổi hoàn toàn
println!("Executing: {}", query);
Ok(vec![])
}
}
}
Trong tương lai có thể thay đổi implementation:
#![allow(unused)]
fn main() {
use std::sync::Arc;
pub struct Database {
// Thay đổi internal structure
connection_pool: Arc<ConnectionPool>,
config: DatabaseConfig,
}
// Public API không thay đổi!
impl Database {
pub fn connect(url: &str) -> Result<Self, String> {
// New implementation
// ...
}
pub fn execute(&self, query: &str) -> Result<Vec<String>, String> {
// New implementation using pool
// ...
}
}
}
Best Practices
- Start private: Mặc định mọi thứ private, expose dần khi cần
- Use
#[non_exhaustive]: Cho enums và structs có thể mở rộng - Prefer methods over public fields: Giữ control và flexibility
- Document stability: Ghi rõ API nào stable, nào experimental
- Use semantic versioning: Breaking changes = major version bump
Anti-patterns
❌ Tránh expose internal types:
#![allow(unused)]
fn main() {
// ❌ Bad
pub use internal::InternalType;
pub fn process() -> InternalType {
// ...
}
}
✅ Wrap hoặc convert:
#![allow(unused)]
fn main() {
// ✅ Good
pub struct PublicType {
// ...
}
pub fn process() -> PublicType {
let internal = internal::InternalType::new();
PublicType::from(internal)
}
}