简单注入器在 BaseClass 中注入多个依赖项

Simple Injector inject multiple dependency in BaseClass

我有一个 BaseViewModel 被多个 ViewModel class 继承。在我的 BaseViewModel 中,我有几个从 ViewModel 注入的依赖项。现在,如果我需要在 BaseViewModel 中添加新的依赖项,我需要更改所有继承 BaseViewModel 的 VM。请让我知道如何在 Simple Injector 中处理它。以下是我的代码结构:

如何使我的基础 class 注入独立,这样我就不需要对所有继承的 class 进行更改?

代码:

public class BaseViewModel
{
    protected readonly IAESEnDecrypt AESEnDecrypt;
    protected readonly IDataService DataService;
    protected readonly INavigationService NavigateToPage;
    public BaseViewModel(INavigationService nav, IDataService data, IAESEnDecrypt encrypt)
    {
        AESEnDecrypt= encrypt;
        NavigateToPage = nav;
        DataService = data;
    }
}


public class ViewModel
{
   public ViewModel(INavigationService nav, IDataService data, IAESEnDecrypt encrypt) : base (nav, data, encrypt)
   {

   }
}

我的 BaseViewModel 包含以下一些接口,其实现是通过构造函数注入的:

- NavigationService
- DataService
- GeoLocationService
- SmartDispatcher
- MessageBus which implement Message Aggregator

它还包含一些公共属性作为静态变量,其数据在整个应用程序中使用,如 UserDetails。并且还包含了CancellationToken,IsBusy来显示进度条。

BaseViewModel 还包含处理来自所有 ViewModel 的所有传入异常的 HandleException 方法。 还包含一些在所有视图中使用的常用命令,如 Si goutCommand、NavigationBar 命令。

实际上它已经开始包含各种ViewModel之间使用的各种常用方法。

请建议我如何重构此代码?

您可以使用ServiceLocator(反)模式使注入独立,但是您不应该这样做,因为它违反了原则固体 Mark Seemann - Service Locator violates SOLID

您应该坚持在构造函数中添加依赖项,因为这符合 SOLID OO 设计原则。

首先阻止拥有这个基地 class。这个 base class 是一个很大的代码味道,结果就是你现在的痛苦。这样的基础 class 将违反单一职责原则 (SRP),并且只会充当所有派生视图模型的大帮手 class,甚至看起来您正在将横切关注点放在那里.基础 class 甚至可能隐藏您的视图模型违反 SRP 的事实。他们可能做得太多了;责任太多了。

相反,请尝试执行以下操作:

  • 将横切关注点从基础 class 移到装饰器中,或者找到另一种方法来应用横切关注点。
  • 将相关的依赖项组合到一个 aggregate service 中,并将此类聚合服务注入到您的视图模型中。

在一个设计良好的应用程序中,几乎不需要具有依赖性的基础class。

如果您无法更改您的设计(但请务必看一下这个;如果没有该基础,您会处在一个更好的位置 class),您可以恢复为显式 属性注射。 Simple Injector 不会开箱即用,但文档 describes 如何做到这一点。

基本上,它归结为编写自定义 IPropertySelectionBehavior,将 BaseViewModel 的构造函数依赖项移动到 public 属性并使用自定义属性标记它们。

但同样,只有在万不得已时才使用 属性 注入。 属性注入只会隐藏设计问题;它不会解决它。

你的最后一句话:

Actually it has started to contain all kinds of common methods used among various ViewModel

准确描述了您的问题!正如 Steven 已经描述的那样,您正在通过单个基础 class 构建几乎完整的应用程序。从而侵犯了您现在正在经历的Open-Closed principle

诀窍是围绕非常小的 SOLID ViewModels 设计您的应用程序,您可以在运行时使用这些模型编写应用程序。通过拆分 ViewModel 并使用 UserControl 作为您的视图,您可以为用户构建复杂的大型视图,同时您仍然可以从使用 SOLID 设计中获得所有好处。因此,让我们来看看您实现的一些不同接口以及您在基础中“处理”的一些功能 class:

导航服务

这听起来像是一种控制应用程序流程的服务。这听起来像你的主视图(模型)。您可以创建一个 MainViewModel 作为单个 属性,比方说 CurrentView。假设您使用的是 WPF,您通常会将此 属性 绑定到 ContentControl。此控件的内容可以是从单个 TextBlock 到完整的 UserControl 的所有内容。用户控件仍然可能非常复杂,因为它们可能由多个子用户控件等组成。使用 MVVM 框架(例如 Caliburn Micro or MVVM Light)是可选的,但会派上用场。

它也可以是一个应用程序全局服务,具有某种回调或委托函数来执行到特定视图(模型)的导航。在任何情况下,它都是您应用程序的基础结构部分,值得拥有 class,不应放在基础 class.

数据服务

单一数据服务是我工作 10 多年的方式。每次我把头撞到墙上。有时您需要一些特殊的东西,但您的数据服务中没有包含这些东西,您可能会检查完整的代码库以进行正确的调整。说到开闭原则……

然后我了解了 Command/Handler 和 Query/Handler 模式。您可以阅读此 here and here。在任何需要数据的地方使用此模式,您只需注入正确的 IQueryHandler<,> 并在那里使用它。并非每个视图(模型)都需要数据,当然也不是相同的数据。那么为什么要使用全局数据服务呢?这也将改善您对 DBContext 对象的生命周期管理。

