.NET应用架构设计—从新认识分层架构(现代企业级应用分层架构核心设计要素)

阅读目录:前端

  • 1.背景介绍
  • 2.简要回顾下传统三层架构
  • 3.企业级应用分层架构(现代分层架构的基本演变过程)
    • 3.1.服务层中应用契约式设计来解决动态条件不匹配错误(经过契约式设计模式来将问题在线下暴露出来)
    • 3.2.应用层中的应用控制器模式(经过控制器模式对象化应用层的职责)
    • 3.3.业务层中的命令模式(事务脚本模式的设计模式运用,很好的隔离静态数据)
  • 4.服务层做为SOA契约公布后DTO与业务层的DomainModel共用基本的原子类型
  • 5.两种独立业务层职责设计方法(能够根据具体业务要求来搭配)
    • 5.1.在应用层中的应用控制器中协调数据层与业务层的互动(业务层将绝对的独立) 
    • 5.2.将业务层直接依赖数据层的关系使用IOC思想改变数据层依赖业务层(业务层将绝对独立)(比较优雅)
  • 6.总结

1.背景介绍

接触分层架构有段时间了,从刚开始的朦朦胧胧的理解到如今有必定深度的研究后,以为有必要将本身的研究成果分享出来给你们,互相学习,也是对本身的一个总结。编程

咱们天天面对的项目结构能够说几乎都是分层结构的,或者是基于传统三层架构演变过来的相似的分层结构,少不了业务层、数据层,这两个层是比较重要的设计点,看似这两个层是互相独立的,可是这两个层如何设计真的还有不少比较微妙的地方,本文将分享给你们我在工做中包括本身的研究中得出的比较可行的设计方法。后端

2.简要回顾下传统三层架构

其实这一节我原本不打算加的,关于传统三层架构我想你们都应该了解或者很熟悉了,可是为了使得本文的完整性,我仍是简单的过一下三层架构,由于我以为它可使得我后面的介绍有连贯性。设计模式

传统三层架构指将一个系统按照不一样的职责划分层三个基本的层来分离关注点,将一个复杂的问题分解成三个互相协做的单元来共同的完成一个大任务。网络

1.显示层:用来显示数据或从UI上获取数据;该层主要是用来处理数据显示和特效用的,不包括任何业务逻辑。多线程

2.业务层:业务层包含了系统中全部的核心业务逻辑,不包括任何跟数据显示、数据存取相关的代码逻辑。架构

3.数据层:用来提供对具体的数据源引擎的访问,主要用来直接存取数据,不包括业务逻辑处理。框架

其实用文字描述这三个层的基本职责还非常比较容易的,可是不一样的人如何理解并设计这三个层就形态万千了,反正我是看过不少各类各样的分层结构,各有各的特色,从某个角度讲都很不错,可是都显得有点乱,由于没有一个统一的架构模式来支撑,代码中充满了对分层架构的理解错位的地方,好比:常常看见将“事物脚本”模式和“表模块”模式混搭使用的,致使我最后都不知道把代码写在哪里,提取出来的代码也不知道该放到哪一个对象里。dom

层虽简单可是要想运用的好不容易,毕竟咱们仍是站在一个比较高的层面比较笼统的层面来谈论分层结构的,一旦落实到代码上就彻底不同了,用不用接口来隔离各层,接口放在哪一个层里,这些都是很微妙的,固然本文不是为了说明我所介绍的设计是多么的好,而是给你们一个能够参考的例子而已。ide

言归正传,三个层之间的调用要严格按照“上层只能调用直接下层,不可以越权,而下层也不可以调用本身的上层”,这是基本的层结构的调用约束,只有这样才能保证一个好的代码结构。显示层只能调用业务层,业务层也只能调用数据层,其实就是这么简单,固然具体的代码设计也能够大概概括为两种,第一种是实例类或静态类直接调用;第二种是每一个层之间加上接口来隔离每一个层,使得测试、部署容易点,可是若是用的很差的话效果不大反而会增长复杂度,还不如直接使用静态类来的直接点,可是用静态类来设计业务类会使多线程操做很难实施,稍微不注意就会串值或报错。

