Dependency Injection di Rust — ServiceLocator, Arc, dan Trait Object
Engineering Team
Dependency Injection di Rust
Rust tidak memiliki framework DI seperti Spring (Java) atau .NET. Tetapi sistem trait Rust menyediakan mekanisme DI yang kuat dan type-safe. Kuncinya: definisikan behavior sebagai trait, terima trait sebagai parameter, dan injeksi implementasi konkret saat runtime.
Trait sebagai Interface
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError>;
async fn create(&self, user: CreateUser) -> Result<User, DbError>;
async fn update(&self, id: Uuid, data: UpdateUser) -> Result<User, DbError>;
async fn delete(&self, id: Uuid) -> Result<(), DbError>;
}
#[async_trait]
pub trait EmailService: Send + Sync {
async fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), EmailError>;
}
Implementasi Konkret
pub struct PgUserRepository {
pool: PgPool,
}
#[async_trait]
impl UserRepository for PgUserRepository {
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError> {
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&self.pool)
.await
.map_err(DbError::from)
}
// ... implementasi lainnya
}
pub struct SmtpEmailService {
host: String,
port: u16,
}
#[async_trait]
impl EmailService for SmtpEmailService {
async fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), EmailError> {
// Kirim email via SMTP
Ok(())
}
}
Service Layer dengan DI
pub struct UserService<R: UserRepository, E: EmailService> {
repo: R,
email: E,
}
impl<R: UserRepository, E: EmailService> UserService<R, E> {
pub fn new(repo: R, email: E) -> Self {
Self { repo, email }
}
pub async fn register(&self, data: CreateUser) -> Result<User, AppError> {
let user = self.repo.create(data).await?;
self.email.send(
&user.email,
"Selamat datang!",
"Terima kasih telah mendaftar."
).await?;
Ok(user)
}
}
Arc untuk Shared Ownership
Dalam aplikasi web, service sering di-share antar request handler. Arc memungkinkan shared ownership yang thread-safe:
type DynUserRepo = Arc<dyn UserRepository>;
type DynEmailService = Arc<dyn EmailService>;
#[derive(Clone)]
pub struct AppState {
pub user_repo: DynUserRepo,
pub email_svc: DynEmailService,
}
impl AppState {
pub fn new(pool: PgPool, smtp_config: SmtpConfig) -> Self {
Self {
user_repo: Arc::new(PgUserRepository { pool: pool.clone() }),
email_svc: Arc::new(SmtpEmailService {
host: smtp_config.host,
port: smtp_config.port,
}),
}
}
}
impl Trait vs dyn Trait
impl Trait (static dispatch):
fn process(repo: &impl UserRepository) { ... }
// Dikompilasi menjadi fungsi terpisah per tipe konkret
// Zero-cost abstraction, tapi tidak bisa menyimpan tipe berbeda
dyn Trait (dynamic dispatch):
fn process(repo: &dyn UserRepository) { ... }
// Menggunakan vtable untuk dispatch saat runtime
// Fleksibel, overhead minimal (~2ns per panggilan)
Gunakan dyn Trait ketika:
- Menyimpan di struct sebagai field
- Menyimpan di koleksi heterogen
- Memerlukan dependency injection saat runtime
Gunakan impl Trait ketika:
- Parameter fungsi dengan tipe yang diketahui saat kompilasi
- Return type fungsi
- Performa kritis (menghindari vtable lookup)
Testing dengan Mock
#[cfg(test)]
mod tests {
use super::*;
struct MockUserRepo {
users: Mutex<Vec<User>>,
}
#[async_trait]
impl UserRepository for MockUserRepo {
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError> {
let users = self.users.lock().unwrap();
Ok(users.iter().find(|u| u.id == id).cloned())
}
async fn create(&self, data: CreateUser) -> Result<User, DbError> {
let user = User {
id: Uuid::new_v4(),
email: data.email,
name: data.name,
};
self.users.lock().unwrap().push(user.clone());
Ok(user)
}
// ...
}
struct MockEmailService {
sent: Mutex<Vec<(String, String)>>,
}
#[async_trait]
impl EmailService for MockEmailService {
async fn send(&self, to: &str, subject: &str, _body: &str) -> Result<(), EmailError> {
self.sent.lock().unwrap().push((to.to_string(), subject.to_string()));
Ok(())
}
}
#[tokio::test]
async fn test_register_sends_email() {
let repo = MockUserRepo { users: Mutex::new(vec![]) };
let email = MockEmailService { sent: Mutex::new(vec![]) };
let service = UserService::new(repo, email);
let user = service.register(CreateUser {
email: "test@example.com".into(),
name: "Test User".into(),
}).await.unwrap();
assert_eq!(user.email, "test@example.com");
// Verifikasi email dikirim
let sent = service.email.sent.lock().unwrap();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].0, "test@example.com");
}
}
Kesimpulan
Dependency injection di Rust menggunakan trait sebagai interface, Arc untuk shared ownership, dan dyn Trait untuk dispatch runtime. Pola ini memberikan testability tinggi tanpa framework DI eksternal — memanfaatkan kekuatan sistem tipe Rust untuk keamanan kompilasi dan zero-cost abstraction.