首页 > 代码库 > Spring Web 应用的最大败笔

Spring Web 应用的最大败笔

开发人员在使用Spring应用是非常擅长谈论依赖注入的好处。不幸的是,他们不是那么真的利用它的好处,如单一职责原则,分离关注原则。如果我们一起来看看大部分Spring的Web应用程序,常见的错误的设计如下:

1.领域模型对象用来存储应用的数据(当作DTO使用),领域模型是贫血模型这样的反模式。

2.服务层每个实体有一个服务。

问题是这样很普遍,错误在哪里呢?

Spring的web应用程序之所以这样是因为他们做事物的方式一直都是这样做的,老习惯难改,特别是如果他们是高级开发人员或软件架构师,这些人捍卫这样做的论据之一是:我们的应用程序遵循关注分离的原则,因为它已经被分为若干层,每个层有自己的特定职责。

1. Web层负责处理用户输入,并返回正确的响应返回给用户。 web层与服务层通信。
2.服务层作为一个事务边界。它也负责授权和包含我们的应用程序的业务逻辑。服务层管理的域模型对象,并与其他服务和存储库层进行通信。
3.存储库/数据访问层负责与所使用的数据的存储进行通信。

分离关注(Soc)是分离计算机程序为不同的部分,每个部分有一个关注聚焦,一个典型的Spring Web应用在一定程度上遵循这一原则,但现实是,该应用程序有一个整体的服务层,它有太多的责任。更具体地,服务层有两个主要问题:

1.在服务层发现业务逻辑
业务逻辑被分散在各个服务层。如果我们需要检查一个业务规则是如何实现的,我们必须先找到它。这可能并不容易。此外,如果相同的业务规则需要在多个服务类,问题是,规则需要从一个服务到另一个简单地复制。这将导致维护的噩梦。

2.每个领域模型一个服务
这完全违反了单一职责原则,它被定义为如下:单一职责原则指出,每一个类都应该有一个责任,责任应该由类完全封装。其所有的服务应该狭义与责任相一致。(不应将原属于领域模型的行为方法等划放在服务中实现,对象不但有属性还有行为)

服务类有很多依赖,以及大量的循环依赖。更像网络紧密耦合和单片服务。这使得很难理解,维护和重用。这听起来有点苛刻,但一个Spring的web应用的服务层往往是最容易出问题的部分。幸运的是,所有的希望都不会丢失。

1. 我们必须将我们的应用程序的业务逻辑从服务层迁移到领域模型类中。

举个例子:假设我是一个服务类,你是一个域模型对象。如果我让你从屋顶上跳下来,你会喜欢我这样的决定吗?(跳下来会摔伤,自己没有脑子或被洗脑,变成僵尸,只听从执行,不思考自己的安全,这就是贫血模型的问题)

将业务逻辑从服务层迁移到域模型类有下面三个优势:

(1)我们的代码将以逻辑方式切割,服务层只要关注应用逻辑,而我们的领域模型关注业务逻辑。
(2)业务逻辑只存在一个地方,容易发现修改。
(3)服务层的源代码是清洁的,不包含任何复制粘贴代码

2. 将每个实体服务切割为单一目标的更小的服务。

比如,有一个单一服务类,提供对人员和用户账户的CRUD操作,我们应该将它分为两个独立的服务类:
第一个是对人员的提供CRUD操作
第二个是提供与用户账户相关的操作。

好处:每个服务类中有一个逻辑组职责。每个服务类的依赖较少,这意味着他们不再是紧耦合的源头。他们是较小的和松耦合的组件。服务类更容易理解,维护和重用。

这两个简单的步骤将帮助我们使得我们的应用程序架构更干净,有助于同行开发商提高生产力和幸福。

贫血模型不只是存在Spring应用,EJB更是普遍,只有架构分层是不够的,还需要更详细的逻辑分层,DDD领域驱动设计正是一个详细帮助建立丰富的有行为的领域模型的方法学。