3.企业级应用分层架构(现代分层架构的基本演变过程)

上节中咱们基本了解了传统三层架构的类型和职责,本节咱们来简单介绍一下现代企业应用分层架构的类型和职责。

随着企业应用的复杂度增长,在原有三层架构上逐渐演化出如今的面向企业级的分层架构,这种架构能很好的支持新的技术和代码上的最佳实践。

在传统的三层结构中的业务层之上多了一个应用层也但是说是服务层,该层是为了直接隔离显示层来调用业务层,由于如今的企业应用基本上都在往互联网方向发展,对业务逻辑的访问不会在是从进程内访问了,而是须要跨越网络来进行。

有了这一层以后会让本来显示层调用业务层的过程变得灵活不少,咱们能够添加不少灵活性在里面,更为重要的是显示层和业务层两个独立的层要想彻底独立是必需要有一个层来辅助和协调他们之间的互动的。在最先的三层架构的书籍中其实也提到了“服务层”来协调的做用,为何咱们不少的项目都未曾出现过,当我本身看到书上有讲解后才恍然大悟。(该部分能够参考:《企业应用架构模式》【马丁.福勒】;第二部分,第9章“服务层”)

图1:(逻辑分层)

应用层中包含了服务的设计部分,应用层的概念稍微大一点,里面不只不含了服务还包含了不少跟服务不相关的应用逻辑,好比:记录LOG,协调基础设施的接入等等,就是将服务层放宽了理解。

图2:(项目结构分层)

在应用层中包含了咱们上述所说的”服务“,将”服务层“放宽后造成了如今分层架构中相当重要的”应用层“。应用层将负责总体的协调”业务层“和”数据层“及“基础设施”,固然还会包括系统运行时环境相关的东西。

3.1.服务层中应用契约式设计来解决动态条件不匹配错误(经过契约式设计模式来将问题在线下暴露出来)

此设计方法主要是想将动态运行时条件不匹配错误在线下自动化回归测试时就暴露出来。由于服务层中的契约可能会面临着被修改的危险性,因此咱们没法知道咱们本次上线的契约中是否包含了不稳定的条件不匹配的危险。

利用契约式设计模式能够在调用时自动的执行契约发布方预先设定的契约检查器,契约检查器分为前置条件检查器和后置条件检查器;咱们来看一个简单的例子;

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks; 
 6 
 7 namespace CompanySourceSearch.Service.Contract
 8 {
 9     using CompanySourceSearch.ServiceDto.Request;
10     using CompanySourceSearch.ServiceDto.Response; 
11 
12     public interface ISearchComputer
13     {
14         GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request);
15     }
16 } 
View Code

在服务契约中我定义了一个用来查询企业中电脑资源的接口,好的设计原则就是不要直接暴露查询字段而是要将其封装起来。

 1 namespace CompanySourceSearch.ServiceDto
 2 {
 3     public abstract class ContractCheckerBase
 4     {
 5         private Func<bool> checkSpecification;
 6         public Func<bool> CheckSpecification
 7         {
 8             get
 9             {
10                 return this.checkSpecification;
11             }
12             private set
13             {
14                 this.checkSpecification = value;
15             }
16         } 
17 
18         public void SetCheckSpecfication(Func<bool> checker)
19         {
20             CheckSpecification = checker;
21         } 
22 
23         public virtual bool RunCheck()
24         {
25             if (CheckSpecification != null)
26                 return CheckSpecification(); 
27 
28             return false;
29         }
30     }
31 } 
View Code

而后定义了一个用来表示契约检查器的基类,这里纯粹是为了演示目的,代码稍微简单点。服务契约的请求和响应都须要经过继承这个检查器类来实现自身的检查功能。

 1 namespace CompanySourceSearch.ServiceDto.Request
 2 {
 3     public class GetComputerByComputerIdRequest : ContractCheckerBase
 4     {
 5         public long ComputerId { get; set; } 
 6 
 7         public GetComputerByComputerIdRequest()
 8         {
 9             this.SetCheckSpecfication(() => ComputerId > 0/*ComputerId>0的检查规则*/);
10         }
11     }
12 }
View Code

