代码改变世界

对企业级应用开发的思考--分层

2015-02-27 11:58 by 李玉宝, ... 阅读, ... 评论, 收藏, 编辑

    首先声明,本文并不是介绍什么是N层架构,然后给张分层图,最后来一堆代码结束。本文主要是对分层过程中常常让人感到困惑地方的思考,以及最近园子里面讨论异常激烈的一些问题的再讨论。本文从个人经验角度出发,努力尝试来解决这些困惑,欢迎拍砖,但,如果你进行人生攻击,我也只能在心里画个圈圈诅咒你一下!

开始

我们先从一幅大家眼熟能详的图开始:

这是应用开发人员最熟悉的N层架构图,其中:

  • 数据访问层:应用程序中全权负责与数据存储对话并持久保存和检索业务对象的层。通常,数据访问层包括所有的CRUD 方法与查询机制,使得业务逻辑层能够针对任何给定的条件检索对象。
  • 业务逻辑层:它包含定义和处理复杂业务功能的所有规则、工作流和验证逻辑,设计软件以满足这些复杂的功能;困惑最多的地方就是这一层。
  • 应用层:封装业务模型,并为所有相关的应用程序提供接口。关于应用层,有一个十分让人困惑的地方,后面会详细说明。

    这是三层的简单定义,当开发者看到这些定义的时候,基本都会有这么一个感觉:哦!也就这么回事。可是在实际编写代码时,尤其是随着项目代码量越来越多,业务越来越复杂的时候,会明显觉得:

    要改这个业务,又要去改数据层的CRUD,太难受了;

    这个操作不分层,我一个函数调用就搞定了,为什么一分层,我要嵌套这么多层,太恶心了;

    ...

当你有这些感觉的时候,请停下手头上的工作,思考一下,有没有因为遇到下面这些情况让你感觉到困惑。

 

困惑1:层之间的依赖关系

 

    该图引用自jesse liu的博客(如有侵权请告知,我会在第一时间修改,实在是懒得画),我觉得很形象。上面是常见的三层中的依赖关系。下面是在领域驱动设计下的依赖关系。不要小看这个图,可以肯定的是90%以上的开发人员在开发过程中对N层架构的依赖关系是这样的,哪怕是在使用领域驱动进行设计时,无形中也摆脱不了这种依赖关系。初学者一般觉得这种依赖关系很正常啊?本来就应该是这样的。造成这种思想的根本原因是初学者通常觉得DAL层(或DDD中的repository)可以方便给其他系统调用呀?不就实现复用了吗?我的BLL层也可以....

    打住,请打住!!你的BLL层能复用?你的BLL层已经在依赖DAL了。

    DAL能复用?是能复用。但,对于特定的开发系统来说,DAL层的复用毫无意义。你会把图书管理系统中的DAL复用给博客网站吗?除了能复用最基本的ADO操作之外,你什么都复用不了。

    所以,层之间的引用应该设计成这样的:

    好吧,图还是jesse liu的。这个图很形象的说明了我们该如何处理层之间的依赖关系。因为系统中真正可以复用的其实是这样的BLL层。它不依赖任何层,对于特定的系统来说,无论你的数据库变了,界面变了,但核心的业务其实是比较稳定的。其实该实现方式的核心思想就是大名鼎鼎的依赖倒置。实现方式可以使用反射或IOC等进行,可以参考园中其他小伙伴的文章或我的另一文:通用数据采集平台,从架构到代码 。

 

困惑2:业务逻辑层实现方式的选择

    业务逻辑层实现方式有三种:事务脚本活动记录领域模型。现在园子里面大兴DDD之风,对领域驱动设计推崇倍至,你要是和他说其实这个用事务脚本封装一下,那个来点活动记录集搞搞就行了。保证他立马喷的你体无完肤。常言道:存在即合理。前两种实现模式自打程序设计出现以来,有着悠久的历史。这里我们简单介绍一下它们及说明一下他们的适用场景。

 

事务脚本(Transaction Script)

    名字叫的很玄乎,其实说直白点它就是使用一系列功能函数来实现系统的业务逻辑。它遵循面向过程的开发方式,而不是面向对象的方法。核心思想是为每个业务创建一个过程,每个过程都包含完成业务事务所需要的所有业务逻辑,包括从工作流、业务规则和验证检查到数据库持久化保存的所有内容。

    从各大经典教材上来看,它适用于具有很少逻辑或没有多少逻辑的简单应用程序,在用户界面中实现所有的业务逻辑。在实际操作中,将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。这时采用自动化程度最高的用户界面创建工具(比如ASP.NET中的服务器控件)和可用的可视化编程工具进行开发。

 

活动记录(Active Record)

    该模式对于数据库中的每个表都存在一个对应的业务对象。业务对象代表数据表中的一行,在业务对象中包含数据和行为,同时包含用于持久化对象的方式及添加新实例和查找数据集合的方法。

    适用于业务只是在数据库之上加一个显示处理界面。在有些精典书籍中提出,以EF与linqtosql为代表的数据访问对象模式(DAO)最适合的就是这种场景。它们都通过一个数据上下文(DbContext)作为入口,实现业务对象与数据表的对应。但个人认为EF(linqtosql用的较少)经过多个版本的演化,已经摆脱了一对一映射的限制。完全适用于领域模型。

 

