抽象参考容器(Rc、Arc、Box、...)
Abstracting reference containers (Rc, Arc, Box, ...)
我爱好用 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
。这个UserDatabase
被注入到main
方法中的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
.
中移动 user_database
两次
所以我们必须将它包装成 Rc
:
let user_database = Rc::new(UserDatabase{});
但这意味着现在,为了能够将其注入 user_service
,我们还必须将 UserService
结构中的类型包装在 Rc
中:
pub struct UserService {
user_database: Rc<UserDatabase>
}
这让我很困扰,因为:
- 改变
UserService
的外部使用方式,迫使我们改变 UserService
的内部结构。
UserService
现在知道 UserDatabase
在别处使用的事实。 UserService
只需要UserDatabase
调用1个函数。为什么它必须知道 UserDatabase
在别处被引用的事实?客观上我知道这个问题的答案:这是因为程序必须弄清楚如何在不干扰代码的其他部分的情况下安全地从堆中取消引用值;但作为程序员,我们不想将不必要的细节泄露给函数的实现。
与 Arc
类似,程序的所有部分现在都必须知道它们在多线程场景中运行。
有什么方法可以实现一种结构,其中 UserService
不知道 UserDatabase
类型是如何包装的?想到的一个想法是具有特征:
trait GetUserDatabase {
fn get(&self, user_id: UserId) -> User;
}
然后以某种方式能够使 Rc<UserDatabase>
或 Arc<UserDatabase>
实现该特征。
也许我在这里遗漏了一些巧妙的技巧,或者我可能只是需要在接近依赖注入等模式时改变我的传统 Golang/Java 思维方式。
Rust 确实不是为 Java 的依赖注入思维而设计的。
UserService
is now aware of the fact that UserDatabase
is used elsewhere. The UserService
only needs the UserDatabase
to call 1 function. Why should it have to be aware of the fact that the UserDatabase
is referenced elsewhere?
因为这正是 Rust 想要明确的事情。
这不仅仅是一个限制:Rust 想让你知道谁拥有你的对象。所有权制度应该设计你的想法和你的程序。这个“share-style”确实不是很 Rusty——这也是 Rc
和朋友在惯用 Rust 中不常见的原因之一(它们确实出现,但与 GC 语言的数量不同) .
不要考虑服务 - 考虑数据。与其问 谁需要访问用户数据库,不如问 谁。所有者不会将数据库注入给任何需要它的人,他只是让他们看看。如果 main()
拥有数据库,服务应该引用它(或者根本不存在)。如果两个服务都拥有它,它应该是 Rc
。无论哪种方式,您都必须明确说明这一点。这是一件好事!
现有答案对于您给定的情况是正确的,因此您一定要遵循那里给出的建议并接受它是正确的。此答案旨在回答标题中给出的直接问题,适用于恰好通过网络搜索登陆此处并且确实需要此功能的任何其他人。
您在这里寻找的标准库特征是 Borrow<T>
,意思是“您可以从中借用一个 T
。”
Borrow<T>
是为智能指针Rc<T>
、Arc<T>
、Box<T, _>
和Cow<'_, T>
实现的。 Borrow<T>
甚至为 T
和 &T
实现,因此您可以像处理智能指针一样处理拥有的值和引用。
利用此特性的一种方法是使用泛型:
pub struct UserService<UserDB: Borrow<UserDatabase>> {
user_database: UserDB
}
此方法的注意事项是 UserService
现在是泛型类型,因此与它接触的任何其他对象都需要(在某种程度上)了解泛型类型参数。如果您对程序中的每种服务类型都使用这种方法,那么泛型可能会“感染”程序的其余部分,坦率地说,达到令人讨厌的程度——任何过去接受 UserService
的东西现在都需要接受UserService<impl Borrow<UserDatabase>>
。这也会将有关服务依赖性的详细信息泄露给使用它的任何设施,这可能是不可取的。
没有通用方法缺点的第二种方法是:
pub struct UserService {
user_database: Box<dyn Borrow<UserDatabase>>
}
现在如果你有一个UserDatabase
,你可以给它一个Box<UserDatabase>
。如果你有一个Rc<UserDatabase>
,你可以给它一个Box<Rc<UserDatabase>>
。要在不使用智能指针的情况下借用现有实例,您可以给它一个 Box<&UserDatabase>
.
但是,这种方法有其自身的问题:存在额外的间接级别,并且需要动态调度来调用 Borrow::borrow()
。性能损失可能值得增加灵活性,但这是您必须做出的决定。
如果您进行依赖注入,您的设计可能应该基于特征。例如:
pub trait UserDatabase {
fn get(&self, user_id: UserId) -> User;
}
// UserService works with any database that implements the trait
pub struct UserService<Db> {
user_database: Db,
}
impl<Db: UserDatabase> UserService<Db> {
fn get_user(&self, user_id: UserId) -> User {
self.user_database.get(user_id)
}
}
trait 的实现并不比以前复杂:
pub struct UserDatabaseImpl {}
impl UserDatabase for UserDatabaseImpl {
fn get(&self, user_id: UserId) -> User {
todo!()
}
}
...你的 main()
看起来也一样:
fn main() {
let user_database = UserDatabaseImpl {};
let user_service = UserService { user_database };
user_service.get_user(todo!());
}
但是现在,如果你想共享数据库,你不再需要修改UserService
- 你只需要提供一个允许这种共享的特征的新实现。在上述情况下,它甚至可以使用 non-sharing 实现:
impl UserDatabase for Rc<UserDatabaseImpl> {
fn get(&self, user_id: UserId) -> User {
self.as_ref().get(user_id)
}
}
数据库现在可以在多个服务之间共享,而无需修改 UserService
或 SomeOtherService
:
的实现
pub struct SomeOtherService<Db> {
user_database: Db,
}
fn main() {
let user_database = Rc::new(UserDatabaseImpl {});
let user_service = UserService {
user_database: Rc::clone(&user_database),
};
let some_other_service = SomeOtherService {
user_database: Rc::clone(&user_database),
};
user_service.get_user(todo!());
}
我爱好用 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
。这个UserDatabase
被注入到main
方法中的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
.
user_database
两次
所以我们必须将它包装成 Rc
:
let user_database = Rc::new(UserDatabase{});
但这意味着现在,为了能够将其注入 user_service
,我们还必须将 UserService
结构中的类型包装在 Rc
中:
pub struct UserService {
user_database: Rc<UserDatabase>
}
这让我很困扰,因为:
- 改变
UserService
的外部使用方式,迫使我们改变UserService
的内部结构。 UserService
现在知道UserDatabase
在别处使用的事实。UserService
只需要UserDatabase
调用1个函数。为什么它必须知道UserDatabase
在别处被引用的事实?客观上我知道这个问题的答案:这是因为程序必须弄清楚如何在不干扰代码的其他部分的情况下安全地从堆中取消引用值;但作为程序员,我们不想将不必要的细节泄露给函数的实现。
与 Arc
类似,程序的所有部分现在都必须知道它们在多线程场景中运行。
有什么方法可以实现一种结构,其中 UserService
不知道 UserDatabase
类型是如何包装的?想到的一个想法是具有特征:
trait GetUserDatabase {
fn get(&self, user_id: UserId) -> User;
}
然后以某种方式能够使 Rc<UserDatabase>
或 Arc<UserDatabase>
实现该特征。
也许我在这里遗漏了一些巧妙的技巧,或者我可能只是需要在接近依赖注入等模式时改变我的传统 Golang/Java 思维方式。
Rust 确实不是为 Java 的依赖注入思维而设计的。
UserService
is now aware of the fact thatUserDatabase
is used elsewhere. TheUserService
only needs theUserDatabase
to call 1 function. Why should it have to be aware of the fact that theUserDatabase
is referenced elsewhere?
因为这正是 Rust 想要明确的事情。
这不仅仅是一个限制:Rust 想让你知道谁拥有你的对象。所有权制度应该设计你的想法和你的程序。这个“share-style”确实不是很 Rusty——这也是 Rc
和朋友在惯用 Rust 中不常见的原因之一(它们确实出现,但与 GC 语言的数量不同) .
不要考虑服务 - 考虑数据。与其问 谁需要访问用户数据库,不如问 谁。所有者不会将数据库注入给任何需要它的人,他只是让他们看看。如果 main()
拥有数据库,服务应该引用它(或者根本不存在)。如果两个服务都拥有它,它应该是 Rc
。无论哪种方式,您都必须明确说明这一点。这是一件好事!
现有答案对于您给定的情况是正确的,因此您一定要遵循那里给出的建议并接受它是正确的。此答案旨在回答标题中给出的直接问题,适用于恰好通过网络搜索登陆此处并且确实需要此功能的任何其他人。
您在这里寻找的标准库特征是 Borrow<T>
,意思是“您可以从中借用一个 T
。”
Borrow<T>
是为智能指针Rc<T>
、Arc<T>
、Box<T, _>
和Cow<'_, T>
实现的。 Borrow<T>
甚至为 T
和 &T
实现,因此您可以像处理智能指针一样处理拥有的值和引用。
利用此特性的一种方法是使用泛型:
pub struct UserService<UserDB: Borrow<UserDatabase>> {
user_database: UserDB
}
此方法的注意事项是 UserService
现在是泛型类型,因此与它接触的任何其他对象都需要(在某种程度上)了解泛型类型参数。如果您对程序中的每种服务类型都使用这种方法,那么泛型可能会“感染”程序的其余部分,坦率地说,达到令人讨厌的程度——任何过去接受 UserService
的东西现在都需要接受UserService<impl Borrow<UserDatabase>>
。这也会将有关服务依赖性的详细信息泄露给使用它的任何设施,这可能是不可取的。
没有通用方法缺点的第二种方法是:
pub struct UserService {
user_database: Box<dyn Borrow<UserDatabase>>
}
现在如果你有一个UserDatabase
,你可以给它一个Box<UserDatabase>
。如果你有一个Rc<UserDatabase>
,你可以给它一个Box<Rc<UserDatabase>>
。要在不使用智能指针的情况下借用现有实例,您可以给它一个 Box<&UserDatabase>
.
但是,这种方法有其自身的问题:存在额外的间接级别,并且需要动态调度来调用 Borrow::borrow()
。性能损失可能值得增加灵活性,但这是您必须做出的决定。
如果您进行依赖注入,您的设计可能应该基于特征。例如:
pub trait UserDatabase {
fn get(&self, user_id: UserId) -> User;
}
// UserService works with any database that implements the trait
pub struct UserService<Db> {
user_database: Db,
}
impl<Db: UserDatabase> UserService<Db> {
fn get_user(&self, user_id: UserId) -> User {
self.user_database.get(user_id)
}
}
trait 的实现并不比以前复杂:
pub struct UserDatabaseImpl {}
impl UserDatabase for UserDatabaseImpl {
fn get(&self, user_id: UserId) -> User {
todo!()
}
}
...你的 main()
看起来也一样:
fn main() {
let user_database = UserDatabaseImpl {};
let user_service = UserService { user_database };
user_service.get_user(todo!());
}
但是现在,如果你想共享数据库,你不再需要修改UserService
- 你只需要提供一个允许这种共享的特征的新实现。在上述情况下,它甚至可以使用 non-sharing 实现:
impl UserDatabase for Rc<UserDatabaseImpl> {
fn get(&self, user_id: UserId) -> User {
self.as_ref().get(user_id)
}
}
数据库现在可以在多个服务之间共享,而无需修改 UserService
或 SomeOtherService
:
pub struct SomeOtherService<Db> {
user_database: Db,
}
fn main() {
let user_database = Rc::new(UserDatabaseImpl {});
let user_service = UserService {
user_database: Rc::clone(&user_database),
};
let some_other_service = SomeOtherService {
user_database: Rc::clone(&user_database),
};
user_service.get_user(todo!());
}