Request类在构造函数中初始化了检查条件为:ComputerId必须大于0。

 1 namespace CompanySourceSearch.ServiceDto.Response
 2 {
 3     using CompanySourceSearch.ServiceDto; 
 4 
 5     public class GetComputerByComputerIdResponse : ContractCheckerBase
 6     {
 7         public List<ComputerDto> ComputerList { get; set; } 
 8 
 9         public GetComputerByComputerIdResponse()
10         {
11             this.SetCheckSpecfication(() => ComputerList != null && ComputerList.Count > 0);
12         }
13     }
14 } 
View Code

一样Response类也在构造函数中初始化了条件检查器为:ComputerList不等于NULL而且Count要大于0。仍是那句话例子是简单了点,可是设计思想很不错。

对前置条件检查器的执行能够放在客户端代理中执行,固然你也能够自行去执行。后置条件检查器其实在通常状况下是不须要的,若是你能保证你所测试的数据是正确的,那么做为自动化测试是应该须要的,当时维护一个自动化测试环境很不容易,因此若是你用后置条件检查器来检查数据动态变化的环境时是不太合适的。

3.2.应用层中的应用控制器模式(经过控制器模式对象化应用层的职责)

应用层设计的时候大部分状况下咱们都喜欢使用静态类来处理,静态类有着良好的代码简洁性,并且还能带来必定的性能提高。可是从长远来考虑静态类存在一些潜在的问题,数据不能很好的隔离,重复代码不太好提取,单元测试不太好写。

为了可以在很长的一段时间内似的项目维护性很高的状况下仍是建议将应用控制器使用实例类设计,这里我喜欢使用“应用控制器”来设计。它很形象的表达了协调前端和后端的职责,可是具体不处理业务逻辑,与MVC中的控制器很像。

 1 namespace CompanySourceSearch.ApplicationController.Interface
 2 {
 3     using CompanySourceSearch.Service.Contract;
 4     using CompanySourceSearch.ServiceDto.Response;
 5     using CompanySourceSearch.ServiceDto.Request; 
 6 
 7     public interface ISearchComputerApplicationController
 8     {
 9         GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request);
10     }
11 } 
View Code

在应用控制器中咱们定义了一个用来负责上述查询Computer资源的的控制器接口。

 1 namespace CompanySourceSearch.ApplicationController
 2 {
 3     using CompanySourceSearch.ApplicationController.Interface;
 4     using CompanySourceSearch.ServiceDto.Request;
 5     using CompanySourceSearch.ServiceDto.Response; 
 6 
 7     public class SearchComputerApplicationController : ISearchComputerApplicationController
 8     {
 9         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
10         {
11             throw new NotImplementedException();
12         }
13     }
14 }
View Code

控制器实现类。这样能够很清晰的分开各个应用控制器,这样对服务实现来讲是个很不错的提供者。

 1 namespace CompanySourceSearch.ServiceImplement
 2 {
 3     using CompanySourceSearch.Service.Contract;
 4     using CompanySourceSearch.ServiceDto.Response;
 5     using CompanySourceSearch.ServiceDto.Request;
 6     using CompanySourceSearch.ApplicationController.Interface; 
 7 
 8     public class SearchComputer : ISearchComputer
 9     {
10         private readonly ISearchComputerApplicationController _searchComputerApplicationController; 
11 
12         public SearchComputer(ISearchComputerApplicationController searchComputerApplicationController)
13         {
14             this._searchComputerApplicationController = searchComputerApplicationController;
15         } 
16 
17         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
18         {
19             return _searchComputerApplicationController.GetComputerByComputerId(request);
20         }
21     }
22 } 
View Code

服务在使用的时候只须要使用IOC的框架将控制器实现直接注入进来就好了,固然这里你能够加上AOP用来记录各类日志。

经过将控制器按照这样的方式进行设计能够很好的进行单元测试和重构。

3.3.业务层中的命令模式(事务脚本模式的设计模式运用,很好的隔离静态数据)

在通常的企业应用中大部分的业务层都是使用"事务脚本"模式来设计,因此这里我以为有个很不错的模式能够借鉴一下。可是不少事务脚本模式都是使用静态类来处理的,这一点和控制器使用静态类类似了,代码比较简单,使用方便。可是依然有着几个问题,数据隔离,不便于测试重构。

