【DNT精英论坛回顾】.NET依赖注入在区块链项目aelf中的实践

2019年9月8日,DNT精英论坛(暨.NET北京俱乐部)第2期在北京微软大厦成功举办。论坛由资深.NET专家和社区活跃用户发起,以“分享、成长、合做、双赢”为原则,旨在打造一个领先的技术分享平台和成长交流生态。html

本次活动邀请到了红帽开放创新实验室高级咨询顾问陈计节、52ABP.COM 站长梁桐铭、aelf高级技术经理赵奕旗,三人分别就《.NET Core 云原生和 DevOps 实践》、《ASP.NET Core 和EF Core 3.0中的亮点和变化》、《.NET依赖注入在区块链项目aelf中的实践》三个话题进行了分享。git

aelf高级技术经理赵奕旗结合aelf的设计理念和BCL应用DI的实例,从什么是依赖注入、设计模式五大原则(SOLID)、DI,IoC,DIP的区别、DI的三个维度这四个方面对.NET依赖注入在aelf中的实践进行了详细的分享和解读。github

如下为赵奕旗分享内容回顾:面试

什么是依赖注入

什么是依赖注入?做为开发者,咱们对依赖这个名词都有概念,当咱们手头作的程序须要用到一个第三方类库时,咱们就能够把这个类库称为依赖。可是注入呢?spring

常言道,看一我的的身价,要看他的对手。依赖注入的对手是谁?依赖查找,Dependency Lookup。依赖注入和依赖查找都是实现IoC,也就是控制反转的方式之一。编程

估计不少人跟我刚开始同样不理解控制反转这个名词,我说一点本身的想法,这个控制指的是对依赖的控制,自己好比我在写一段代码,写着写着突然发现一个功能的实现依赖于其余的模块或者类库,基于面向对象的角度考虑,可能我想立马new一个对象出来,在哪里跌倒,就在哪里爬起来,在哪里碰到阻碍,就在哪里new出来一个依赖,开始借助这个依赖继续实现业务逻辑。可是这样很差,专业点讲这样会提高代码的耦合,致使代码难以维护。就是new这个操做很差,由于一旦当场建立实例,就会在如今写代码的这个类里产生一个易变的依赖。万一之后这个实例换了一个实现,就得返回来改代码,去new另外一个实现,以后我会说一些依赖注入的反模式。因此根据前辈的经验,最科学的作法应该是在哪里跌倒,就在哪里趴下,想办法不经过new来获取这个依赖。设计模式

前面说的两个方式,依赖注入,依赖查找,就是当你趴在原地找依赖的时候,能够用的手段。当你选择趴下的时候,获取依赖的控制权就反转了。安全

插一句,用过spring框架的确定据说过服务定位器,如今有不少前辈认为Service Locator,是一种反模式。其实Service Locator就是依赖查找的一个具体实现。虽说服务定位器和依赖注入自己的思想截然相反,MS.DI在实现依赖注入框架的时候,仍是用到了服务定位器。以后会看到。框架

若是没用过spring也没事。函数式编程

咱们尝试一下厘清两者的关系。显然在依赖注入里,注入这个词是有重要地位的,Stack Overflow上有一段关于依赖注入的回答颇有意思,问题是如何向一个五岁的孩子解释依赖注入:

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.

What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.

你想象你家的冰箱塞满了东西,而后一个五岁的小朋友去你家冰箱里翻吃的,你慌不慌?他可能最后忘了把冰箱门合上,可能找到一些不适合本身吃的东西,甚至会试图在冰箱里寻找xbox,那固然一无所得,你家的冰箱若是有意识,可能会想抛出一个空引用异常。

因此合适的作法是什么?去问一问小朋友须要什么,而后咱们来从冰箱里找东西,找出来交给他。

作个类比,小朋友从冰箱翻东西就相似于依赖查找,咱们做为成年人把东西提供给小朋友,就是依赖注入。

