BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Spring Web应用的最大瑕疵

| 作者 张龙 关注 14 他的粉丝 发布于 2013年11月9日. 估计阅读时间: 6 分钟 | CNUTCon 了解国内外一线大厂50+智能运维最新实践案例。

众所周知, 现在的Spring框架已经成为构建企业级Java应用事实上的标准了,众多的企业项目都构建在Spring项目及其子项目之上,特别是Java Web项目,很多都使用了Spring并且遵循着Web、Service、Dao这样的分层原则,下层向上层提供服务;不过Petri Kainulainen在其博客中却指出了众多Spring Web应用的最大瑕疵,请继续阅读看看文中所提到的问题是否也出现在你的项目当中。

使用Spring框架构建应用的开发者很乐于谈论依赖注入的好处。但遗憾的是,他们很多人并没有在其应用中很好地利用其优势,如单一职责原则关注分离原则。如果仔细看看基于Spring的Web应用,你会发现很多都是使用如下这些常见且错误的设计原则来实现的:

  • 领域模型对象只是用来存储应用的数据。换句话说,领域模型使用了贫血模型这种反模式。
  • 业务逻辑位于服务层中,管理域对象的数据。
  • 在服务层中,应用的每个实体对应一个服务类。

可问题是:如果这种做法很普遍,那为什么说是不对的呢?下面来阐述一下。

旧习难改

Spring Web应用之所以看起来是这个样子原因在于这是人们长久以来的做法,旧习难改,特别是在高级开发者或是软件架构师强制开发人员这样做的时候。问题在于这些人非常擅于捍卫自己的观点。他们喜欢的一个论调就是应用应该遵循关注分离原则,因为它被划分成了几个层次,每个层次都有自己具体的职责。

一个典型的Spring Web应用会有如下几个层次:

  • Web层:负责处理用户的输入并向用户返回正确的响应。Web层只会与服务层通信。
  • 服务层:作为事务边界。它还负责授权并包含了应用的业务逻辑。服务层管理着领域对象模型并且与其他服务及存储层通信。
  • 存储/数据访问层:负责与所用的数据存储进行通信。

关注分离原则的定义是这样的:关注分离(Soc)是一种将计算机程序划分到不同部分的一种设计原则,这样每一部分都会有单独的关注点。虽然一个典型的Spring Web应用也在一定程度上遵循了这个原则,不过实际情况却是应用拥有一个整体的服务层,它包含了太多的职责了。更具体一些,服务层主要有两个问题:

首先,应用的业务逻辑来自于服务层。

这是个问题,因为业务逻辑散落在服务层。如果需要查看某个业务规则是如何实现的,我们需要先找到它才行,这可不是那么轻松的事情。此外,如果有多个服务类都需要相同的业务规则,那么开发人员很可能会将这个业务规则从一个服务复制到另一个服务中,这会导致维护的梦魇。

其次,每个领域模型类在服务层中都有一个服务类。

这违背了单一职责原则:单一职责原则表明每个类都应该只有一个职责,这个职责应该完全被这个类所封装。它的所有服务都应该与这个职责保持一致。

服务类存在大量的依赖和大量的循环依赖。一个典型的Spring Web应用的服务层没有包含只拥有一个职责的松耦合的服务,它更像是一个紧耦合的大量服务的集合。这使得它很难理解、维护与重用。看起来有点苛刻,不过服务层经常是Spring Web应用最容易出现问题的一环。幸好对我们来说还存在着希望。

推翻

目前的状况并不好,不过也不是完全没有希望的。下面我们来看看如何打破旧有的习惯。

首先,我们需要将应用的业务逻辑从服务层移动到领域模型类中。

为何要这么做呢,看看下面这个例子:

假设我是个服务类,你是个领域模型对象。如果我告诉你从房顶跳下来,那么你是否会拒绝呢?

将服务层的业务逻辑移动到领域模型类中有如下3个好处:

  • 根据合理的方式划分代码的职责。服务层会负责应用的逻辑,而领域模型类则负责业务逻辑。
  • 应用的业务逻辑只会位于一处。如果需要验证特定的业务规则是如何实现的,我们总是知道该去哪里寻找。
  • 服务层的源代码将会变得更加整洁,再不会包含任何复制粘贴的代码了。

其次,我们需要将特定于实体的服务划分为更小的服务,每个服务只有一个目标。

比如说,如果应用有一个服务类,它为与用户帐户相关的人与操作提供了CRUD操作,那么我们就应该将其划分到两个单独的服务类中:

  • 第1个服务提供人的CRUD操作。
  • 第2个服务提供与用户帐户相关的操作。