将事务脚本使用命令模式进行对象化,进行数据隔离,测试重构都很方便,若是你有兴趣实施TDD将是一个不错的结构。

1 namespace CompanySourceSearch.Command.Interface
2 {
3     using CompanySourceSearch.DomainModel; 
4 
5     public interface ISearchComputerTransactionCommand
6     {
7         List<Computer> FilterComputerResource(List<Computer> Computer);
8     }
9 } 
View Code

事务命令控制器接口,定义了一个过滤Computer资源的接口。你可能看见了我使用到了一个DominModel的命名空间,这里面是一些跟业务相关的且经过不断重构抽象出来的业务单元(有关业务层的内容后面会讲)。

 1 namespace CompanySourceSearch.Command
 2 {
 3     using CompanySourceSearch.Command.Interface; 
 4 
 5     public class SearchComputerTransactionCommand : CommandBase, ISearchComputerTransactionCommand
 6     {
 7         public List<DomainModel.Computer> FilterComputerResource(List<DomainModel.Computer> Computer)
 8         {
 9             throw new NotImplementedException();
10         }
11     }
12 } 
View Code

使用实例类进行业务代码的组装将是一个不会后悔的事情,这里咱们定义了一个CommandBase类来作一些封装工做。

应用控制器一样和服务类同样使用IOC的方式使用业务命令对象。

 1 namespace CompanySourceSearch.ApplicationController
 2 {
 3     using CompanySourceSearch.ApplicationController.Interface;
 4     using CompanySourceSearch.ServiceDto.Request;
 5     using CompanySourceSearch.ServiceDto.Response;
 6     using CompanySourceSearch.Command.Interface; 
 7 
 8     public class SearchComputerApplicationController : ISearchComputerApplicationController
 9     {
10         private readonly ISearchComputerTransactionCommand _searchComputerTransactionCommand;
11         public SearchComputerApplicationController(ISearchComputerTransactionCommand searchComputerTransactionCommand)
12         {
13             this._searchComputerTransactionCommand = searchComputerTransactionCommand;
14         } 
15 
16         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
17         {
18             throw new NotImplementedException();
19         }
20     }
21 } 
View Code

到目前为止每一个层之间坚持使用面向接口编程。

4.服务层做为SOA契约公布后DTO与业务层的DomainModel共用基本的原子类型

这里有个矛盾点须要咱们平衡,当咱们定义服务契约时会定义服务所使用的DTO,而在业务层中为了很好的凝聚业务模型咱们也定义了部分领域模型或者准确点讲,在事务脚本模式的架构中咱们是经过不断重构出来的领域模型,它封装了部分领域逻辑。因此当服务中的DTO与领域模型中的实体须要使用相同的原子类型怎么办?好比某个类型的状态等等。

若是纯粹的隔离两个层面,咱们彻底能够定义两套如出一辙的原子类型来使用,可是这样会带来不少重复代码,难以维护。若是不定义两套那么又将这些共享的类型放在哪里比较合适,放在DTO中显示不合适,业务模型是不可能引用外面的东西的,若是放在领域模型中彷佛也有点不妥。

这里我是采用将原子类型独立一个项目来处理的,能够相似于"CompanySourceSearch.DomainModel.ValueType"这样的一个项目,它只包含须要与DTO进行共享的原子值类型。

5.两种独立业务层职责设计方法(能够根据具体业务要求来搭配)

以前咱们没有谈业务层的设计,这里咱们重点讲一下业务层的设计包括与数据层的互操做。

从应用层开始考虑,当咱们须要处理某个逻辑时从应用控制器开始可能就会认为直接进入到服务层了,而后服务层再去调用数据层,其实这只是设计的一种方式而已。这样的设计方式好处就是简单明了,实现起来比较方便。可是这种方法有个问题就是业务层始终仍是依赖数据层的,业务层的变更依然会受到数据层的影响。还有一个问题就是若是这个时候你使用不是“事务脚本”模式来设计业务层的话也会天然而然的写成过程式代码,由于你将本来用来协调的应用控制器没有作到该作的事情,它实际上是用来协调业务层和数据层的,咱们并不必定非要在业务层中去调用数据层,而是能够将业务层须要的数据从控制器中获取好而后传入到业务层中去处理,这和直接在业务层中去调用数据层是差很少的,只不过是写代码的时候不能按照过程式的思路来写了。