在展开依赖注入以前,我认为咱们有必要温习一下设计模式的五大原则(有些地方提了六大甚至七大,可是意思多少有重叠,这里就说五大原则),顺便看一下这五大原则和依赖注入的关系。 我估计不少人被SOLID面试题也折磨地不清,这里我分享下本身的见解,也算给之后群里讨论提供一些话题了。

SOLID

Single Responsibility Principle (SRP,单一职责原则)

There should never be more than one reason for a class to change.

你们可能据说过一个段子。

  • 你写过没有bug的代码吗?

  • 写过啊,Hello World。

这个段子告诉咱们一个道理:代码越简单,越不容易出bug。

怎么简单呢?让一段代码只作一件事情就行了。在实践中,咱们让一个类只作一件事情就行了。并且若是咱们可以给这个类起一个亲切又直观的名字,代码自己的自述性就会很好,我记得阿里写Java的规范就对命名有要求,好比若是某个类使用了某种设计模式,就要求把设计模式的名字体如今名字,这个时候注释都显得多余,并且容易让人类读懂的代码更好维护。

回到单一职责原则的描述:只能存在一个致使类变动的缘由。这个表达挺抽象的。碰到这种抽象的句子,我就会试图仿写例句。好比只能存在一个致使我变动的缘由。那可能我是一个没有感情的复读机,只有别人发的东西变了,我才能换一句复读。或者我是一个购物车,只有添加购物车的这我的发工资了,我这个购物车才会清一点东西。不知道你们能不能get个人意思。

还有一个判断一个类是否符合单一职责原则的标准,就是要看这个类里面元素之间的关联性,你不能一个实现复读机的类里出现一个写做文的功能,想都不要想,他最多关心一下剪贴板大小。话说回来,剪贴板自己也不该该包含在复读机这个类里,而是单独做为一个服务存在,能够考虑用仓储(Repository)模式。

关联性高意味着高内聚,在功能上粒度更小。

这里说的类,指的能够是实体,也能够是服务。领域驱动设计这本书中,Eric把应用里的对象分红三种,值,实体,服务。能够认为实体就是一些值的组合,服务是包含业务逻辑的代码。

Open Closed Principle (OCP,开闭原则)

Software entities like classes, modules and functions should be open for extension but closed for modifications.

说到开闭原则,对我而言,结合一个设计模式确定特别容易理解,装饰器模式。咱们设想一个场景。如今在作超市的收银系统,或者是什么电商网站,反正有一个商品的实体,商品实体有一个GetPrice方法,如今要给商品加一个打折的功能。假设商品这个歌类知足单一职责原则,如今有三种方案:

  1. 给接口加一个方法,专门用来提供如今须要的功能

  2. 修改类的实现,用之前的方法签名提供新的功能

  3. 从新建立一个类,继承以前的实现类,对之前的方法包装一层,提供新的实现

开闭原则但愿咱们选第三种解决方案。简单说一下缘由:第一个,修改接口意味着其余实现了这个接口的类须要新增一个实现,这个新增的实现颇有多是没必要要的,万一哪天全部的商品都不打折了,这个方法就废掉了,因此pass;第二个简直就是一个危险操做,必定会影响单元测试不说(若是有的话),很大几率会给依赖原来实现的模块引入bug,pass。第三个方案最安全。咱们能够为之前的实现提供一个装饰器,而后调用装饰器来提供新的功能。以前随便举的例子,之前的实现就是返回商品原价,装饰器能够叫什么“打折装饰器”,而后调用一样的方法返回的是打折后的价格,之前的实现要经过构造方法参数注入进装饰器实现里。实际使用可能没这么粗暴,不过大意就是这样。

另外,开闭原则和另一个“三大编程原则”版本中的Don't Repeat Yourself原则很类似,只不过角度不一样于上面使用装饰器模型的场景了。《重构》中提到了Rule of three,一样的代码在第三次出现前,考虑一下怎么抽象它。

以后讲依赖注入的组合根的时候会再次提到开闭原则。

Liskov Substitution Principle (LSP,里氏替换原则)

Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.

