关于依赖注入的基础概念,我就不啰嗦太多了,咱们直接从咱们写代码时最头疼的痛点说起,这样大家更容易理解为什么我们需要 Prism 里的 IoC 容器。(注:本篇文章由AI结合个人文章优化而成,个人确认内容无误,如有错误处请联系指出,谢谢)
从新手的写法,到我们遇到的问题
刚学 C# 的时候,我写代码的思路特别直接:哪里要用服务,我就直接new一个实例出来不就行了?比如我要调用用户服务,就var service = new UserService();,要调用日志服务,就var logger = new FileLogger();。
但是写着写着就发现不对了:比如我的客户端类(比如 WPF 里的 ViewModel,这是咱们的高层模块)要用到好几个服务,那我就要在 ViewModel 里把这些服务都new一遍。这时候问题就来了:如果我要换服务的实现怎么办?
比如原来的日志服务是写本地文件的,现在产品说要改成上报到服务器,那我就要把所有new FileLogger()的地方都改成new RemoteLogger()。如果这个日志服务在 10 个 ViewModel 里都用到了,那我就要改 10 次代码,改完还要一个个检查有没有漏改,万一漏了一个,线上出问题都看不到日志,这不纯纯给自己找事吗?
这就是典型的高层模块依赖低层模块—— 我改个低层的服务实现,高层的调用代码全都要跟着改,太麻烦了。
怎么解决?从 DIP 原则说起
那怎么解决这个问题?咱们换个思路:高层模块不要直接依赖具体的服务类,而是依赖一个抽象的接口。所有的服务都实现这个接口,高层模块只调用接口里的方法,根本不管你这个接口具体是哪个类实现的。
比如我定义个ILogger接口,里面有个Log方法,然后FileLogger和RemoteLogger都实现这个接口。那我的 ViewModel 里就只用到ILogger,不管你是本地还是远程,我只要调用_logger.Log()就行。
这样一来,不管我怎么改服务的实现,ViewModel 的代码一行都不用动!这就是咱们常说的DIP 依赖倒置原则:高层不依赖低层,两者都依赖抽象。
理清楚:DIP、DI、IoC、容器,到底都是啥?
说到这,很多人会搞混这几个词,我给大家用大白话理清楚,别再搞混了:
-
DIP:是咱们的指导思想,就是告诉我们 “要依赖抽象,不要依赖具体”,这是原则,是我们要达到的目标。
-
DI 依赖注入:是实现 DIP 的具体手段,就是把依赖的对象,从外面传给需要它的类,而不是类自己
new。比如 ViewModel 需要 ILogger,我就把 logger 通过构造函数传进去,这就是最常用的构造注入,除此之外 C# 原生还支持属性注入和接口注入,不过日常开发用构造注入就够了。 -
IoC 控制反转:是我们用了 DI 之后得到的结果。原来对象的创建是由我们自己控制的,我要什么就自己
new,现在反过来,把创建对象的控制权交给了别人,这就是 “控制反转”。 -
IoC 容器:就是帮我们自动做 DI 的工具!如果没有容器,我们就要手动把所有依赖都创建好,然后一个个传进去,有了容器,这些活它都帮我们干了。
如果你只是想了解 IoC 模式的基础理论,推荐你看看这篇入门文章:https://www.cnblogs.com/liuhaorain/p/3747470.html,讲得通俗易懂。今天这篇我们主要讲在 WPF 里用 Prism 的 IoC 容器,能给我们带来什么实际的好处。
手动依赖注入,到底有多麻烦?
咱们先看看,不用容器的话,手动做 DI 是个什么样子。
先看个简单的例子,假设我们有这么几个服务:
// 定义接口和实现类
public interface IA { }
public class A : IA { public A() { } }public interface IB { }
public class B : IB
{private IA _a;public B(IA a) { _a = a; }
}public interface IC { }
public class C : IC
{private IA _a;private IB _b;// C同时依赖A和Bpublic C(IA a, IB b) { _a = a; _b = b; }
}public class Client
{private IC _c;public Client(IC c) { _c = c; }public void DoSomething() { /* 使用_c */ }
}
你看,依赖关系其实已经有点绕了,那我们要使用这个 Client,手动管理依赖的话,要这么写:
// 必须严格按顺序创建,少一个都不行
IA a = new A();
IB b = new B(a);
IC c = new C(a, b);
Client client = new Client(c);
client.DoSomething();
这才 3 个依赖,你已经能感觉到有点麻烦了对吧?那如果依赖链更长呢?
咱们来个更贴近实际的长依赖链的例子:
// 模拟一个更长的依赖链:A ← B ← C ← D ← E ← F ← CanService
public interface IA { }
public class A : IA { public A() { } }public interface IB { }
public class B : IB
{private IA _a;public B(IA a) { _a = a; }
}public interface IC { }
public class C : IC
{private IA _a;private IB _b;public C(IA a, IB b) { _a = a; _b = b; }
}public interface ID { }
public class D : ID
{private IC _c;public D(IC c) { _c = c; }
}public interface IE { }
public class E : IE
{private ID _d;public E(ID d) { _d = d; }
}public interface IF { }
public class F : IF
{private IE _e;public F(IE e) { _e = e; }
}// 我们真正要用的业务服务
public interface ICanService
{void DoWork();
}public class CanService : ICanService
{private IF _f;public CanService(IF f) { _f = f; }public void DoWork() { Console.WriteLine("完成了服务"); }
}
这时候,如果我们还是手动管理依赖,要怎么用这个CanService?
// 手动创建所有依赖,顺序不能错,一个都不能漏
IA a = new A();
IB b = new B(a);
IC c = new C(a, b);
ID d = new D(c);
IE e = new E(d);
IF f = new F(e);
ICanService service = new CanService(f);// 最后才能把service传给ViewModel
MainViewModel vm = new MainViewModel(service);
你看看这代码,是不是看着就头大?这才 6 个依赖,要是以后再加个 G、H,那这行代码是不是要写得更长?
而且如果哪天B的依赖变了,比如B现在需要一个IE的依赖了,那你整个创建顺序都要改,所有用到B的地方都要改代码,稍不注意就出错。
这还只是一个服务,如果我们有十几个这样的服务,那手动管理依赖简直是噩梦。
Prism 的 IoC 容器:帮你搞定所有麻烦
这时候 Prism 的 IoC 容器就该出场了!有了它,上面那一大串手动创建的代码,我们全都不用写了!
怎么用?其实特别简单,就两步:
第一步:把服务注册到容器里
这个注册一般放在App.xaml.cs里,因为这是 WPF 程序的入口,注册完整个程序都能用。
首先我们的 App 类要继承PrismApplication,这是 Prism 给我们的基础类,然后重写RegisterTypes方法,在这里把所有的服务都注册进去:
public partial class App : PrismApplication
{protected override void RegisterTypes(IContainerRegistry containerRegistry){// 告诉容器:当别人要IA的时候,你给我一个A的实例,而且整个程序只有一个(单例)containerRegistry.RegisterSingleton<IA, A>();containerRegistry.RegisterSingleton<IB, B>();containerRegistry.RegisterSingleton<IC, C>();containerRegistry.RegisterSingleton<ID, D>();containerRegistry.RegisterSingleton<IE, E>();containerRegistry.RegisterSingleton<IF, F>();containerRegistry.RegisterSingleton<ICanService, CanService>();}
}
这里的RegisterSingleton就是说,这个服务整个程序里只有一个实例,不管谁来拿,都是同一个,这样可以节省资源,也方便共享状态。如果你需要每次用的时候都创建一个新的,也可以用RegisterTransient,这个看你的需求。
你看,我们就注册了这么几行,剩下的事容器都帮我们干了。它会自动帮我们分析每个服务的依赖,谁需要谁,它都门清,不用我们管。
很多人会问,Prism 底层用的是什么容器?老版本用 Unity,新版本默认用 DryIoc,不过对我们来说完全不用管!我们用的是 Prism 给我们的统一的
IContainerRegistry接口,不管底层用什么容器,我们的注册代码都是一样的,不用改。
第二步:用的时候,直接声明你需要什么
注册完了,我们用的时候就更简单了!比如我们在 ViewModel 里要用CanService,只要在构造函数里声明你需要什么就行了:
public class MainViewModel
{private ICanService _server;// 你只要说:我需要一个ICanServicepublic MainViewModel(ICanService server){_server = server;// 直接用就行!容器已经帮你把所有A、B、C、D、E、F都创建好了_server.DoWork();}
}
就这么简单?对!就这么简单!
你再也不用管那些乱七八糟的依赖顺序了,只要告诉容器我要什么,容器就会把所有东西都准备好给我送过来。就像你去餐厅吃饭,你只要告诉服务员你要吃什么菜,不用自己去菜市场买买菜、洗洗菜、炒菜,厨师都帮你做好了端上来,你直接吃就行。
一个更贴近日常的例子:日志服务
说到这,可能还有人没太有感觉,咱们再举个咱们日常开发天天都会碰到的例子,大家就秒懂了:
我们有个日志服务,原来我们是写本地文件的,那如果不用容器的话,我们的代码里到处都是var logger = new FileLogger();。
后来产品说,我们要把错误日志上报到服务器,方便线上排查问题,那我们就要把所有的new FileLogger()都改成new RemoteLogger(),如果有 20 个 ViewModel 用到了日志,那就要改 20 次,改完还要一个个检查有没有漏,太麻烦了。
但是用了容器之后呢?
-
我们定义个
ILogger接口,然后FileLogger和RemoteLogger都实现这个接口 -
注册的时候,我们这么写:
// 原来的本地日志
containerRegistry.RegisterSingleton<ILogger, FileLogger>();
- 所有的 ViewModel 里,只要这么写:
public class UserViewModel(ILogger logger)
{private readonly ILogger _logger = logger;// 业务代码里直接用_logger.Log()就行,不管它是本地还是远程
}
那当我们要改成远程日志的时候,只要改注册的那一行:
// 就改这一行!所有地方的日志都变成远程的了
containerRegistry.RegisterSingleton<ILogger, RemoteLogger>();
就这一行!所有 20 个 ViewModel 的日志自动就都变成远程的了,不用改任何一行业务代码,也不用担心漏改,是不是爽翻了?
用了 IoC 容器,我们到底得到了什么?
总结一下,IoC 容器给我们带来的好处,真的太多了:
-
不用再手动管依赖了:再也不用纠结先 new A 还是先 new B,容器自动帮你搞定所有依赖的创建顺序,再也不会因为依赖顺序错了而出错。
-
改服务实现太简单了:只要改注册的那一行,所有用到的地方自动生效,不用改业务代码,也不用担心漏改。
-
代码更干净:ViewModel 里只要声明我需要什么服务,不用管这个服务怎么来的,你专注写你的业务逻辑就行了,其他的交给容器。
-
方便管理对象生命周期:是要整个程序共用一个实例,还是每次用都新创建一个,注册的时候一句话就搞定,不用自己管。
-
解耦!解耦!解耦!:重要的事说三遍,高层和低层完全解耦,低层怎么改,高层都不用动,代码的可维护性直接拉满。
新手容易踩的两个坑
最后给刚接触的朋友提两个小提醒,别踩坑:
-
优先用构造函数注入:这是 Prism 最推荐的方式,最稳定,也最清晰,别人一看你的构造函数就知道你这个类依赖了什么服务,一目了然。
-
不要手动去 Resolve 服务:很多新手刚学的时候,会忍不住去容器里自己拿服务,比如
Container.Resolve<ILogger>(),这其实就是服务定位器模式,会破坏依赖注入的解耦,尽量不要这么做,老老实实通过构造函数声明依赖就好。
最后
其实 IoC 容器说复杂也复杂,说简单也简单,核心就是一句话:不要自己 new,告诉容器你需要什么,容器给你准备好。
用熟了之后,你会发现写代码的效率提升了太多,再也不用跟那些乱七八糟的依赖较劲了。
如果大家想深入了解 IoC 和 DI 的原理,可以看看这篇深入的文章:https://www.cnblogs.com/fuchongjundream/p/3873073.html
希望这篇文章能帮到刚接触 Prism 的朋友,也欢迎大家一起交流讨论~