无论咱们是使用事务脚本模式仍是表模块模式或者当下比较流行的领域模型模式,均可以使用这种方法进行设计。

5.1.在应用层中的应用控制器中协调数据层与业务层的互动(业务层将绝对的独立)

咱们将在应用控制器中去调用数据层的方法拿到数据而后转换成领域模型进行处理。

namespace CompanySourceSearch.Database.Interface
{
    using CompanySourceSearch.DatasourceDto; 

    public interface IComputerTableModule
    {
        List<ComputerDto> GetComputerById(long cId);
    }
} 
View Code

咱们使用"表入口“数据层模式来定义了一个用来查询Computer的方法。

 1 namespace CompanySourceSearch.ApplicationController
 2 {
 3     using CompanySourceSearch.ApplicationController.Interface;
 4     using CompanySourceSearch.ServiceDto.Request;
 5     using CompanySourceSearch.ServiceDto.Response;
 6     using CompanySourceSearch.Command.Interface;
 7     using CompanySourceSearch.Database.Interface;
 8     using CompanySourceSearch.DatasourceDto;
 9     using CompanySourceSearch.Application.Common; 
10 
11     public class SearchComputerApplicationController : ISearchComputerApplicationController
12     {
13         private readonly ISearchComputerTransactionCommand _searchComputerTransactionCommand;
14         private readonly IComputerTableModule _computerTableModule;
15         public SearchComputerApplicationController(ISearchComputerTransactionCommand searchComputerTransactionCommand,
16             IComputerTableModule computerTableModule)
17         {
18             this._searchComputerTransactionCommand = searchComputerTransactionCommand;
19             this._computerTableModule = computerTableModule;
20         } 
21 
22         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
23         {
24             var result = new GetComputerByComputerIdResponse(); 
25 
26             var dbComputer = this._computerTableModule.GetComputerById(request.ComputerId);//从数据源中获取Computer集合
27             var dominModel = dbComputer.ConvertToDomainModelFromDatasourceDto();//转换成DomainModel 
28 
29             var filetedModel = this._searchComputerTransactionCommand.FilterComputerResource(dominModel);//执行业务逻辑过滤 
30 
31             return result; 
32 
33         }
34     }
35 }
View Code

控制器中不直接调用业务层的方法,而是先获取数据而后执行转换在进行业务逻辑处理。这里须要澄清的是,此时我是将读写混合在一个逻辑项目里的,因此大部分的查询没有业务逻辑处理,直接转换成服务DTO返回便可。将读写放在一个项目能够共用一套业务逻辑模型。固然仅是我的见解。

这个是业务层将是彻底独立的,咱们能够对其进行充分的单元测试,包括迁移和公用,甚至你能够想着领域特定框架发展。

5.2.将业务层直接依赖数据层的关系使用IOC思想改变数据层依赖业务层(业务层将绝对独立)(比较优雅)  

上面那种使用业务层和数据层的方式你也许以为有点别扭,那么就换成使用本节的方式。

以往咱们都是在业务层中调用数据层的接口来获取数据的,此时咱们将直接依赖数据层,咱们能够借鉴IOC思想,将业务层依赖数据层进行控制反转,让数据层依赖咱们业务层,业务层提供依赖注入接口,让数据层去实现,而后在业务命令对象初始化的时候在动态的注入数据层实例。

若是你已经习惯了使用事物脚本模式来开发项目,不要紧,你可使用此模式来将数据层完全的隔离出去,你也能够试着在应用控制器中帮你分担点事物脚本的外围功能。

6.总结

文章中分享了本人以为到目前来讲比较可行的企业应用架构设计方法,并不能说彻底符合你的口味,可是能够是一个不错的参考,因为时间关系到此结束,谢谢你们。

 

相关文章
相关标签/搜索