这个估计是平时最容易忽视的一个原则。

使用基类的地方,替换成它的子类,程序还能够正常运行。这个主要是让咱们继承一个父类的时候,不要随便重写父类的方法,重写以前要思考一下。其实这个原则的提出应该起源于1988年一我的提出的Design by Contract(契约式设计)理论,这我的我也不认识,契约式设计咱们如今也不必深究,不过咱们刚开始学接口可能也听过,一个接口方法,就是一个契约,契约式设计把一个契约的实现分红三部分,如今只要理解前两个就能够了:

  1. 前置条件检查,就是验证校验参数是否合法。

  2. 后置条件检查,就是验证方法的执行结果是否符合契约,是否诚实,熟悉函数式编程的朋友可能知道FP中的一个原则要编写诚实的函数,FP里的诚实是说一个函数要作到言出必行,履行本身的函数签名,不能说让它返回一个商品打折后的价格,它抛了一个异常,由于这个异常没有在它的签名里获得体现,签名里说传入一个string,返回一个int,既然指明了这个映射,那返回值就得是int这个集合里的。这里说的后置条件验证是否遵照契约,我就是这么理解的。

  3. 不变式检查,对象检查自身的状态,确保本身的本质不变,这个不在SOLID原则里,要实现的话也跟AOP有关。

总之,里氏替换原则是说,若是你要在子类里重写父类的方法,前置条件的检查要么和父类相同,要么更宽松;后置条件的检查要么相同,要么更严格。由于原本在子类里重写父类方法的动机就是想对父类作一个扩展,这么说来里氏替换原则原则仍是很符合直觉的。

我以前犯过一个错误,就是有一个方法我设想子类不提供,就在这个子类重写了父类的方法,而后直接throw new NotImplementedException();,这就严重违反了里氏替换原则。这时候应该把这一个方法单独提出来,作一个接口,我正在写的这个子类不要继承这个接口就行了。

Interface Segregation Principle (ISP,接口隔离原则)

