我已经有一段时间喜欢在Rust上编程了,当我试图创建抽象时,有一些东西让我很恼火.下面是一个小例子,我用依赖项注入风格编写程序时得到的代码类型:
type UserId = u8;
pub struct User {}
pub struct UserDatabase {}
impl UserDatabase {
pub fn get(&self, user_id: UserId) -> User {
todo!()
}
}
pub struct UserService {
user_database: UserDatabase
}
impl UserService {
fn get_user(&self, user_id: UserId) -> User {
self.user_database.get(user_id)
}
}
fn main() {
let user_database = UserDatabase{};
let user_service = UserService{
user_database
};
user_service.get_user(todo!());
}
在本例中,UserService
公开了get_user
方法.在内部,这称为UserDatabase
.在main
方法中,这UserDatabase
被注入到UserService
中.
因此,这一切都很好,但考虑现在的情况下,这UserDatabase
个需要使用(注入)另一个服务:
pub struct SomeOtherService {
user_database: UserDatabase
}
fn main() {
let user_database = UserDatabase{};
let user_service = UserService{
user_database
};
let some_other_service = SomeOtherService{
user_database
};
user_service.get_user(todo!());
}
显然,这段代码无法编译,因为我们在main
中try 了两次移动user_database
.
所以我们必须把它包装成Rc
号:
let user_database = Rc::new(UserDatabase{});
但这意味着现在,为了能够将其注入user_service
,我们还必须将UserService
struct 中的类型封装在Rc
中:
pub struct UserService {
user_database: Rc<UserDatabase>
}
这让我很困扰,因为:
- 改变了
UserService
的外部使用方式,迫使我们改变了UserService
的内部 struct . -
UserService
现在意识到UserDatabase
在其他地方使用的事实.UserService
只需要UserDatabase
就可以调用1个函数.为什么它必须意识到UserDatabase
在其他地方被引用的事实?客观地说,我知道答案:这是因为程序必须解决如何在不干扰代码其他部分的情况下安全地从堆中取消对值的引用;但是作为程序员,我们不想把不必要的细节泄露给函数的实现.
类似地,对于Arc
这样的对象,程序的所有部分现在都必须知道它们是在多线程场景中运行的.
我们有没有办法实现一种UserService
不知道UserDatabase
是如何包装的 struct ?我想到的一个 idea 是有一个特点:
trait GetUserDatabase {
fn get(&self, user_id: UserId) -> User;
}
然后以某种方式让Rc<UserDatabase>
或Arc<UserDatabase>
人实现这种trait .
也许我在这里错过了一些巧妙的技巧,或者也许我只是需要在处理依赖注入之类的模式时改变我传统的Golang/Java思维方式.