结构分层的经验谈
http://www.agilelabs.cn/blogs/linkin/archive/2006/06/06/1298.aspx
为了将业务规则从界面和数据库中剥离出来,通常的做法是抽象出一个业务逻辑层出来,专门负责对业务逻辑进行处理。一般多采用三层结构,既表现层,业务层和数据层。
当开发人员在以前的两层结构中痛苦煎熬了很长一段时间,突然看到了三层结构的解决方案的时候,一般会有终于找到了救世主的感觉。但是这种感觉往往会导致掉到另外一个同样恐怖的陷阱“过度设计”中。在我以前曾经供职的一家公司,以前都是把SQL语句直接写在ASPX页面的,后来在读到了一些关于多层结构方面的资料之后,一下子又把整个系统分成了:表现层(ASPX)、接口外观层(IF),业务外观层(BF),数据访问外观层(DAF),数据访问层(DA)和数据访问组件(SQLHelper)。但是我并没有吸取教训,导致后来也犯了同样的错误。
犯错误的原因有很多,不过主要是因为没有一个比较明确的如何分层的指导性原则。假如说我们分层的原则是为了抽象逻辑,分三层的原因是要让业务逻辑和界面及数据库解除耦合,那么如果按照这个分层原则,我把逻辑重新归类更加细的分为四层、五层、六层行不行呢?如果不行,那是什么原因不行呢?在没有正确的原则指导下,分层技巧很容易被滥用,导致分出许多没有必要的层出来。无端的增加了开发和维护成本,以及更重要的是增加了重构的代价,降低了团队的敏捷能力。
面向对象架构设计大师Martin Fowler在介绍如何设计分布式系统的时候曾说过:分布式系统的设计原则的第一条是,不要使用分布式。他的意思当然不是说要绝对禁止使用分布式设计,而是劝导人们尽量把问题简单化。能不分布式设计的,就不要分布式设计。
我套用他的这句话提出我对分层的感受就是:多层结构系统的设计原则第一条是,不要使用多层结构。
当然我的意思也并不是说层数越少就越好,而是希望你能清醒的认识到增加层数会增加结构的复杂性,不要轻易的作出分层的决定,一定要到感觉必须要增加一层才能解决问题的时候,再来决定增加一层。
过多的层次除了会给系统带来不必要的复杂性外,还会影响你的系统结构设计。如果你打算采用面向对象的领域模型来设计系统的话,在业务系统内的分层会给面向对象系统的设计带来很多麻烦,会很容易走回到事务脚本的老路上去。关于这一点,我在面向对象系统设计经验谈这篇BLOG里详细的谈到过,这里就不再赘述。
下图是我们最终定型的应用系统结构层次:
总结一下:
建立一个完全面向对象建模的领域模型层,让这个层尽量处理多的业务逻辑。其它层尽可能的薄一点,把业务逻辑都转移到领域模型层中。
UI尽可能和领域模型贴近一点,中间不要经过太多中转,物理边界也尽可能的少。
业务对象只能有一套,也就是领域模型。只要出了领域模型层,外面全部是零散数据,没有对象的概念。
只有在领域模型层才可以处理对象。
如果一定要分布式。全部用简单数据类型通过接口访问领域模型。
这个分层结构其实是经历了多次精简完成的,所有的感触都归结为一句话:不要过度设计,简单就是美。
posted on 2006年6月6日 10:00 由 卢彦
Comments
# re: 结构分层的经验谈
2006年6月7日 13:01 by a512225508
确实过分了就不好了
# re: 结构分层的经验谈
2006年6月7日 17:26 by edward
5、如果一定要分布式。全部用简单数据类型通过接口访问领域模型。
这一点在理解上没有问题, 在实施上可能会比较困难, 毕竟一个服务接口定义一个对象参数会比定义几十个基本类型参数方便的多,而且似乎也没有功能上的缺失。
不知道卢彦能否讲的更为详细点。 :)
# re: 结构分层的经验谈
2006年6月7日 17:45 by 卢彦
定义复杂的DTO对象没有必要。你仔细想想看,在界面上先要把几十个基本类型值赋给DTO,然后到了领域模型又要把几十个值再赋给领域模型。这个过程跟在界面上把几十个值赋给方法作为参数,然后到了领域模型再把参数交给领域模型的代价是完全相同的。
而最主要缺点是为相同的业务模型建立多套不同用途的对象会容易搞混淆,造成维护成本上升。
想想看C#里的匿名对象,这个道理是一样的。
# re: 结构分层的经验谈
2006年6月7日 18:13 by edward
如果直接将领域模型里的对象作为服务端客户端交互的接口参数呢?
# re: 结构分层的经验谈
2006年6月7日 20:15 by 卢彦
你认为这种做法在实施上更容易吗。
对象往往不是独立的,而是相互之间有很复杂联系的。如果你切断对象的所有连接关系只传单个对象到客户端,那么你就无法获得面向对象对象最基本的连接好处。
要么你把整个对象树都传递到客户端去,这样做无疑是很有性能问题的。并且还会很容易造成并发冲突。而像Nhibernate这样的ORM框架,你还要考虑SessionScope的问题,否则就不能LazyLoad。
# re: 结构分层的经验谈
2006年6月8日 9:16 by edward
假设客户端要修改一份订单,那他对服务方法IOrderService.GetOrder的访问你认为应该只返回Order对象的数据呢?还是应该同时返回OrderItem的数据?我相信你会说, 这个取决于具体的场景,因为如果在大多数的使用场景中用户都会修改OrderItem的话,那就应该传递。
那么我的观点是,在具体到某一个场合的时候,不会出现你所说的让你犹豫不决的情况,因为你在写一个服务方法的时候已经明确的知道要返回什么样的数据。
当然,我相信你也早已确认了我说的这一点,那存在争议的一点就是:能不能直接将业务实体传递到客户端。我认为可以,至少客户端还是能获得一些面向对象的好处。我不认为客户端纯粹面向数据是一个好的想法。
# re: 结构分层的经验谈
2006年6月8日 11:48 by 卢彦
你有点主观臆断我的想法了。无论用户是否会修改OrderItem,我都不会建议传递Order的同时传递OrderItem。
并且传递出去的数据根本就不是Order对象了,而是一组只有显示意义字符串。
我不知道你为什么要让客户端获得面向对象的好处,我也不明白你如何让客户端如何获得对象的好处。对象的好处仅仅体现在处理业务逻辑上,但是客户端却应该是不负责处理任何逻辑的。而且对象对于显示来说也并没有体现出任何好处。
当然,如果在你的设计中,可以允许把业务逻辑放到表现层中,那么我们就没有什么好讨论的了。因为我们最基本的设计原则都不一致。
# re: 结构分层的经验谈
2006年6月8日 12:07 by edward
客户端在绑定数据显示的时候,直接绑定到对象上会比绑定到一个datatable上更不容易出错,而且对于客户端开发人员来说,面向对象思考的时候思路会更加清晰。
当然,此时在客户端的对象仅仅是一些vo而已, 并不负责处理任何业务逻辑。
# re: 结构分层的经验谈
2006年6月8日 14:56 by 卢彦
你刚才不是一直在说把业务对象传到客户端吗?
怎么又变成了VO?
如果是VO的话,讨论话题又循环回去了,请参考我的第一个回复。
我比较纳闷,为什么直接绑定到对象上会比绑定到一个datatable更不容易出错?
而且你认为
datagrid.DataSource=dto;
就比
datagrid.DataSource=datatable;
思路更清晰吗?
# re: 结构分层的经验谈
2006年6月8日 16:20 by edward
我想你并没有尝试去理解我的想法, 呵呵
# re: 结构分层的经验谈
2006年6月8日 16:37 by 卢彦
你的想法就是我以前的想法,我不仅仔细的思考过,还都实际的做过。以前我们就是用VO,DTO的,并且在这种方式下我们几乎做了半年时间。
不过这种做法现在已经在新的重构中被废弃了,即使是代价很高。原因是因为引入了不必要的复杂性,且并没有带来任何实际好处。
所以,我反而觉得你并没有理解我的想法。
# re: 结构分层的经验谈
2006年6月9日 10:32 by edward
不明白你为什么不建议在同一个处理事务中一次性的将所需的数据全部取到客户端,难道分多次取反而性能更高吗?事实上我们原来的架构就是基于即时需要即时去服务端取的模式,这样频繁的连接服务端对系统性能造成了很大的影响。
再者,最终用户面向的是数据,但是开发人员呢?你确实认为开发人员编程时操作一个datatable会比一个强类型的对象更加方便的话,我只能认为这是一种思维的倒退。而且,就算客户端不处理任何业务逻辑,但是一样还会有UI逻辑,对吧。 举个例子:客户端在维护用户数据的时候,在输入用户数据的同时, 要给这个用户赋上关联角色。你不用面向对象的思维会怎么处理?用一个包括两个datatable的dataset?其实你这个时候还是在面向对象,不是吗? 你在具体编码的时候, 还是会把一个datatable内的数据转换成用户的概念,然后把另外一个datatable的数据转换成角色的概念。
我想表达的观点是:不管在UI层是否处理业务逻辑,但是对于开发者而言,在编程的时候一定会针对自己处理的数据自动映射到领域模型。就是说,业务模型一定是存在于并且应该存在于项目开发各个层次的开发者思维中。
还有,讨论问题的时候保持平和的心态更有利于沟通。 :)
# re: 结构分层的经验谈
2006年6月9日 12:12 by 卢彦
不明白你为什么不建议在同一个处理事务中一次性的将所需的数据全部取到客户端
-----------
这因为你无法预测客户会需要什么样的数据。比如说,你把用户对象传递到客户端,但是用户很可能会要求在用户显示数据后加一列显示他今天购买物品的总价值。你怎么办?把购买物品的所有对象也传过去?那客户又要求显示一个客户的历史返点平均值呢?为了满足这样的需求,最终会导致你把整个对象树传递到客户端。并且还要在客户端做比较复杂的业务逻辑操作。
事实上我们原来的架构就是基于即时需要即时去服务端取的模式,这样频繁的连接服务端对系统性能造成了很大的影响。
-----------------
我认为这是不可能发生的。想想看所有的B/S结构的程序,是不是每个请求都必须发回给服务器端做处理?照你的说法,那每个B/S结构的系统都有非常严重的性能问题。
在输入用户数据的同时, 要给这个用户赋上关联角色。你不用面向对象的思维会怎么处理?
-------------
当然要用面向对象的思维去处理。不过不是在客户端,而是在服务器端处理。所以不会用一个包括两个datatable的dataset。
我想表达的观点是:不管在UI层是否处理业务逻辑,但是对于开发者而言,在编程的时候一定会针对自己处理的数据自动映射到领域模型。
------
问题就是各种自己处理的数据无法“自动”映射到领域模型。
业务模型一定是存在于并且应该存在于项目开发各个层次的开发者思维中。
------
为什么要分层呢?请问你分层的目的是什么?而领域模型的目的又是什么?我分层的目的是为了抽象出业务逻辑,领域模型的用途也仅仅是处理业务逻辑。如果分了层,但是业务逻辑继续分布在各个层中,那分层只能是流于形式的一种做法。
讨论问题的时候保持平和的心态更有利于沟通。
-------
我的心态很平和。我想你应该也一样。
# re: 结构分层的经验谈
2006年6月9日 12:13 by 刘乐光
正如段兄所说,这种方式在某些情况会损失一些性能,但我们可以采取其他的办法来降低这个损失,比如客户端和服务器的缓存,还有很多地方可以做优化。其实我们在采用这种方式前也有很多争论,很多问题和段兄提出的一样。就像“在同一个处理事务中一次性的将所需的数据全部取到客户端”这点,很难把握我取多少数据到客户端,我在客户端到底做哪些工作,很容易就把业务相关的工作从领域模型转移到客户端了,这样没有一个可以把握的标准,我们就宁愿走点极端,我们更需要编程模型的一致。另外段兄所举的那个用户角色的例子,这样处理是否能理解:在UI上添加用户,同时赋予角色,UI上其实是没有user和role这两个对象的,只有代表用户名,年龄,角色ID等等的参数,将这些参数传给服务器后,先创建一个user对象,根据角色ID查询到角色对象。。。。一些对象间的操作都在这里处理。总的原则是不要让业务逻辑到处扩散,造成维护的困难。另外,linkin一讨论问题就那种风格,段兄可能还不大适应,千万不要误会。:)
# re: 结构分层的经验谈
2006年6月12日 11:40 by edward
我意识到这种分离确实能让领域模型更加独立。
而且乐光兄弟所举的例子也是我一直赞同的。
分歧在于能否直接将业务对象传递到客户端,
比如说一个组织节点类,持久化表用一个type字段标示这个节点是总公司/分公司/部门/其他,如果不允许将业务对象传递到客户端,那么在客户端我必须判断datatable的Type字段,然后做相应的处理。不知道这算不算逻辑分散。
当然,如果前提是客户端已经被定义为只能处理平面数据,那这是问题的,但是相比以前的方式, 客户端编程确实是失去了很多的便捷。不知道你认为我说的对不对?
# re: 结构分层的经验谈
2006年6月12日 15:18 by 卢彦
客户端并不是完全不能处理逻辑,而是不能处理业务逻辑。而显示逻辑当然是由表现层来处理的。
那么在回答你的问题之前,我要请问:你判断Type字段的目的是什么?
如果你判断Type字段来区分是总公司/分公司/部门,然后为不同的机构加上不同的图标,或者是不同的颜色。那么这是属于UI显示逻辑,正是表现层的本职工作。
如果你是要根据Type字段区分出部门,然后根据每个部门做不同的计算规则,那么很明显。这是业务逻辑,就在服务器端算完了然后再把结果放到datatable中传到客户端直接显示。不要再到客户端来根据不同的Type,还要再传一堆相关的对象来做业务处理。
如果硬要说便捷性的话
DataTable方式:
if(depart == "company") .....;
对象方式:
if(depart is company) ....;
我不认为对象方式有很明显的便捷性。
说来说去,只有先回到本质问题,你分层的目的是什么?我想如果有了这个比较明确的目标,然后再讨论话题才能有比较清晰思维。否则我会感觉讨论比较混乱。