静态类到依赖项注入 [英] Static class to dependency injection

查看:52
本文介绍了静态类到依赖项注入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我要开始使用的应用程序使用MVVM.这篇文章的最大部分是对我尝试过的东西和工作的解释.问题在帖子的底部.此处使用的 Localizer 类仅作为示例,可以很容易地用另一个类替换.

我有一个 class库和一个 Localizer 类.此类的目的是即时更改应用程序的语言,而不必重新启动应用程序.本地化器必须先实例化,然后才能使用,但是一旦实例化,就应该在整个应用程序中使用.(该类使用应用程序资源来本地化应用程序.)

我想到的第一种方法是使 Localizer 成为具有 public static void Initialize 的 public static class .代码>方法.这样,我可以像这样初始化 Localizer

  Localizer.Initialize(/*此处需要的参数*/); 

在应用程序级别上,并在我想要的类库或此类应用程序中随处使用它

 字符串示例= Localizer.GetString(/*资源字典中的键*/); 

考虑到类库是由我编写的(只有我才有源代码),并且由对源代码一无所知的其他人(他们只知道类库可以做什么)使用,我必须明确声明在某种如何使用此类库"中,他们需要在应用程序级别调用 Localizer.Initialize 以便在其应用程序中的任何地方使用它.

进行了一些研究后,很多人指出这是一种不好的做法,并建议调查什么 依赖注入 (DI)和 Control (IoC),我做到了.我了解到DI与我的第一种方法大致相同,但是删除了静态内容,使用 Localizer.Initialize 作为构造函数,并将实例化的类注入到其他类中.

因此,第二种方法是依赖注入,这就是我要坚持的地方.我设法让我的应用程序使用一个带有以下代码的 MainWindowView MainWindowViewModel 进行编译:

 受保护的重写void OnStartup(StartupEventArgs e){ILocalizer localizer =新的Localizer(Current.Resources,System.Reflection.Assembly.GetExecutingAssembly().GetName().Name,"Languages","Language","en");var mainWindowViewModel = new MainWindowViewModel(localizer);var mainWindowView = new MainWindowView {DataContext = mainWindowViewModel};mainWindowView.Show();base.OnStartup(e);} 

以上代码的作用是将 localizer 注入到 MainWindowViewModel 中.这样,不会在后面的 MainWindowView 代码中添加任何其他代码,并且不会将视图绑定到视图模型.

MainWindowViewModel 中,构造函数类似于(请注意,消息框在其他地方被调用,但已将其移到此处以最小化代码):

  ILocalizer _localizer;公共MainWindowViewModel(ILocalizer本地化器){_localizer =本地化程序;MessageBox.Show(_localizer.GetString(/*资源字典中的键*/));} 

上面的代码仍然可以正常编译并正常运行.当我在我的类库中有 UserControls 的视图和视图模型也需要 localizer 实例时,就会出现问题.

我想我有一个解决方案,当我的应用程序程序集中有一个 UserControl 时,但感觉比使用一个 static类时更加复杂">.我通常只是将 UserControl 的视图模型绑定到后面代码中的视图.这样,我可以简单地将 UserControl 添加到我的.xaml代码中,例如< local:UserControl1//> ,而不会带来很多麻烦.这样,视图模型的父视图模型就不必关心子视图模型.

使用DI时,我将在我的父母中做类似的事情(孩子将与前面的代码块中的孩子相同):

查看

 < n:UserControl1 DataContext ="{Binding UC1ViewModel}"/> 

ViewModel

  public UserControl1ViewModel UC1ViewModel {get;放;}ILocalizer _localizer;公共MainWindowViewModel(ILocalizer本地化器){_localizer =本地化程序;UC1ViewModel =新的UserControl1ViewModel(localizer);} 

以上仍然一切正常,到目前为止没有问题.唯一更改的是在父视图中设置了 DataContext ,而在父视图模型中设置了 DataContext 的内容.

问题我的类库中还有几个 UserControls .这些可以由 class库的用户使用,但是他们不能更改它们.这些大多数 UserControls 是一些固定的 pages ,它们显示有关人,车等的信息.其目的是,例如,带有人名的标签为名称"(英语),"Naam"(荷兰语)等(它们都在视图中声明并且可以正常工作),但是后面的代码中也必须对文本进行本地化,这就是我要坚持的地方./p>

我应该像在应用程序程序集中使用 UserControl 一样处理该问题吗?如果说在单个父视图中使用了20个以上的 UserControls ,这真的会适得其反.