这么做有如下3个好处:

  • 每个服务类都有一套合理的职责。
  • 每个服务类的依赖会更少,这意味着他们不再是紧耦合的庞然大物了。他们是更加小巧且松耦合的组件。
  • 服务类更易于理解、维护与重用。

这两个简单的步骤可以帮助我们清理应用的架构,提升开发者的生产力和幸福度。现在,我们想知道如果所有这些都是必要的,那么该何时解决这些问题呢?

有时生命是黑白的

我经常听到有人说我们不应该过多的关注于“架构”,因为我们的应用很小并且很简单。虽然这个论调有一定的正确性,不过我们必须要记住一开始很小的项目最后会变得很大。如果不考虑这种情况,那么一旦发生状况,我们就会陷入到巨大的麻烦当中。在未知的水域中航行可不是个好做法,但我们必须要知道,泰坦尼克号在撞到冰山沉没时是在熟悉的航线中航行的。这种事情也会发生在我们的应用中。当事情变得无法控制时,我们必须要有勇气说不。

如果你打算改变,那么我推荐你阅读一下Olivier Gierke所写的“Whoops! Where did my architecture”(或是观看他在SpringOne2GX上关于这个项目的演讲)。但请注意,习惯的力量还是很强大的。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

说的很好 by 黄 海平

给个例子吧;我们总不能,只按上面说的,而不能按上面的做法学吧

Re: 说的很好 by 龙 张

其实可以简单总结一下,就是很多Spring Web项目的领域对象只有属性(还有setter及getter)但却没有行为,所有的行为全部被放到了Service层中,这其实是一种典型的失血模型,失血模型被公认为是一种反模式,作者的意思是应该将Service及领域模型的职责划分开来,该放到领域中的行为要放到领域中,而不能统统都放到Service层中。

从服务层中剥离~想法不错 by 贾 珣

从服务层中剥离~想法不错

.. by lone flying

说实话看不懂。。可能我太菜了吧。。随便说个没看懂的地方:

比如说,如果应用有一个服务类,它为与用户帐户相关的人与操作提供了CRUD操作,那么我们就应该将其划分到两个单独的服务类中:
第1个服务提供人的CRUD操作。
第2个服务提供与用户帐户相关的操作。

完全没理解为什么非得把crud提取出来单独放一个类
还有这个

假设我是个服务类,你是个领域模型对象。如果我告诉你从房顶跳下来,那么你是否会拒绝呢?

这个比喻真的理解不能。。

Re: .. by 龙 张

这个比喻指的是领域模型除了属性还应该有适合自身的行为,还拿这个比喻来说,是否从房顶跳下来不应该是别人让我做的,而是我自己来决定是否应该这样做。

充血模型 by zhang zhijia,.

rails早已证明一开始使用充血模型,然后业务真的复杂到一定程度的时候,抽出service层,才是敏捷之道。

文人相轻自古始然 by 严 海东

领域模型与你所讲的spring mvc,以及di这种东西根本就是解决不同问题的,这样也能作为推翻的依据我觉得有些牵强。承认spring有其局限性,也有误用会导致变得四不像,但是我始终相信每种实现方案往往是经过深思符合实际情况的,有时候理解比所谓的喷击更难得

show me the code by 刘 斌

这种做法确实如你所说,太普遍了.
正因为这样,如果要修正这种做法,请给出实际例子.
show me the code

um... by ma ming

将业务逻辑归还给领域模型,这样使得服务层的职责更清晰了,有了更多的AOP的感觉,
但基于关系数据库的持久化层让我们在设计领域模型时不得不有所顾忌.

我有话说 by 黄 国华

其实在项目实践中,我们也由类似的体验,将业务放至领域类中,但是,困难多多,不知道有没有一些最佳实践

又来了,每月一次的套路 by Zheng Ken

充血了还是贫血了?这纯是那种过度设计的蛋疼想法,和日本人那种过度设计没有本质不同。
Spring的官方demo全是贫血的,而ruby和groovy之类的都是充血的。
本质上java这个语言,做成贫血的的确更方便。

Spring MVC 只是 Web 框架而已 by lin Jai

Spring MVC 只是 Web 框架而已,更多是表示层的东西。如果从整个 Spring 框架来看,Spring 本身并不是一个专攻 企业业务应用框架,而是一个可拆可组的 松散 支撑性质的框架组合。。你想充血,想贫血 都没问题。。问题在于你的选择而已

别总是嘴炮了,省省吧 by Wang Han

