具有多个结构的泛型和依赖倒置
Posted
技术标签:
【中文标题】具有多个结构的泛型和依赖倒置【英文标题】:Generics and dependency inversion with multiple structs 【发布时间】:2021-12-28 10:24:42 【问题描述】:我正在尝试在 Rust 中创建一个干净的架构结构,其中一些结构使用特征进行依赖倒置。
我的想法基本上是:
只有一个字段的User
模型。
符合存储库特征/接口的存储库,其中包含从 mysql 数据库检索用户的方法。
一个依赖于存储库特征/接口的用例,并在使用new
方法实例化时接收此存储库的实例。我还持有一个execute
方法来触发存储库操作。
依赖于用例特征/接口的控制器,并在使用new
方法对其进行实例化时接收此用例的实例。它还包含一个execute
方法来触发用例操作。
User:
+ id
UserRepository complies with IUserRepository:
- get_all_users: () -> Vec<Users>
GetUsersUseCase complies with IGetUsersUseCase:
+ user_repository: IUserRepository
- new: (user_repository: IUserRepository) -> GetUsersUseCase
- execute: () -> Vec<Users>
GetUsersController:
+ get_users_use_case: IGetUsersUseCase
- new: (user_use_case: IGetUsersUseCase) -> GetUsersController
- execute: () -> Vec<Users>
我有一个实现,但是泛型有问题。基本代码:
游乐场:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ca1f4f9d6cfe4df2864d7e574966ad6b.
代码:
// MODEL
#[derive(Debug)]
struct User
id: i8,
// REPOSITORY for DB
trait IUserRepository
fn get_users(&self) -> Vec<User>;
struct MySQLUserRepository;
impl IUserRepository for MySQLUserRepository
fn get_users(&self) -> Vec<User>
let mock_user1 = User id: 1 ;
let mock_user2 = User id: 2 ;
let mock_users = vec![mock_user1, mock_user2];
mock_users
// USE CASE
trait IGetUsersUseCase
fn new<T: IUserRepository>(repository: T) -> GetUsersUseCase<T>;
fn execute(&self) -> Vec<User>;
struct GetUsersUseCase<T>
user_repo: T,
impl<T: IUserRepository> IGetUsersUseCase for GetUsersUseCase<T>
fn new<K: IUserRepository>(user_repo: K) -> GetUsersUseCase<K>
GetUsersUseCase user_repo
fn execute(&self) -> Vec<User>
let users = self.user_repo.get_users();
users
// CONTROLLER for HTTP requests
struct GetUsersController<T>
get_users_use_case: T,
impl<T: IGetUsersUseCase> GetUsersController<T>
fn new(get_users_use_case: T) -> GetUsersController<T>
GetUsersController get_users_use_case
fn execute(&self) -> Vec<User>
let users = self.get_users_use_case.execute();
users
fn main()
// Lets imagine we are handling an HTTP request
let mysql_repo = MySQLUserRepository ;
// Error here: cannot infer type for type parameter `T` declared on the struct `GetUsersUseCase`
let get_users_use_case = GetUsersUseCase::new(mysql_repo);
let get_users_controller = GetUsersController::new(get_users_use_case);
let users = get_users_controller.execute();
println!(":?", users);
如您所见,问题在于 GetUsersUseCase —impl<T: IUserRepository> IGetUsersUseCase for GetUsersUseCase<T>
- 的实现。由于实现接收两个泛型参数,在构造GetUsersUseCase::new(mysql_repo)
时收到以下错误:
cannot infer type for type parameter `T` declared on the struct `GetUsersUseCase`rustc(E0282)
解决方法是将user_repo
GetUsersUseCase
公开,而不是像往常一样使用GetUsersUseCase::new(mysql_repo)
实例化它:
[…]
struct GetUsersUseCase<T>
pub user_repo: T,
[…]
let get_users_use_case = GetUsersUseCase
user_repo: mysql_repo,
;
[…]
这可行,但我真的很想知道如何使用公共函数构造结构而不暴露私有字段。
这是公开该字段的操场:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=917cee9d969dccd08c4e27753d04994f
欢迎任何想法!
【问题讨论】:
你可以这样帮忙编译,第64行let get_users_use_case = GetUsersUseCase::<MySQLUserRepository>::new(mysql_repo);
非常感谢。它可以工作,并且保持依赖关系方向。但是,就良好实践而言……这是否意味着我的结构复杂或不切实际?
您的结构显然受到 OOP MVC 模型的启发,而 Rust 有一些方法可以做到这一点。您可以阅读此文档 (doc.rust-lang.org/book/ch17-00-oop.html) 但是,我的观点是,尝试将 OOP 范式与更面向功能的 Rust 一起使用可能会很困难。您可以在 Diesel (diesel.rs) 等板条箱中找到灵感
注意:我使用的结构与 MVC 无关,而是与清洁架构、六边形、洋葱或端口/适配器有关。基本上通过依赖倒置将外部依赖与域逻辑解耦。但我同意 Rust 不是纯粹的 OOP,尽管我看到有一些组合方法;唯一没有提供的是继承,我一点也不怀念。
一个小评论:在 Rust 中,在语法上不可能使用预期结构的特征,反之亦然。这意味着您不需要做 Java(和 C#)的事情,即在所有特征前加上 I(用于接口)。
【参考方案1】:
代码的快速修复:
// No more error here
let get_users_use_case = GetUsersUseCase::<MySQLUserRepository>::new(mysql_repo);
为什么会这样?
它从trait IGetUsersUseCase
开始:它不需要泛型类型,但是它的方法fn new<T>()
需要,所以struct GetUsersUseCase<T>
。看看这里:
impl<T: IUserRepository> IGetUsersUseCase for GetUsersUseCase<T>
fn new<K: IUserRepository>(user_repo: K) -> GetUsersUseCase<K>
GetUsersUseCase user_repo
这是一个正确的实现,具有这样的特征,它实际上表明T
没有被引用对于K
,就像它们是“不同”(K
是为函数定义的,T
是为结构定义的,两者都没有为特征定义)。因此,您需要帮助编译器使用 turbofish 运算符 (GetUsersUseCase::<MySQLUserRepository>
)。
这不是唯一的方法。例如,您可以将泛型从方法级别移动到特征级别。
// BEFORE
trait IGetUsersUseCase
fn new<T: IUserRepository>(repository: T) -> GetUsersUseCase<T>;
fn execute(&self) -> Vec<User>;
// AFTER
trait IGetUsersUseCase<T: IUserRepository>
fn new(repository: T) -> GetUsersUseCase<T>;
fn execute(&self) -> Vec<User>;
但是,在下一个特征声明时,您会发现事情变得相当复杂。 (见操场https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=38f41d8b50464acc28c7dc0c70842e6e)。
我会建议您审查您的“界面”(特征)设计。现在,您有了一种“继承”Repo -> UseCase -> Controller
,但是,例如,您可以将控制器实现为一组简单的函数,也许您不需要到处都使用泛型类型。
【讨论】:
非常感谢,有史以来最好的答案,很难量化您的帮助。我试图在特征级别而不是方法级别传递泛型,但不知道PhantomData
强制使用泛型参数。
关于依赖方向:程序的核心是模型User
。在外层我们有GetUsersUseCase
和IUserRepository
,这取决于它。在外层,在一个边缘,我们有 MySQLUserRepository
取决于 IUserRepository
和实体 User
。同样在外层,在另一边,我们有GetUsersController
,它依赖于IUserRepository
和IUserUseCase
,都在内层中。所以依赖方向是正确的AFAIK。
这是一个描述依赖方向的图表:i.stack.imgur.com/1gHka.jpg
我明白你在做什么,事情是这样的: 1. 控制器不必具有特征。它在您的图表上不存在,或者您不会在 Clean Architecture.2 中找到它。控制器可以是一个简单的功能。 3.“获取用户控制器”?控制器不执行这么狭隘的功能,它应该只是“控制器”。 4.问题始于控制器的特征:删除它。 5. 我怀疑 HTTP -> Controller 流程,因为 Clean Architecture 显示 HTTP -> UI/Presenter 流程,而控制器则搁置一旁。以上是关于具有多个结构的泛型和依赖倒置的主要内容,如果未能解决你的问题,请参考以下文章