我也觉得我没有正确实施100%DI.

解决方案

问题

DI并不像您看起来的那么简单.有一些DI框架可以解决DI问题,它们是成熟的软件.

由于DI 应该的工作方式,因此如果不设计DI容器就无法真正自己做DI

DI解决了一些问题,其中几个主要问题是:

  • IoC-通过将分辨率和相关性的提供移出组件类之外,确保组件之间不紧密耦合

  • 生命周期范围-确保组件具有明确定义的生命周期/生命周期,并在应用程序的关键点正确地实例化和处置这些组件

看起来如何?

您甚至都不应该看到容器!-您应该只看到组件的依赖关系,其余的看起来都像魔术一样……

DI容器应该非常透明.您的组件和服务应仅通过指定其依赖项(在其构造函数中)来要求它们的依赖项

我当前有什么问题?

您不需要使用以下代码手动连接子依赖项:

 公共MainWindowViewModel(ILocalizer本地化器){_localizer =本地化程序;UC1ViewModel =新的UserControl1ViewModel(localizer);//<-哎呀} 

上述内容存在许多问题:

  1. 您正在使 MainWindowViewModel 负责创建 UC1ViewModel 并管理对象的生命周期(这并非总是一件坏事,因为有时您希望这样做以管理特定组件中对象的生命周期)

  2. 您正在将 MainWindowViewModel 的实现与 UserControl1ViewModel 的构造函数实现耦合-如果您在 UserControl1ViewModel 中需要另一个依赖项,突然之间,您必须更新 MainWindowViewModel 来注入该依赖关系,并提示很多重构.这是因为您要自己实例化类型,而不是让容器来实例化.

容器如何防止上述代码?

使用任何容器,您应该注册组件

容器将跟踪可能的组件和服务的列表,并使用此注册表来解决依赖关系.

它还跟踪依赖项的生命周期(单例,实例化等)

好,我已经注册了所有内容,接下来要做什么?

一旦注册了所有依赖性,就可以从容器中解析根组件.这称为合成根,应该是应用程序的入口点"(通常是主视图或主方法).

容器应负责连接并为源自该合成根的一切创建依赖项.

示例:

(伪代码)

 公共类ApplicationBootstrapper{私有IContainer _container;公共ApplicationBootstrapper(){_container = new SomeDIContainer();_container.Register< SomeComponent>().AsSingleton();//Singleton实例,每个解析都使用相同的实例_container.Register< SomeOtherComponent>().AsTransient();//每个解析有新实例//...您所有组件的更多注册码//大多数容器都有基于约定的注册//系统,例如_container.Register().Classes().BasedOn< ViewModelBase>等等var appRoot = _container.Resolve< MainWindowViewModel>();appRoot.ShowWindow();}} 

现在,当您的应用程序运行时,所有依赖项都会注入到根目录中,而根目录中的所有依赖关系都将依此类推

您的 MainWindowViewModel 然后可以这样指定对UC的依赖关系:

 公共MainWindowViewModel(UC1ViewModel vm){} 

注意 MainWindowViewModel 如何不再需要 ILocalizer 实例,它将为您解析并注入到 UC1ViewModel 中(当然,除非如此)你需要它).

要注意的点

  • 您不应传递容器的实例.如果您在应用程序启动期间以外的任何地方都在应用程序代码中引用了该容器,则可能是做错了什么

  • 依赖关系的延迟解析通常是通过工厂(专门为代表您的组件从容器中解析而设计的类型)实现的.应该将工厂注入到组件中,然后组件可以调用工厂以获取所需的实例.这还允许您将参数传递给依赖项.

  • 使用SOLID原理,取决于抽象而非具体的类.这样,如果您决定更改某些工作方式,则换出组件要容易得多(您只需更改注册代码以使用实现相同接口的其他具体类,瞧,无需重构应用程序即可)

其他

这绝不是DI的简洁视图,有很多要考虑的地方,但是希望它可以帮助您入门.正如史蒂文(Steven)所述,如果您打算重新分发该库,则应该阅读最佳实践.

dos/dont's 上的原始帖子在这里:

依赖项注入(DI)为友好";库

您应该使用哪个DI容器?

世界就是你的牡蛎.我是温莎城堡(Castle Windsor)的粉丝-它并不是最快的(我想不出我写过一个我需要组件分辨率才能快速实现忍者速度的应用...),但是它确实具有完整功能./p>

更新:我没有真正解决的几个非查询

插件

城堡Windsor具有内置的插件功能-因此,您可以将DLL放入应用程序目录中,从而通过向容器注册组件来为应用程序添加功能.不知道这是否适用于您的UC类库(您可以使应用程序依赖它,除非它实际上需要是一个插件)

其他内容

也有很多MVVM框架,它们在视图/视图模型解析上采用了几种不同的方法(视图模型优先,视图优先,混合方法).

如果您还没有使用某个应用程序(听起来并不像您一样),则可能要考虑使用其中一种来指导您构建应用程序.

The application I'm trying to get to work uses MVVM. The biggest part of this post is an explanation of what I tried and what I got working. The question is near the bottom of the post. The used Localizer class is only used here as an example and can easily be replaced with another class.

I have a class library with a Localizer class. This purpose of this class is to change the language of the application on the fly, without having to restart the application. The `Localizer has to be instantiated before it can be used but once instantiated, should be usable in the entire application. (The class uses the application resources to localize the application.)

My first approach I could think of is making the Localizer a public static class with a public static void Initialize method. This way I could initialize the Localizer like this

Localizer.Initialize(/* Needed arguments here */);

on the application level and use it wherever I want in either my class library or application like this

string example = Localizer.GetString(/* A key in the resource dictionary */);

Considering the class library is written by me (only I have the source code) and used by other people who have no clue about the source code (they only know what the class library can do), I would have to explicitly state in some sort of "How to use this class library" that they need to call Localizer.Initialize on the application level in order to use it everywhere in their application.

After doing some research a lot of people state that this is a bad practice and suggest investigating what Dependency Injection (DI) and Inversion of Control (IoC), so I did. I learned that DI is doing about the same as my first approach but remove the static stuff, use Localizer.Initialize as the constructor and inject the instantiated class in my other classes.

So the second approach is dependency injection and this is where I'm stuck. I managed to let my application compile with a single MainWindowView and MainWindowViewModel with the following code:

protected override void OnStartup(StartupEventArgs e)
{
    ILocalizer localizer = new Localizer(Current.Resources, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, "Languages", "Language", "en");

    var mainWindowViewModel = new MainWindowViewModel(localizer);

    var mainWindowView = new MainWindowView { DataContext = mainWindowViewModel };

    mainWindowView.Show();

    base.OnStartup(e);
}

What the above code does is, is inject the localizer into MainWindowViewModel. This way no additional code is added to the MainWindowView code behind and has the view a view model bound to.

In MainWindowViewModel the constructor is like this (note that the message box is called somewhere else but was moved here to minimize the code):

ILocalizer _localizer;

public MainWindowViewModel( ILocalizer localizer)
{
    _localizer = localizer;

    MessageBox.Show(_localizer.GetString(/* A key in the resource dictionary */));
}

The above code is still compiling and running fine without exceptions. The problem occurs when I have either UserControls in my class library with a view and view model that also require the localizer instance.

I think I have a solution for when I have a UserControl in my application assembly but it feels like it is more 'complex' then when I would use a static class. I usually just bind the view model of a UserControl to the view in its code behind. This way I can simply add the UserControl to my .xaml code like this <local:UserControl1 /> without a lot of extra hustle. This way the view model parent view model doesn't have to be concerned about child view models.

With DI I would do something like this in my parent (the child would be the same as in the previous block of code):

View

<n:UserControl1 DataContext="{Binding UC1ViewModel}" />

ViewModel

public UserControl1ViewModel UC1ViewModel { get; set; }
ILocalizer _localizer;

public MainWindowViewModel(ILocalizer localizer)
{
    _localizer = localizer;
    UC1ViewModel  = new UserControl1ViewModel(localizer);
}

The above is still all working fine, no problems so far. The only thing that changed is that the DataContext is set in the parents view and the content of the DataContext is set in the view model of the parent.

The question I also have several UserControls in my class library. These can be used by the users of the class library but they can't alter them. Most of these UserControls are some fixed pages that display information about a person, car, etc. The intention is that, for example the label with the name of the person is "Name" in English, "Naam" in Dutch, etc. (which are all declared in the view and are working fine) but there is also text in the code behind that has to be localized and this is where I'm stuck.

Should I approach the problem the same way as I'm doing with a UserControl in my applications assembly? This feels really counterproductive if let say 20+ of these UserControls are used in a single parent view.

I also feel that I'm not implementing DI 100% correctly.

解决方案

Problem

DI is not as simple as you've made it look. There are DI frameworks out there which take care of the DI concern, and they are mature pieces of software.

You can't really do DI yourself without designing a DI container because of the way DI should work

DI solves a few problems, a couple of the main ones are:

  • IoC - ensuring that components aren't tightly coupled by moving the resolution and provision of dependencies outside of the component classes

  • Lifetime scope - ensures that components have a well defined lifetime/lifecycle and that they are correctly instantiated and disposed of at key points in your application

How does it look?

You shouldn't even see the container! - you should only see components dependencies and the rest should look like magic...

DI containers should be very transparent. Your components and services should require their dependencies simply by specifying what the dependencies are (in their constructors)

What's my current problem?

You don't want to be having to manually wire up sub-dependencies with code like this:

public MainWindowViewModel(ILocalizer localizer)
{
    _localizer = localizer;
    UC1ViewModel  = new UserControl1ViewModel(localizer); // <-- ouch
}

There are a number of problems with the above:

  1. You are making the MainWindowViewModel responsible for creating the UC1ViewModel and managing the lifetime of the object (this isn't always a bad thing as sometimes you want to manage the lifetime of an object in a particular component)

  2. You are coupling the implementation of the MainWindowViewModel to the constructor implementation of UserControl1ViewModel - if you require another dependency in UserControl1ViewModel, suddenly you have to update MainWindowViewModel to inject that dependency, cue a lot of refactoring. This is because you are instantiating the type yourself instead of letting a container do it.

How do containers prevent code like the above?

With any container you should be registering components

The container will track the list of possible components and services and use this registry to resolve dependencies.

It also tracks the dependencies lifecycle (singleton, instanced etc)

Ok I've registered everything, what next?

Once you have registered all your dependencies, you then resolve your root component from the container. This is known as the composition root and should be the 'entry point' for your application (usually the main view or main method).

The container should take care of wiring up and creating the dependencies for everything that stems from that composition root.

Example:

(Pseudo code)

public class ApplicationBootstrapper
{
    private IContainer _container;

    public ApplicationBootstrapper() {
        _container = new SomeDIContainer();

        _container.Register<SomeComponent>().AsSingleton(); // Singleton instance, same instance for every resolve
        _container.Register<SomeOtherComponent>().AsTransient(); // New instance per resolve
        // ... more registration code for all your components
        // most containers have a convention based registration
        // system e.g. _container.Register().Classes().BasedOn<ViewModelBase> etc

        var appRoot = _container.Resolve<MainWindowViewModel>();
        appRoot.ShowWindow();
    }
}

Now when your application runs, all dependencies are injected into the root and all dependencies of the root and so on

Your MainWindowViewModel could then specify a dependency on the UC as such:

public MainWindowViewModel(UC1ViewModel vm)
{
}

Notice how the MainWindowViewModel no longer needs an ILocalizer instance, it will be resolved and injected into the UC1ViewModel for you (unless of course you need it).

Couple of points to note

  • You should not pass an instance of the container around. If you are referencing the container in your application code anywhere other than during application startup you are probably doing something wrong

  • Deferred resolution of dependencies is usually achieved with factories (types that are designed specifically to resolve from the container on behalf of your components). The factory should be injected into the component and the component can then call the factory to get the instance it needs. This also allows you to pass arguments to the dependency.

  • Use SOLID principles, depend on abstractions not concrete classes. This way it's much easier to swap out components if you decide to change the way something works (you just change the registration code to use a different concrete class that implements the same interface, et voila, no refactoring the app)

Anything else

This is by no means a concise view of DI, there is a lot to consider, but hopefully it will get you started. As Steven mentioned, if you are planning on redistributing the library you should read up on best practices.

The original post on dos/dont's is here:

Dependency Inject (DI) "friendly" library

Which DI container should you use?

The world is your oyster. I'm a fan of Castle Windsor - it's not the fastest (I can't think of an app I've written where I've ever needed component resolution to be ninja fast...), but it's certainly fully featured.

Update: couple of non queries I didn't really address

Plugins

Castle Windsor has plugin capabilities built in - so you can drop a DLL into your application directory which adds functionality to your application by registering components with the container. Not sure if this applies to your UC class library or not (you could just make the app depend on it unless it needs to actually be a plugin)

Other stuff

There are also quite a lot of MVVM frameworks with several different approaches on view/viewmodel resolution (viewmodel-first, view-first, hybrid approaches).

You may want to consider using one of these to help guide you in structuring your application if you are not already using one (it doesn't sound like you are).

这篇关于静态类到依赖项注入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