Clients should not be forced to depend upon interfaces that they don`t use.

The dependency of one class to another one should depend on the smallest possible.

咱们aelf的智能合约执行框架是经过grpc和.NET的反射实现的。咱们有一个跨合约调用的功能,在定义咱们链上须要的智能合约的时候,定义了一些智能合约须要实现的接口,咱们称之为:ACS,即AElf Contract Standard。每个ACS定义了一些接口,相似于咱们平时在开发中定义interface的场景,开发者在作跨合约调用的时候,能够不用关心某些ACS中定义的接口具体是哪一个合约实现的,只须要提供实现了这个ACS的某一个合约的地址就能够了。另外一方面,每个合约能够实现多个ACS,被其余合约作跨合约调用时,可能只对它实现的某一个ACS感兴趣,那么就能够仅仅依赖于这一个ACS,不会把这个合约的其余方法暴露给跨合约调用,这即是对接口隔离原则的最好阐述。

在aelf的代码实现过程当中,咱们还会在链上对一些接口提供支持,好比说共识合约。由于aelf的设计理念就是但愿区块链的共识是能够被替换的。目前咱们采用的共识是DPOS,可是理论上,也能够支持其余的共识合约如POW或者POC。所以,咱们在代码实现的时候,把咱们认为一个共识合约应该提供的服务抽象了出来:好比确认如今能不能出块?由于POW模式下是每一个人均可以出块的,可是若是是POS或者DPOS的话,你须要通过一系列选举投票成为记帐节点或者见证人才能进行出块;若是能出块的话,节点须要多长时间出一个块;出块的时候应当如何组织这个块的数据,同时这个共识合约还应该告诉你当你收到一个块以后,应当怎样去验证它等等。这个高度抽象概括后的共识合约接口标准,咱们将其定义为ACS4。

Dependency Inversion Principle (DIP,依赖倒置原则)

High level modules should not depend upon low level modules.

Both should depend upon abstractions.  Abstractions should not depend upon details. Details should depend upon abstractions.

高层模块和底层模块都应该依赖抽象。很直白。一句话:面向接口编程。咱们能够把接口放在比较底层的模块中,而后在高层的模块中提供对这个接口的实现。不一样的高层模块之间的交流,也是经过底层模块里的接口来进行的。就好比咱们把日志看成一个高层模块,底层模块中只有一个ILogger接口,提供一些打印不一样等级的日志的方法,具体打印的实现放在日志模块里。其余模块可能都依赖日志模块才能打出日志,能够用NLog,也能够用log4net,日志模块就变成了一个依赖项,具体用哪一个,要看咱们怎么手动注入这个依赖。这里终于谈到了依赖注入。因此怎么注入?很快咱们会说到组合根(Composition Root)。

DI,IoC,DIP的区别

这里的DI是说DI技术,不是DI容器,咱们开发时用到的依赖注入框架,好比Autofac,Ninject,等等,都属于DI容器。

DI实际上是一系列设计模式的组合。好比有一个很是重要的设计模式:Composition Root(组合根)。这个原本是接下来说的,咱们提早说一下,否则很差说明白单纯的DI技术是怎么用的。 组合根是一种设计模式,使用这个设计模式的重点在于咱们要找到放置组合根的位置,好在前人给了咱们经验:尽量地靠近应用程序地入口点。那么组合根是用来作什么的?简而言之,配置——若是简单说配置可能会使人误解组合根是一个DI容器——换个说法,设定,依赖关系。也就是这个应用程序中,哪一个抽象类型对应哪一个具体类型,都须要在组合根中经过某种方式进行设定。这里说的抽象类型在C#中能够是接口,也能够是抽象类,事实上在应用DI的实践中用那种方式进行抽象根本不重要,咱们只须要在组合根中设定好抽象类型对应具体类型的依赖关系就能够了。这个设定在DI容器中能够直接调用Register或者AddSingleton之类的方法,然而应用DI技术并不必定要用DI容器,咱们接下来会给一个不实用DI容器,可是使用了DI的技术或者说思想的最简单的例子。

咱们从“以终为始”的角度从新理解一下组合根这个概念:能够想象,一个完整的能够用于生产的应用,必定包含大量的服务,而若是咱们想让这个应用程序的实现是松耦合的,就会用到接口隔离原则,也就是说,当咱们在某个服务中须要用到其余服务时,由于写代码的时候不知道其余服务会经过什么实现,因此在代码中,咱们直接调用一个抽象。而应用程序运行起来以后,在针对抽象的调用实际发生以前,就必定有一个装配的过程,所谓的装配就是根据代码里的抽象类型,在运行时给出一个具体的实现,这就要求咱们手动提供一个抽象类型和具体类型的映射关系。组合根就是手动提供这个关系的地方。最简单的,控制台应用,要跑来的话须要各个组件配合,高层组件对底层组件的依赖注入有不少种,目前看最经常使用的是构造方法注入,后面会说其余的注入方式,这里的代码适用构造方法注入,你不能把一个接口丢进去,确定要本身手动new出来一个实现放进去,这个操做就是设定了抽象类型和具体类型的依赖关系。

这里插一句,咱们回过头看一下开闭原则,若是从组合根的角度看,当某一个功能须要修改的时候,咱们能够不改变原有的实现,而是用装饰模式从新提供一个实现,而后在组合根中将对应的依赖设定成新的实现,只须要添加代码、改一下组合根就能够了,新的实现有问题还能够随时撤回来,一直到肯定旧的实现不须要了,再消除这个冗余的关系,这一步就是重构了。重构的时候如何肯定已经实现的feature不被影响?人老是不靠谱的,特别是把重构这个活交给别人的话。因此咱们要在重构前补充上足够的单元测试,甚至一开始实现前就写好单元测试。测试驱动设计(Test Driven Design)要求先写单元测试,单元测试会fail,而后提供实现,直到测试经过,可是若是软件开发到此为止,只能称做测试优先设计(Test First Design),测试驱动设计的核心是重构,解耦到满意的地步,测试用例就是重构不会破坏功能的重要保证。

IoC,控制反转,刚开始的时候就说过,DI实际上是IoC的一种实现,可是IoC这个概念从它提出的角度而言,更偏向于方向。可能有人听过好莱坞原则:don't call me, I'll call you,怎么理解呢,就是一个具体类型做为一个依赖模块,不须要关心本身什么时候会被调用,怎么调用,只须要等着本身被调用就好了,just do it。为何叫好莱坞模式,由于若是在好莱坞,一个演员很火,很被市场须要,就像有一个模块很被咱们正在写的代码须要同样,那这个演员什么都不须要关心,导演会拿着剧本去找他,或者找他的经纪人,演员只须要投入工做就能够了。 最后说SOLID里的DIP,它指明的是高层模块不该该依赖底层模块,它们应该都依赖抽象,从这里看,DIP更加关注抽象的程度,也就是在具体实现上,代码的形态。

可是咱们能够粗略地认为,这三个概念表达的是一个思想,若是有什么东西必须提炼的话,那就是面向接口编程。并且要把接口放在底层,实现放在高层。

DI的三个维度

接下来咱们简单说一下依赖注入这个技术的三个维度,它们是相辅相成的,

对象组合

也就是组合根实际上作的事情,即配置依赖关系。

生命周期管理

前面只说组合根是设定依赖关系的地方,其实在设定依赖关系的同时,依赖注入还要求管理每个对象的生命周期。

咱们已经知道,应用DI时,依赖都是在组合根中设定的,咱们也能够把组合对象的对象或方法称为组合器(Composer),组合器是一个统一术语,就指组装依赖的对象或者方法。一般而言,DI容器就是一个组合器。

因为组合器的存在,对象注定管不了它的依赖的建立过程。那依赖的销毁呢?若是依赖不及时销毁,就会有内存泄露的风险。

用过.NET的都知道,GC会自动地回收不会被使用的对象,除非这个对象咱们实现了IDisposable接口,咱们才能够本身亲手销毁对象。

在DI中,对象的生命周期是由组合器管理的。组合器能够决定某个依赖对象是否在其余不一样的对象中共享,也能够决定释放对象的时机:是超出某一个消费者的做用域就释放,仍是超出全部消费者的做用于才释放。

对象生命周期的管理应该是依赖注入中最复杂的问题之一,时间关系就不展开讲了,稍后咱们看一下微软一个叫Extensions的repo里为依赖注入的实现提供支持的代码,这个项目为全部对象准备了三种lifestyle,定义在ServiceLifetime中。这个单词自己的意思是生活方式或工做方式,这里彷佛翻译成生命周期类型比较好。这三种生命周期类型放在DI容器的角度可能更好理解,不过管理对象生命周期这个命题在不使用DI容器的时候也是存在的,这三种分类依然适用:

  • Singleton。整个应用程序中会一直共享某个抽象对应的同一个实例。

  • Scoped。在一个给定的做用于中使用单例,等同于Singleton,不一样做用域就提供不一样的实例。

  • Transient。每次请求都会返回一个刚new出来的实例。

拦截

理论上来讲,若是代码实现时遵照了SOLID原则,就能够经过装饰器模式来实现面向切面(AOP)编程。大体作法就是针对之前的XXService,建立其子类命名为XXServiceDecorator,把XXService做为构造方法参数注入XXServiceDecorator,在XXServiceDecorator从新实现XXService中的方法作拦截,通常而言就是直接调用,先后加条件。最后把在组合根设定上XXServiceDecorator而非XXService就好了。

不过实现AOP还有另外两种方法,动态拦截(Dynamic Interception)(也叫动态代理)和编译时织入(Compile-Time Weaving)(也叫IL编织)。

Demo

github.com/EanCuznaivy…

参考资料

stackoverflow.com/questions/1…

softwareengineering.stackexchange.com/questions/2…

martinfowler.com/articles/di…

相关文章
相关标签/搜索