具有多个结构的泛型和依赖倒置

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&lt;T: IUserRepository&gt; IGetUsersUseCase for GetUsersUseCase&lt;T&gt;- 的实现。由于实现接收两个泛型参数,在构造GetUsersUseCase::new(mysql_repo) 时收到以下错误:

cannot infer type for type parameter `T` declared on the struct `GetUsersUseCase`rustc(E0282)

解决方法是将user_repoGetUsersUseCase 公开,而不是像往常一样使用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::&lt;MySQLUserRepository&gt;::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&lt;T&gt;() 需要,所以struct GetUsersUseCase&lt;T&gt;。看看这里:

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::&lt;MySQLUserRepository&gt;)。

不是唯一的方法。例如,您可以将泛型从方法级别移动到特征级别。

// 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 -&gt; UseCase -&gt; Controller,但是,例如,您可以将控制器实现为一组简单的函数,也许您不需要到处都使用泛型类型。

【讨论】:

非常感谢,有史以来最好的答案,很难量化您的帮助。我试图在特征级别而不是方法级别传递泛型,但不知道 PhantomData 强制使用泛型参数。 关于依赖方向:程序的核心是模型User。在外层我们有GetUsersUseCaseIUserRepository,这取决于它。在外层,在一个边缘,我们有 MySQLUserRepository 取决于 IUserRepository 和实体 User。同样在外层,在另一边,我们有GetUsersController,它依赖于IUserRepositoryIUserUseCase,都在内层中。所以依赖方向是正确的AFAIK。 这是一个描述依赖方向的图表:i.stack.imgur.com/1gHka.jpg 我明白你在做什么,事情是这样的: 1. 控制器不必具有特征。它在您的图表上不存在,或者您不会在 Clean Architecture.2 中找到它。控制器可以是一个简单的功能。 3.“获取用户控制器”?控制器不执行这么狭隘的功能,它应该只是“控制器”。 4.问题始于控制器的特征:删除它。 5. 我怀疑 HTTP -> Controller 流程​​,因为 Clean Architecture 显示 HTTP -> UI/Presenter 流程​​,而控制器则搁置一旁。

以上是关于具有多个结构的泛型和依赖倒置的主要内容,如果未能解决你的问题,请参考以下文章

依赖倒置原则(DIP)

七大设计原则之依赖倒置原则应用

手撸golang 架构设计原则 依赖倒置原则

架构案例-依赖倒置循环依赖解耦

面向对象设计原则四:依赖倒置原则

敏捷开发_依赖导致原则