HandleException

为什么你的基础 class 负责处理你的视图模型的异常? base class 对这个异常了解多少?基数 class 有什么作用?记录异常,向用户显示一条消息(什么样的消息?)然后默默地继续?让应用程序在 3 分钟后崩溃并让用户不知道发生了什么? I.M.O。如果您一开始就没想到会抛出异常,则不应捕获异常。比在应用程序级别记录异常(例如在您的 Main 中),向用户显示“Excuse me”消息并关闭应用程序。如果您预计会出现异常,请立即处理它并根据情况进行处理。

用户详细信息

问问自己,您的 40 个 ViewModel 中有多少实际需要此信息?如果所有 40 个都需要此信息,则说明您的设计存在其他问题。如果没有,只在实际使用它们的 ViewModels 中注入这些细节(或者更好的 IUserContext)。

如果您将它用于某种身份验证,请考虑使用装饰器包装他们需要获得执行权限的任务。

IsBusyIndi​​cator

再次强调:每个 ViewModel 都需要这个吗?我想不是。此外,我认为,向用户显示忙碌指示器是 View 的责任,而不是 ViewModel 的责任,并且由于任务的长度决定了您是否需要显示它,因此将其作为任务的责任(假设您正在查看通过使用例如已经提到的 Command/Handler 模式),您的任务也以 SOLID 方式完成。

使用 WPF,您可以定义一个可以绑定到视图的 Dependency Property,从而显示某种忙碌指示器。现在只要在需要显示的任务中注入一个ShowBusyIndicatorService即可。或者将所有(冗长的)任务包装在 ShowBusyIndicatorDecorator 中。

设计

现在让我们看看您可以定义的一些简单接口来构建您的视图(模型)。假设我们决定让每个 ViewModel 负责一个任务,并且我们定义了以下(典型的 LoB)任务:

  • 显示(任何类型的)数据
  • Select 或选择数据
  • 编辑数据

单个任务可以简化为“显示单个数据类型(实体)的数据”。现在我们可以定义如下接口:

  • IView<TEntity>
  • ISelect<TEntity>
  • IEdit<TEntity>

对于每种界面类型,您将根据您的语义偏好创建一个 Processor/Service 或 DialogHandler,这将执行典型的 MVVM 操作,例如找到相应的视图并将其绑定到视图模型并以某种方式显示(a模态 window,将其作为用户控件注入某些内容控件等)。

通过在您需要导航或显示不同视图的“父”ViewModel 中注入这个 Processor/Service 或 DialogHandler,您可以通过一行代码显示任何类型的实体并转移责任到下一个 ViewModel。

我现在在一个项目中使用这 3 个界面,我真的可以做我过去能做的一切,但现在是 SOLID 时尚。我的 EditProcessor、界面和视图模型看起来像这样,从所有不那么有趣的东西中剥离出来。我正在使用 Caliburn Micro 进行 ViewModel-View 绑定。

public class EditEntityProcessor : IEditEntityProcessor
{
    private readonly Container container;
    private readonly IWindowManager windowManager;

    public EditEntityProcessor(Container container, IWindowManager windowManager)
    {
        this.container = container;
        this.windowManager = windowManager;
    }

    public void EditEntity<TEntity>(TEntity entity) where TEntity : class
    {
        // Compose type
        var editEntityViewModelType = 
        typeof(IEntityEditorViewModel<>).MakeGenericType(entity.GetType());

        // Ask S.I. for the corresponding ViewModel, 
        // which is responsible for editing this type of entity
        var editEntityViewModel = (IEntityEditorViewModel<TEntity>)
                 this.container.GetInstance(editEntityViewModelType);

        // give the viewmodel the entity to be edited
        editEntityViewModel.EditThisEntity(entity);

        // Let caliburn find the view and show it to the user
        this.windowManager.ShowDialog(editEntityViewModel);
    }
}

public interface IEntityEditorViewModel<TEntity> where TEntity : class
{
    void EditThisEntity(TEntity entity);
}

public class EditUserViewModel : IEntityEditorViewModel<User>
{
    public EditUserViewModel(
        ICommandHandler<SaveUserCommand> saveUserCommandHandler,
        IQueryHandler<GetUserByIdQuery, User> loadUserQueryHandler)

    {
        this.saveUserCommandHandler = saveUserCommandHandler;
        this.loadUserQueryHandler = loadUserQueryHandler;
    }

    public void EditThisEntity(User entity)
    {
        // load a fresh copy from the database
        this.User = this.loadUserQueryHandler.Handle(new GetUserByIdQuery(entity.Id));
    }

    // Bind a button to this method
    public void EndEdit()
    {
        // Save the edited user to the database
        this.saveUserCommandHandler.Handle(new SaveUserCommand(this.User));
    }

    //Bind different controls (TextBoxes or something) to the properties of the user
    public User User { get; set; }
}

IView<User> 现在可以使用这行代码编辑当前选定的用户:

// Assuming this property is present in IView<User>
public User CurrentSelectedUser { get; set; }

public void EditUser()
{    
    this.editService.EditEntity(this.CurrentSelectedUser);
}

请注意,通过使用此设计,您可以将 ViewModel 包装在装饰器中以执行横切关注点,例如日志记录、身份验证等。

所以这是一个长答案,一个简短的答案是:松开底座 class,它正在咬你,它会咬你越来越难!