ABP框架的体系结构及模块系统讲解
ddd分层
为了减少复杂性和提高代码的可重用性,采用分层架构是一种被广泛接受的技术。
为了实现分层的体系结构,abp遵循ddd(领域驱动设计)的原则,将分为四个层次:
- 展现层(presentation):提供一个用户界面,实现用户交互操作。
- 应用层(application):进行展现层与领域层之间的协调,协调业务对象来执行特定的应用程序的任务。它不包含业务逻辑。
- 领域层(domain):包括业务对象和业务规则,这是应用程序的核心层。
- 基础设施层(infrastructure):提供通用技术来支持更高的层。例如基础设施层的仓储(repository)可通过orm来实现数据库交互。
根据实际需要,可能会有额外添加的层。例如:
分布式服务层(distributed service):用于公开应用程序接口供远程客户端调用。比如通过asp.net web api和wcf来实现。
这些都是常见的以领域为中心的分层体系结构。不同的项目在实现上可能会有细微的差别。
abp的体系结构
一个简单的解决方案,大致包含5个项目:
每一层可以用一个或多个程序集来实现。
1.领域层(domain)
领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。
2.实体(entity)
实体代表业务领域的数据和操作,在实践中,通过用来映射成数据库表。
3.仓储(repository)
仓储用来操作数据库进行数据存取。仓储接口在领域层定义,而仓储的实现类应该写在基础设施层。
4.领域服务(domain service)
当处理的业务规则跨越两个(及以上)实体时,应该写在领域服务方法里面。
5.领域事件(domain event)
在领域层某些特定情况发生时可以触发领域事件,并且在相应地方捕获并处理它们。
6.工作单元(unit of work)
工作单元是一种设计模式,用于维护一个由已经被修改(如增加、删除和更新等)的业务对象组成的列表。它负责协调这些业务对象的持久化工作及并发问题。
应用层(application)
应用层提供一些应用服务(application services)方法供展现层调用。一个应用服务方法接收一个dto(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个dto。在展现层到领域层之间,不应该接收或返回实体(entity)对象,应该进行dto映射。一个应用服务方法通常被认为是一个工作单元(unit of work)。用户输入参数的验证工作也应该在应用层实现。abp提供了一个基础架构让我们很容易地实现输入参数有效性验证。建议使用一种像automapper这样的工具来进行实体与dto之间的映射。
基础设施层(infrastructure)
当在领域层中为定义了仓储接口,应该在基础设施层中实现这些接口。可以使用orm工具,例如entityframework或nhibernate。abp的基类已经提供了对这两种orm工具的支持。数据库迁移也被用于这一层。
web与展现层(web & presentation)
web层使用asp.net mvc和web api来实现。可分别用于多页面应用程序(mpa)和单页面应用程序(spa)。
在spa中,所有资源被一次加载到客户端浏览器中(或者先只加载核心资源,其他资源懒加载),然后通过ajax调用服务端webapi接口获取数据,再根据数据生成html代码。不会整个页面刷新。现在已经有很多spa的js框架,例如: angularjs、 durandaljs、backbonejs、emberjs。 abp可以使用任何类似的前端框架,但是abp提供了一些帮助类,让我们更方便地使用angularjs和durandaljs。
在经典的多页面应用(mpa)中,客户端向服务器端发出请求,服务器端代码(asp.net mvc控制器)从数据库获得数据,并且使用razor视图生成html。这些被生成后的html页面被发送回客户端显示。每显示一个新的页面都会整页刷新。
spa和mpa涉及到完全不同的体系结构,也有不同的应用场景。一个管理后台适合用spa,博客就更适合用mpa,因为它更利于被搜索引擎抓取。
signalr是一种从服务器到客户端发送推送通知的完美工具。它能给用户提供丰富的实时的体验。
已经有很多客户端的javascript框架或库,jquery是其中最流行的,并且它有成千上万免费的插件。使用bootstrap可以让我们更轻松地完成写html和css的工作。
abp也实现了根据web api接口自动创建 javascript的代码函数,来简化js对web api的调用。还有把服务器端的菜单、语言、设置等生成到js端。(但是在我自己的项目中,我是把这些自动生成功能关闭的,因为必要性不是很大,而这些又会比较影响性能)。
abp会自动处理服务器端返回的异常,并以友好的界面提示用户。
abp模块系统
abp框架提供了创建和组装模块的基础,一个模块能够依赖于另一个模块。在通常情况下,一个程序集就可以看成是一个模块。在abp框架中,一个模块通过一个类来定义,而这个类要继承自abpmodule。
译者注:如果学习过orchard的朋友,应该知道module模块的强大了。模块的本质就是可重用性,你可以在任意的地方去调用,而且通过实现模块,你写的模块也可以给别人用。
assembly程序集:assembly是一个包含来程序的名称,版本号,自我描述,文件关联关系和文件位置等信息的一个集合。最简单的理解就是:一个你自己写的类库生成的dll就可以看做是一个程序集,这个程序集可以包括很多类,类又包括很多方法等。
.net可以通过反射获取一个程序集中的类以及方法。
下面的例子,我们开发一个可以在多个不同应用中被调用mybolgapplication模块,代码如下:
public class myblogapplicationmodule : abpmodule //定义 { public override void initialize() //初始化 { iocmanager.registerassemblybyconvention(assembly.getexecutingassembly()); //这行代码的写法基本上是不变的。它的作用是把当前程序集的特定类或接口注册到依赖注入容器中。 } }
abp框架会扫描所有的程序集,并且发现abpmodule类中所有已经导入的所有类,如果你已经创建了包含多个程序集的应用,对于abp,我们的建议是为每一个程序集创建一个module(模块)。
生命期事件
在一个应用中,abp框架调用了module模块的一些指定的方法来进行启动和关闭模块的操作。我们可以重载这些方法来完成我们自己的任务。
abp框架通过依赖关系的顺序来调用这些方法,假如:模块a依赖于模块b,那么模块b要在模块a之前初始化,模块启动的方法顺序如下:
- preinitialize-b
- preinitialize-a
- initialize-b
- initialize-a
- postinitialize-b
- postinitialize-a
下面是具体方法的说明:
1.preinitialize
预初始化:当应用启动后,第一次会调用这个方法。在依赖注入注册之前,你可以在这个方法中指定自己的特别代码。举个例子吧:假如你创建了一个传统的登记类,那么你要先注册这个类(使用iocmanager对登记类进行注册),你可以注册事件到ioc容器。等。
2.initialize
初始化:在这个方法中一般是来进行依赖注入的注册,一般我们通过iocmanager.registerassemblybyconvention这个方法来实现。如果你想实现自定义的依赖注入,那么请参考依赖注入的相关文档。
3.postinitialize
提交初始化:最后一个方法,这个方法用来解析依赖关系。
4.shutdown
关闭:当应用关闭以后,这个方法被调用。
模块依赖(module dependencies)
abp框架会自动解析模块之间的依赖关系,但是我们还是建议你通过重载getdependencies方法来明确的声明依赖关系。
[dependson(typeof(myblogcoremodule))]//通过注解来定义依赖关系 public class myblogapplicationmodule : abpmodule { public override void initialize() { iocmanager.registerassemblybyconvention(assembly.getexecutingassembly()); } }
例如上面的代码,我们就声明了myblogapplicationmodule和myblogcoremodule的依赖关系(通过属性attribute),myblogapplicationmodule这个应用模块依赖于myblogcoremodule核心模块,并且,myblogcoremodule核心模块会在myblogapplicationmodule模块之前进行初始化。
如何自定义的模块方法
我们自己定义的模块中可能有方法被其他依赖于当前模块的模块调用,下面的例子,假设模块2依赖于模块1,并且想在预初始化的时候调用模块1的方法。
public class mymodule1 : abpmodule { public override void initialize() //初始化模块 { iocmanager.registerassemblybyconvention(assembly.getexecutingassembly());//这里,进行依赖注入的注册。 } public void mymodulemethod1() { //这里写自定义的方法。 } } [dependson(typeof(mymodule1))] public class mymodule2 : abpmodule { private readonly mymodule1 _mymodule1; public mymodule2(mymodule1 mymodule1) { _mymodule1 = mymodule1; } public override void preinitialize() { _mymodule1.mymodulemethod1(); //调用mymodulemethod1的方法。 } public override void initialize() { iocmanager.registerassemblybyconvention(assembly.getexecutingassembly()); } }
就这样,就把模块1注入到了模块2,因此,模块2就能调用模块1的方法了。