领域模型(Domain)

    该实现模式与活动记录非常相似,它与活动记录集的主要差异就是:领域模型的业务实体不知道如何持久化自身,且数据模型与业务模型之间并不是一定要存在一对一映射的关系。

    关于该模型的文章有很多。这里就不详细介绍。它适用于对复杂业务逻辑进行建模,至于这个复杂业务逻辑的标准是什么,我也在探索中。我想有些开发者使用它是为了表现自己的技术能力。其实完全没有必要。个人觉得理解它的思想就行,在一些小项目中能不用尽量不要去用。折腾自己也折腾同事。

    这里要特别说明一点的是:因为业务实体并不知道自己如何持久化,所以领域模式依赖于ORM或Repository模式来持久化。我们在这里的依赖并不是在设计时领域要依赖下面的数据操作,而是领域层最终要靠ORM或Repository来实现数据存储的读写。DDD设计中最大的争论也就此展开:Repository模式到底有没有必要,尤其在使用EF的情况下。我们单独为此开一个专题。

 

困惑3:Repository模式

Repository的争论曾经在园子深挖DDD的几位牛人圈子里面持续了一段时间:

初探领域驱动设计(2)Repository在DDD中的应用

Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念

尤其是当用EF实现Repository时,争论可以达到惨烈的程度: 

博客园的大牛们,被你们害惨了,Entity Framework从来都不需要去写Repository设计模式

那段时间我也是看着他们的文章,陪着他们一起困惑,一起纠结。但某个风高月黑的晚上。我突然间有了以下想法:

    领域模型的业务实体不知道如何持久化自身,如果我们想把这些实体存储到电脑中,我们必定需要一个可以持久化的方法。方法太多了,ADO.NET、linqtosql、EF等等,于是我们提取一个数据化的接口在业务逻辑层,取个名字叫Ixx..叫什么呢?Martin Fowler大大说叫IRepository吧,于是Repository模式就出来了。记住:这里在业务逻辑层里面添加的是接口,而不是实现,这很重要!

    其实我赞同Leo C.W在那篇慷慨激昂的文章里面提到的观点:EntityFramework 本身就是基于Repository设计的,我也赞同他说的那些EF+Repository包裹后的缺点。但,这不足以让我们抛弃Repository,哪怕是和EF做搭档。当他们两个一起工作的时候,Repository可以理解为设计模式中的适配器模式。为了保持整个设计的接口统一,为了协调领域和数据映射层。因为如果在业务逻辑层里面直接使用EF,你的领域就已经不再是纯洁的领域。所以我在上一段结尾的时候强调了在业务逻辑层里面添加的是IRepository接口。

    而且就像我在困惑2中提到的一样,当你选择使用领域模型的时候,就可以假定你所处理的业务逻辑比较复杂,而且需要兼顾扩展性(比如数据库更换),所以Repository有存在的意义。如果你的项目属于短期的项目,或者说你不用考虑更换数据访问层,你大可直接选择活动记录模式,那么如果你不想用Repository,那就不用。不然直白点说:你用领域就是为了装那个啥。

 

困惑4:应用层

    很多人觉得这一层很简单,就是处于界面与业务逻辑之间的一个外观模式。但,就像田园里的蟋蟀说的一样:

    有时候我们在领域驱动设计的时候最容易混淆的就是应用层和领域层,网上关于领域层和应用层的定义概念一搜一大把,你可能也会说几句,比如什么应用层是很薄的一层,主要工作是协调任务的等等,但是实践起来呢?用代码表示就蒙了。

    对于某个XXService,你会无比纠结它到底是放在业务逻辑层中还是应用层中,尤其是像我这种对代码整齐有洁癖的人,更是痛不欲生。蟋蟀后来提出:

应用层很薄,所做的工作是:

  1. 发起一个请求
  2. 确认处理结果
  3. 提交工作单元

    看到他的这个观点,瞬间让我想到《ASP.NET设计模式》这本书中的一个观点:Document Message消息传送模式。作者在应用层里面唯一做的一件事情就是把所有的界面逻辑封装成一个Request,然后通过业务逻辑层中的功能组合计算出一个Response,返回给界面(详见该书6.3.2章节)。

 

 

    综合上面的两点经验,也算是给我们指明了一条怎样使应用层“变薄”的出路了。

总结

    真正对分层进行研究是因为公司里面一个水务集团的项目,数据量业务逻辑都相对比较复杂,当时硬着头皮要求使用领域驱动设计,结果很悲惨。项目一半的时候大家都被折磨疯了,那时才知道我们离真正的建模差距有多大。我们以前的分层,无非是借着面向对象的外壳,使用类来进行面向过程的分层罢了。中间为了设计模式而设计模式,为了分层而分层。后来项目虽然完工,但远没达到我心目中的期望。于是静下心来,看了园子里面很多这方面的文章,记录下些许心得。还请各位大大不吝指教,感谢!