阅读文件夹:
- 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.数据层:用来提供对详细的数据源引擎的訪问。主要用来直接存取数据,不包含业务逻辑处理。
其有用文字描写叙述这三个层的基本职责还非常是比較easy的,可是不同的人怎样理解并设计这三个层就形态各异了,反正我是看过非常多各种各样的分层结构,各有各的特点,从某个角度讲都非常不错,可是都显得有点乱。由于没有一个统一的架构模式来支撑。代码中充满了对分层架构的理解错位的地方,比方:常常看见将“事物脚本”模式和“表模块”模式混搭使用的,导致我最后都不知道把代码写在哪里。提取出来的代码也不知道该放到哪个对象里。
层虽简单可是要想运用的好不easy,毕竟我们还是站在一个比較高的层面比較笼统的层面来谈论分层结构的。一旦落实到代码上就全然不一样了。用不用接口来隔离各层,接口放在哪个层里,这些都是非常微妙的。当然本文不是为了说明我所介绍的设计是多么的好。而是给大家一个能够參考的样例而已。
言归正传。三个层之间的调用要严格依照“上层仅仅能调用直接下层。不可以越权,而下层也不可以调用自己的上层”,这是主要的层结构的调用约束,仅仅有这样才干保证一个好的代码结构。显示层仅仅能调用业务层。业务层也仅仅能调用数据层,事实上就是这么简单,当然详细的代码设计也可以大概归纳为两种,第一种是实例类或静态类直接调用。另外一种是每一个层之间加上接口来隔离每一个层,使得測试、部署easy点。可是假设用的不好的话效果不大反而会添加复杂度,还不如直接使用静态类来的直接点,可是用静态类来设计业务类会使多线程操作非常难实施,略微不注意就会串值或报错。
3.企业级应用分层架构(现代分层架构的基本演变过程)
上节中我们基本了解了传统三层架构的类型和职责,本节我们来简介一下现代企业应用分层架构的类型和职责。
随着企业应用的复杂度添加,在原有三层架构上逐渐演化出如今的面向企业级的分层架构,这样的架构能非常好的支持新的技术和代码上的最佳实践。
在传统的三层结构中的业务层之上多了一个应用层也但是说是服务层。该层是为了直接隔离显示层来调用业务层。由于如今的企业应用基本上都在往互联网方向发展。对业务逻辑的訪问不会在是从进程内訪问了,而是须要跨越网络来进行。
有了这一层之后会让原本显示层调用业务层的过程变得灵活非常多,我们能够加入非常多灵活性在里面,更为重要的是显示层和业务层两个独立的层要想全然独立是必需要有一个层来辅助和协调他们之间的互动的。在最早的三层架构的书籍中事实上也提到了“服务层”来协调的作用,为什么我们非常多的项目都不曾出现过,当我自己看到书上有解说后才恍然大悟。(该部分能够參考:《企业应用架构模式》【马丁.福勒】。第二部分。第9章“服务层”)
图1:(逻辑分层)
应用层中包括了服务的设计部分。应用层的概念略微大一点,里面不仅不含了服务还包括了非常多跟服务不相关的应用逻辑。比方:记录LOG,协调基础设施的接入等等,就是将服务层放宽了理解。
图2:(项目结构分层)
在应用层中包括了我们上述所说的”服务“。将”服务层“放宽后形成了如今分层架构中至关重要的”应用层“。应用层将负责总体的协调”业务层“和”数据层“及“基础设施”,当然还会包括系统执行时环境相关的东西。
3.1.服务层中应用契约式设计来解决动态条件不匹配错误(通过契约式设计模式来将问题在线下暴露出来)
此设计方法主要是想将动态执行时条件不匹配错误在线下自己主动化回归測试时就暴露出来。由于服务层中的契约可能会面临着被改动的危急性,所以我们无法知道我们本次上线的契约中是否包括了不稳定的条件不匹配的危急。
利用契约式设计模式能够在调用时自己主动的运行契约公布方预先设定的契约检查器,契约检查器分为前置条件检查器和后置条件检查器。我们来看一个简单的样例。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace CompanySourceSearch.Service.Contract
{
using CompanySourceSearch.ServiceDto.Request;
using CompanySourceSearch.ServiceDto.Response; public interface ISearchComputer
{
GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request);
}
}
在服务契约中我定义了一个用来查询企业中电脑资源的接口。好的设计原则就是不要直接暴露查询字段而是要将其封装起来。
namespace CompanySourceSearch.ServiceDto
{
public abstract class ContractCheckerBase
{
private Func<bool> checkSpecification;
public Func<bool> CheckSpecification
{
get
{
return this.checkSpecification;
}
private set
{
this.checkSpecification = value;
}
} public void SetCheckSpecfication(Func<bool> checker)
{
CheckSpecification = checker;
} public virtual bool RunCheck()
{
if (CheckSpecification != null)
return CheckSpecification(); return false;
}
}
}
然后定义了一个用来表示契约检查器的基类,这里纯粹是为了演示目的,代码略微简单点。服务契约的请求和响应都须要通过继承这个检查器类来实现自身的检查功能。
namespace CompanySourceSearch.ServiceDto.Request
{
public class GetComputerByComputerIdRequest : ContractCheckerBase
{
public long ComputerId { get; set; } public GetComputerByComputerIdRequest()
{
this.SetCheckSpecfication(() => ComputerId > 0/*ComputerId>0的检查规则*/);
}
}
}
Request类在构造函数中初始化了检查条件为:ComputerId必须大于0。
namespace CompanySourceSearch.ServiceDto.Response
{
using CompanySourceSearch.ServiceDto; public class GetComputerByComputerIdResponse : ContractCheckerBase
{
public List<ComputerDto> ComputerList { get; set; } public GetComputerByComputerIdResponse()
{
this.SetCheckSpecfication(() => ComputerList != null && ComputerList.Count > 0);
}
}
}
相同Response类也在构造函数中初始化了条件检查器为:ComputerList不等于NULL而且Count要大于0。
还是那句话样例是简单了点。可是设计思想非常不错。
对前置条件检查器的运行能够放在client代理中运行,当然你也能够自行去运行。
后置条件检查器事实上在普通情况下是不须要的,假设你能保证你所測试的数据是正确的,那么作为自己主动化測试是应该须要的。当时维护一个自己主动化測试环境非常不easy,所以假设你用后置条件检查器来检查数据动态变化的环境时是不太合适的。
3.2.应用层中的应用控制器模式(通过控制器模式对象化应用层的职责)
应用层设计的时候大部分情况下我们都喜欢使用静态类来处理,静态类有着良好的代码简洁性,并且还能带来一定的性能提升。可是从长远来考虑静态类存在一些潜在的问题,数据不能非常好的隔离。反复代码不太好提取,单元測试不太好写。
为了可以在非常长的一段时间内似的项目维护性非常高的情况下还是建议将应用控制器使用实例类设计,这里我喜欢使用“应用控制器”来设计。它非常形象的表达了协调前端和后端的职责,可是详细不处理业务逻辑,与MVC中的控制器非常像。
namespace CompanySourceSearch.ApplicationController.Interface
{
using CompanySourceSearch.Service.Contract;
using CompanySourceSearch.ServiceDto.Response;
using CompanySourceSearch.ServiceDto.Request; public interface ISearchComputerApplicationController
{
GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request);
}
}
在应用控制器中我们定义了一个用来负责上述查询Computer资源的的控制器接口。
namespace CompanySourceSearch.ApplicationController
{
using CompanySourceSearch.ApplicationController.Interface;
using CompanySourceSearch.ServiceDto.Request;
using CompanySourceSearch.ServiceDto.Response; public class SearchComputerApplicationController : ISearchComputerApplicationController
{
public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
{
throw new NotImplementedException();
}
}
}
控制器实现类。
这样能够非常清晰的分开各个应用控制器,这样对服务实现来说是个非常不错的提供者。
namespace CompanySourceSearch.ServiceImplement
{
using CompanySourceSearch.Service.Contract;
using CompanySourceSearch.ServiceDto.Response;
using CompanySourceSearch.ServiceDto.Request;
using CompanySourceSearch.ApplicationController.Interface; public class SearchComputer : ISearchComputer
{
private readonly ISearchComputerApplicationController _searchComputerApplicationController; public SearchComputer(ISearchComputerApplicationController searchComputerApplicationController)
{
this._searchComputerApplicationController = searchComputerApplicationController;
} public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
{
return _searchComputerApplicationController.GetComputerByComputerId(request);
}
}
}
服务在使用的时候仅仅须要使用IOC的框架将控制器实现直接注入进来即可了,当然这里你能够加上AOP用来记录各种日志。
通过将控制器依照这种方式进行设计能够非常好的进行单元測试和重构。
3.3.业务层中的命令模式(事务脚本模式的设计模式运用,非常好的隔离静态数据)
在一般的企业应用中大部分的业务层都是使用"事务脚本"模式来设计,所以这里我认为有个非常不错的模式能够借鉴一下。可是非常多事务脚本模式都是使用静态类来处理的,这一点和控制器使用静态类相似了,代码比較简单,使用方便。可是依旧有着几个问题,数据隔离,不便于測试重构。
将事务脚本使用命令模式进行对象化。进行数据隔离,測试重构都非常方便。假设你有兴趣实施TDD将是一个不错的结构。
namespace CompanySourceSearch.Command.Interface
{
using CompanySourceSearch.DomainModel; public interface ISearchComputerTransactionCommand
{
List<Computer> FilterComputerResource(List<Computer> Computer);
}
}
事务命令控制器接口,定义了一个过滤Computer资源的接口。
你可能看见了我使用到了一个DominModel的命名空间。这里面是一些跟业务相关的且通过不断重构抽象出来的业务单元(有关业务层的内容后面会讲)。
namespace CompanySourceSearch.Command
{
using CompanySourceSearch.Command.Interface; public class SearchComputerTransactionCommand : CommandBase, ISearchComputerTransactionCommand
{
public List<DomainModel.Computer> FilterComputerResource(List<DomainModel.Computer> Computer)
{
throw new NotImplementedException();
}
}
}
使用实例类进行业务代码的组装将是一个不会懊悔的事情。这里我们定义了一个CommandBase类来做一些封装工作。
应用控制器相同和服务类一样使用IOC的方式使用业务命令对象。
namespace CompanySourceSearch.ApplicationController
{
using CompanySourceSearch.ApplicationController.Interface;
using CompanySourceSearch.ServiceDto.Request;
using CompanySourceSearch.ServiceDto.Response;
using CompanySourceSearch.Command.Interface; public class SearchComputerApplicationController : ISearchComputerApplicationController
{
private readonly ISearchComputerTransactionCommand _searchComputerTransactionCommand;
public SearchComputerApplicationController(ISearchComputerTransactionCommand searchComputerTransactionCommand)
{
this._searchComputerTransactionCommand = searchComputerTransactionCommand;
} public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
{
throw new NotImplementedException();
}
}
}
到眼下为止每一个层之间坚持使用面向接口编程。
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);
}
}
我们使用"表入口“数据层模式来定义了一个用来查询Computer的方法。
namespace CompanySourceSearch.ApplicationController
{
using CompanySourceSearch.ApplicationController.Interface;
using CompanySourceSearch.ServiceDto.Request;
using CompanySourceSearch.ServiceDto.Response;
using CompanySourceSearch.Command.Interface;
using CompanySourceSearch.Database.Interface;
using CompanySourceSearch.DatasourceDto;
using CompanySourceSearch.Application.Common; public class SearchComputerApplicationController : ISearchComputerApplicationController
{
private readonly ISearchComputerTransactionCommand _searchComputerTransactionCommand;
private readonly IComputerTableModule _computerTableModule;
public SearchComputerApplicationController(ISearchComputerTransactionCommand searchComputerTransactionCommand,
IComputerTableModule computerTableModule)
{
this._searchComputerTransactionCommand = searchComputerTransactionCommand;
this._computerTableModule = computerTableModule;
} public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
{
var result = new GetComputerByComputerIdResponse(); var dbComputer = this._computerTableModule.GetComputerById(request.ComputerId);//从数据源中获取Computer集合
var dominModel = dbComputer.ConvertToDomainModelFromDatasourceDto();//转换成DomainModel var filetedModel = this._searchComputerTransactionCommand.FilterComputerResource(dominModel);//运行业务逻辑过滤 return result; }
}
}
控制器中不直接调用业务层的方法,而是先获取数据然后运行转换在进行业务逻辑处理。这里须要澄清的是。此时我是将读写混合在一个逻辑项目里的,所以大部分的查询没有业务逻辑处理,直接转换成服务DTO返回就可以。
将读写放在一个项目能够共用一套业务逻辑模型。当然仅是个人看法。
这个是业务层将是全然独立的,我们能够对其进行充分的单元測试,包含迁移和公用。甚至你能够想着领域特定框架发展。
5.2.将业务层直接依赖数据层的关系使用IOC思想改变数据层依赖业务层(业务层将绝对独立)(比較优雅)
上面那种使用业务层和数据层的方式你或许认为有点别扭,那么就换成使用本节的方式。
以往我们都是在业务层中调用数据层的接口来获取数据的,此时我们将直接依赖数据层,我们能够借鉴IOC思想,将业务层依赖数据层进行控制反转,让数据层依赖我们业务层,业务层提供依赖注入接口,让数据层去实现。然后在业务命令对象初始化的时候在动态的注入数据层实例。
假设你已经习惯了使用事物脚本模式来开发项目。没关系,你能够使用此模式来将数据层彻底的隔离出去,你也能够试着在应用控制器中帮你分担点事物脚本的外围功能。
6.总结
文章中分享了本人认为到眼下来说比較可行的企业应用架构设计方法,并不能说全然符合你的口味。可是能够是一个不错的參考,因为时间关系到此结束,谢谢大家。
作者:王清培
出处:http://blog.csdn.net/wangqingpei557
本文版权归作者和CSDN共同拥有。欢迎转载,但未经作者允许必须保留此段声明,且在文章页面明显位置给原始连接,否则保留追究法律责任的权利。
版权声明:本文博主原创文章。博客,未经同意不得转载。