嘴炮谁都会啊。现有的这种方式实际上是长期实践中慢慢演变出来的。如果觉得它有各种各样缺点,请拿出你认为更先进更合理更符合理论规范的解决方案出来吧。对了,如果没有开源的DEMO代码,那么我默认你投降认输。

理想主义者的畅想。。。。 by w ym

作者的论点基于几个他认为有问题的地方:
1.因为业务逻辑散落在服务层。如果需要查看某个业务规则是如何实现的,我们需要先找到它才行,这可不是那么轻松的事情。
2.如果有多个服务类都需要相同的业务规则,那么开发人员很可能会将这个业务规则从一个服务复制到另一个服务中,这会导致维护的梦魇。
3.每个领域模型类在服务层中都有一个服务类。这违背了单一职责原则:单一职责原则表明每个类都应该只有一个职责,这个职责应该完全被这个类所封装。它的所有服务都应该与这个职责保持一致。
4.服务类存在大量的依赖和大量的循环依赖。一个典型的Spring Web应用的服务层没有包含只拥有一个职责的松耦合的服务,它更像是一个紧耦合的大量服务的集合。这使得它很难理解、维护与重用。

以下是个人观点,认为作者觉得有问题的地方,未必是因为分层的设计或者贫血模型导致的:
1.业务规则的方便查找,一般是体现在系统的可维护性上。谁会去代码中查业务规则?主要是开发人。他们为什么会去查?一般是出现缺陷要修复,或者想了解系统具体实现的时候。而提出缺陷的一般是测试或者运营运维人员,这类人员都是从用例或者功能点的角度来描述问题的,例如,某某功能出现了问题。于是,开发人员要去找针对这个功能的代码,其中包含了业务规则。而WEB系统找代码很常见的就是从界面开始找其层层调用,这个调用链是非常清晰的,无论如何分散都是很方便的找到。例如界面上的一个按钮触发了一个URL调用,这个URL对应了控制层的那个方法,这个方法里又调用了哪个service的哪个方法,所有的调用都能按顺序找出来,难道按作者所描述的不分散就能加快这个过程么?如果直接从service层去找业务逻辑肯定是比较麻烦的,但是什么情况下会这么做呢?应该是比较无聊,到处乱看看的时候吧。。。
2.复制代码的问题。。。这个是分层设计或者贫血充血模型的问题么???服务层就不能再细分了么?比如facade和通用服务。。。把这个问题强加在分层设计上实在牵强。
3.单一职责。。。如果我把pojo的职责就定义为传输数据,那贫血pojo非常完美的体现了其单一职责。。这完全取决于在架构设计时,对不同对象不同层次的职责定义。作者的论点很难成立。
4.服务大量循环依赖,很难理解、维护和重用。。。这又让我笑了。。。。首先说“难理解”,难道一个开发人员是直接去看这个类依赖了多少类来理解的么???拜托,正常的开发人员都是从方法里的每行代码来理解逻辑的,方法的命名和注释的有效性才是最能帮助理解的,就算依赖了100个类又怎么样,只要方法里的代码很清晰,就很好理解。再说说循环依赖,这个跟分层和贫血模式有毛的关系啊?常见的分层设计都是单向依赖的,你把单向依赖的设计实现成了循环依赖,这是设计能力和编码习惯的问题。同样在不改变分层模式的情况下,完全以把循环依赖重新组装成非循环依赖,根本不需要改成了充血领域模型。最后说说重用,作者说把一些逻辑放到领域对象里就可以提高重用??这难道不也是职责划分的问题么,假设我对服务层的可重用代码单独定义一套很简单的命名规则,大家一看这个类或者方法的命名就知道是重用代码,一样可以实现这个目标,根本不需要大费周章的去变成充血的领域模型吧??

总体感觉,整个文章就是一个理想主义者的美好愿望,对于实用主义者来说,根本没有提供非常有力的论据来驱动大家去改变!

Re: 理想主义者的畅想。。。。 by wan tao

以前我也想过服务层和领域模型中,业务逻辑放在哪个比较方便。
1.业务员规则的快速查找,如果是自己编写的一般直接去业务层去找,如果不是自己编写的,可能就需要去一层层去找。
2.复制代码,一般是做一个公共帮助类去提供服务,但是存在多人的时候,有重复的现象,还会有出入以及频繁修改被牵连的问题。这个是一个问题
3.我认为pojo还是应该就是pojo,一旦有持久层,或者进行持久层切换,业务逻辑放在领域模型就有问题

Re: 充血模型 by Alan Chen

严重支持!!

各司其职,松散耦合---很好 by 高 健

