业务系统的开发中,很多人习惯使用事务脚本(TS, MF,PoEAA)的方式实现领域逻辑。遇到复杂的业务,如果同样使用TS方式,应该也是可以实现的。但是实现的结果是一套复杂的、难以阅读的代码,随着对领域理解的深入和业务需求的不断加强,后果就是维护成本高昂,重复代码变多,测试难以进行。我们经常可以听到这句话:“hi,XX测试人员,这个功能我改完了,但是没有数据或环境太难配置,我没法测试,你在测试环境/uat上测一下吧”。结果很可能是测试人员费尽辛苦准备了一套测试数据,运行到修改的部分后,程序出现异常。这不是最糟的,有时候,运行结果导致测试数据变更,但结果仍然不正确,测试人员还需要准备下一套测试数据(求此时tester的心理阴影面积),这在各系统联调时成本更高。在使用rdbms作为数据存储和JPA作为持久化框架系统中,大多数人遵循这样一个开发流程:
设计表结构 -> 按表建立实体类 -> 生成每个属性的setter和getter -> 建立一个service和实现类 -> 在service的方法中实现业务逻辑,查询、组合、计算实体需要的属性值 -》 调用实体的setter方法设置值 -> 使用持久化框架提供的方法执行数据最终的持久化。
这一套流程下来,已经看不到软件设计的影子,完全是针对将要持久化的数据结果进行组合,拼凑,说白了,就是想方设法的计算出表的字段值。如果业务简单,只有简单的CRUD,比如一个字典表的维护,使用上面这种贫血模型应该是很好的选择。谈不到OO,设计之类的软件开发元素。按以上的开发方式看来,业务软件的开发确实不需要什么设计了。
实际上当然不是这样。如果你读过一些OO设计方面的书,那就算你还没法编写出很OO的代码,起码也应该感觉出这种使用TS方式处理领域逻辑的代码有点那么“不对劲儿”了。可能有人感觉如果编写一些不需要数据持久化的代码,能够更好地使用OO的设计,比如一些工具类。对于普通的POJO来说,的确没有持久化方面的牵绊,对象可以嵌套其他对象、枚举等。对象是个多维的东西,如果完全不受二维的数据库表的限制,就可以更加自由的设计、编写和使用这些类。但业务软件的开发,这种代码显然是少数。
基于以上类与数据库表阻抗失配的情况,最干净、纯粹的解决方案就是:拆分领域模型和数据模型。 领域模型就是普通的POJO,在这个模型中是不考虑数据持久化的;到了数据需要存储的时候,将领域模型中的数据复制出来一份放到数据模型中(数据模型可以使用JPA,mybatis之类的持久化框架,也可以直接使用持久化基础设置的接口,如JDBC),让数据模型执行持久化。这种方式是最纯粹的方式,但是成本稍微有点高,同一个概念的对象可能需要有2个同名的类,一个执行领域逻辑,一个用于持久化。当然如果逻辑够复杂,应该毫不犹豫使用这种方式。但一般情况下,一个系统中大部分功能还是相对简单的,并没有太复杂的逻辑。这就需要找到一个平衡点,既可以使用POJO领域模型灵活的一面,又让这个模型可以执行数据的持久化,也就是仍然使用领域模型和数据模型混用的模式。具体使用哪种方式,需要综合考虑。软件设计毕竟是门艺术,不是科学。下面通过一个简单实例说一下使用JPA如何做到领域模型和数据模型混合使用。
我们使用客户信息作为一个例子:我们需要在系统中使用客户信息,客户有姓名、手机号等属性。现在根据手机号这个属性,说明一下如何使用JPA来处理关于手机号的需求。手机号属于用户主要的信息之一,需求包括:格式验证,确保手机号是个合法的数字集合;在短信发送的时候,需要使用手机号后4位放入短信内容中;或者还有根据手机号前3位判断运营商、前4位判断所属地区的需求。
这个客户类我们大致可以做成这样:
class Customer{
private Mobile mobile;//手机号
}
而Mobile类可能这样:
class Mobile{
private String number;//号码字符串
private Mobile(){
//给jpa用的构造
}
public Mobile(String number){
this.setNumber(number)
}
private void setNumber(String num){
//验证....
this.number = num;
}
public String last4Number(){
return 后四位;
}
}
这样在使用客户手机号后4位的时候可以这样:
Customer c = repository.get(id);
c.getMobile().last4Number();
如果简单用字符串表示手机号,不用Mobile这种VO的时候,在每次使用的时候都用customer.getMobile().substring(startIndex) (这里的mobile属性是个String,startIndex需要使用前在外部计算,而且Mobile是否为null也是个隐患),startIndex计算如果出错,那么就要修改代码,如果有2处以上使用了“后4位”这个功能,那么就修改多处代码,这还不算你忘了的情况。如果此段代码处于一个复杂业务逻辑的代码中,那么修改这个小片段可能会使整个逻辑无法进行(比如空指针),严格来说只要修改了,那么这段包含复杂业务逻辑的代码就需要重新测试一下。而且单元测试要测试这么复杂的逻辑也是需要较高的成本,而黑盒的成本可能更加高昂。
如果我们使用了Mobile VO类的方式,那如果后4位算错的情况下,修改Mobile类的方法,并且独立执行Mobile的单元测试就可以了(就一个pojo,测试类运行起来也是嗖嗖的)。这也是SRP的一个体现。
说完了类,说说在JPA中的映射方法,上面的Customer和Mobile缺少了一点Jpa的注解:
Customer类仍然需要注解为@Entity,在其mobile属性上应该加上注解@Embedded和@AttributeOverrides:
@Embedded
@AttributeOverrides({
//如果类有多个属性,可以在这里写上多个@AttributeOverride,逗号分隔。
@AttributeOverride(name="number", column=@Column(name="手机号字段名", columnDefinition="varchar(20)"))
})
在Mobile的VO类上:
@Embeddable //需要增加这个!
public class Mobile {
...
}
还有个小建议,就是不要随意公开实体类的setter和getter方法,保持package、protected是个好的实践。这可以避免模型被Service层代码随意更改,导致模型状态不完整、破环其内部规则。
基本这样就OK了。这样就能使用更加丰富的OO手法设计和开发,毕竟Gavin King的搞Hibernate也是这个目的,而不是让我们使用只有setter/getter的实体类。
大致这样吧