理论大片,缺少些例子。让我看的云里雾里,姑且认为又是一贫血模式批判片。

目前我的认知,打个比喻:领域模型就是人物刻画,比如萧峰、段誉、慕容复,这些人物有着各自的武功、特长、技能、秉性。如果他们都呆在各自的小房间与世隔绝,那也就不会发生什么事情。而服务就是发生业务的舞台,比如“比武”这个服务,将这些领域模型放入进去,就会发挥领域模型——人物,自身的武功进行交互。

 

我举个例子吧。以网上书店为案例,原帖:http://www.jdon.com/44851

业务需求:用户浏览选择书籍,放入购物车,生成订单,付款,等待书籍送上门。

原来服务设计的问题:
一个bookstoreService中又要处理订单,又处理购物车管理,还和Account等实体依赖,也就是说,Account一旦修改,bookstoreService也要修改,这些都违反单一职责和分离关注原则:




如果反映在代码,那么bookstoreService接口代码如下:

public interface BookstoreService{
//将书籍加入购物车
void addShoppingcart(Book book);
//从购物车去除
void removeShoppingcart(Book book);

void addOrder(Book book);

void removeOrder(Book book);

}

好了,如果看得懂这个代码和图,下面我们就要进行分离。

一般很容易将BookstoreService拆成OrderService和CartService。

其实这种区分可能是一种直觉,没有反映内在规律,也就是没有考虑Order和Cart两个实体之间的关系。那么如何考虑这两个实体之间关系呢?一般思路到这里就堵住了。

如果我们从DDD和事件角度考虑,Order和Cart为什么存在?是因为什么上下文存在?显然是在购书这个活动场景中才有这两个实体存在,而且这两个实体是一种先后关系,先有购物车,后有订单,而且购物车好像是订单的预先准备,草稿一样,这决定了这两个实体应该在同一个上下文,也就是模块中,同时两者存在逻辑关系。

我们设计一个DDD聚合,聚合根分别是Order和Cart两个,可以认为主副之分。

那么订单Cart的类是如洗:


public class Cart{

private int id;
public void add(Book book){
..//将书籍加入购物车
}

public void remove(Book book){
..
//将书籍从购物车移走
}


}



订单比Cart复杂一些:


public class order{

private int id;

private boolean checkRule(){
../将书籍加入订单之前需要检查的规则
}

public void add(Book book){
if (checkRule()){
..//将书籍加入订单
}
}

public void remove(Book book){
..
//将书籍从订单移走
}

public float count(){
//统计订单总金额
}

public Cart getCartForUser(User user){
//为某个用户创建一个购物车
}

public Order confirm(Car cart){
//购物车确认,生成订单
}

}



而BookStoreService就变成OrderService


public interface OrderService{

Cart getCart(User user);//派给一个用户购物车,内部委托Order.getCartForUser实现。

Order createOrder(Cart cart);
//创建一个订单,内部委托Order.confirm方法实现

//其他大粒度服务 如 订单跟踪 多组织销售订单等等复杂服务
}




Order和Cart从之前的贫血模型,也就是只有属性的setter/getter,没有职责行为,到现在富模型,有自己的行为,原来的行为跑到bookstoreService服务里面去了。举个例子:
服务代表一个张三, 李四代表贫血模型,李四就是一堆数据集合,张三是服务,张三获得李四实例以后,想对李四干什么就干什么,让它从屋顶跳下来它就得跳下来。

通过以上行为归类以后,服务里面的方法就变得更加面向业务,比如我们通常讲加油服务,其实加油这个服务内部涉及很多流程,需要实体加油箱,需要加油员拿起枪,需要我们付费,不是简单的一个过程。如果这些复杂过程都写在加油服务里面,无疑变成面向过程,一堆乱麻了。

有的服务后面的流程需要涉及多个部门系统,比如请假单,需要生产部门 人事部门和总经理办公室都要查阅同意,你不可能将这三个部门的处理细节都写在一个请假服务中,况且这三个部门使用的软件厂商可能不同,有微软有Java的。