简单注入器在 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
)。
如果您将它用于某种身份验证,请考虑使用装饰器包装他们需要获得执行权限的任务。
IsBusyIndicator
再次强调:每个 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,它正在咬你,它会咬你越来越难!
我有一个 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
)。
如果您将它用于某种身份验证,请考虑使用装饰器包装他们需要获得执行权限的任务。
IsBusyIndicator
再次强调:每个 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,它正在咬你,它会咬你越来越难!