很同意作者的观点,实际情况确实如此。
model管理自身行为,action只负责类似路由的操作和参数传递,service来处理应用逻辑
思路很好,可很大程度改善目前的极其普遍地复制粘贴一个模板代码的情况,具有很强的探讨价值
对提高程序员的幸福感有帮助

无状态的service和有状态的领域模型,到底哪个好? by 王 杰

我也有同作者类似的想法,认为应该将业务逻辑移动到领域模型里面,这样领域模型才有状态和行为,才真正像一个对象。当领域模型需要有不同的子类的时候,也可以通过继承结构将不同的具体逻辑封装在子类里面。

而现在好多基于Spring的Web项目里面,都是用无状态的Service,值都是用方法的参数传给里面的逻辑,这样的问题:一是如果有不同类型,就需要在逻辑里面判断类型,然后根据类型执行不同逻辑。很多if判断。二是如果某个方法可能需要多传一个参数,就需要更改service里方法的签名,但在其他地方这个方法只要原来的参数就可以了,导致要传个null给它。

这些麻烦一直困扰着我,我也想把业务逻辑移动到领域模型里,这样就变成有状态的领域模型了。可有些同事就是反对,说这样要生成很多对象,在web请求多的时候,就导致jvm gc的负担很重。这样一个理由,我也不确定成不成立。

对于这个,大家觉得应该怎么做?请给出你的理由

Re: 理想主义者的畅想。。。。 by ireland Ken

支持!

Re: 无状态的service和有状态的领域模型,到底哪个好? by ireland Ken

我感觉"无状态的service"是挺方便,容易理解的; 至少你说参数要存个null什么的,这可以service的设计问题而已;

有状态的领域模型,也不见得很多方便,有时得记住它的属性的也不好理解..

不要一刀切 by n yuxiao

我只想说没有银弹 充血有充血的好处 贫血有贫血的适应场景 没有必要一刀切

我对文中的“应用逻辑”和“业务逻辑”的理解 by XIANG Ying

看了下原文中作者对Sean Mitchell的回复的回复,我的理解如下:

文中的“应用逻辑”其实是粗粒度的业务逻辑,放在service中,对应涉及到多个领域对象的业务逻辑;

文中的“业务逻辑”其实是细粒度的业务逻辑,或者说“领域逻辑”,放在domain中,对应单个领域对象自身的业务逻辑。

Re: .. by 陈 立朝

同意,我也不能理解

假设我是个服务类,你是个领域模型对象。如果我告诉你从房顶跳下来,那么你是否会拒绝呢?

Re: .. by 陈 立朝

这个比喻指的是领域模型除了属性还应该有适合自身的行为,还拿这个比喻来说,是否从房顶跳下来不应该是别人让我做的,而是我自己来决定是否应该这样做。

应不应该这样做,本来就应该是业务逻辑来决定的,根据不同的业务场景可能需要进行完全不同的操作,请问领域模型如果要考虑这些业务场景,那得充多少血才够?

不提倡业务逻辑回归领域模型,建议再增加biz业务逻辑层 by 陈 立朝

现在很多设计确实存在Service层循环依赖问题,我的做法是在Service层之上增加Business业务逻辑层. Biz层负责响应用户的请求,并实现事务控制,而Service层回归单一职责原则,为Biz层提供服务。Biz层简单的CRUD操作允许直接调用Persistence层方法,对于复杂逻辑或者是可复用的逻辑则放在Service层,由Biz层调用。Biz层禁止相互依赖,如若确实需要其他Biz的业务逻辑时,应该将业务逻辑下降到Service层提供。

这样可以很好的解决横跨跨多个领域模型的复杂业务逻辑,散落其他Service中的问题,同时代码结构也清晰明了,也更贴合单一职责原则。

Re: 不提倡业务逻辑回归领域模型,建议再增加biz业务逻辑层 by Wong Peter

最后3层的架构被你搞成了100层

Re: 又来了,每月一次的套路 by Wong Peter

因为JavaEE和Rails的考虑角度不同。JavaEE一开始就是针对企业的,需要考虑事务,长期实践下来就导致启动项目时就设计了Service层,作为事务边界,所以自然而然就出现了贫血对象。而Rails开始是针对Web应用的快速开发,所以一开始就是充血对象。

Re: 不提倡业务逻辑回归领域模型,建议再增加biz业务逻辑层 by 杨 霖

有没有例子学习了解下

不敢苟同 by LUO Hao

这个不是Spring的问题而是贫血模型和事务脚本带来的问题!文章标题欠妥!!!

Re: 不提倡业务逻辑回归领域模型,建议再增加biz业务逻辑层 by Wang Tao

这是一种思路,但service层还是在相互依赖啊,循环依赖好像并没有解决

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

30 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT