本文写写贫血模式对人的误导,顺便提一下状态模式
从十几年前开始,B/S架构就铺天盖地了。听的最多的词可能就是MVC了。面试的时候也经常被问起过。MVC本身是一个非常牛逼的设计模式。这么多年经久不衰也说明它的成功。不过今天要谈一下MVC带来的问题,这个问题不是MVC的错,而是由于MVC太成功,让很多程序员一叶障目,不见泰山了。甚至可以说, 忘了面向对象编程的部分初衷了。
MVC模式是基于“贫血”模型来设计的。 贫血模型是这样定义的
贫血模型是指领域对象里只有get和set方法(POJO),所有的业务逻辑都不包含在内而是放在Business Logic层。
如果对象里面只有get set方法,那其实这个对象就是一个传递信息的媒介。这个对象没有任何复杂的操作。所以,虽然你定义了一个对象Persion,但是这个对象只提供姓名,性名,年龄等等信息。做为人类应该有的其他能力他丧失了。这个Persion甚至都不是一个人,只是一个人的定义,一个名片。如果想让这个人有行为,怎么办呢?在贫血模型下是加一个service层,譬如PersionService。这里面定义了人的一些行为。当要做某个动作的时候, 就调用persionService.doSomething。
说到这里,估计很多人会问,这有什么问题吗?我这么多年一直是这样写代码的。
正如我在开头说的, MVC是很牛逼的设计模式。按MVC的方式写代码,写成这样是没有错的。只是有的时候不要仅仅这样做,合适的时候可以设计一些对象,给这些对象更多的操作空间,让你的对象丰满起来,成为真正的对象。
我要发起一个工作居住证的申请,里面要填写很多个人信息,要经过很多人的申批,简单起见,咱们只用两个人:hr和直接领导。开始填写的时候,申请的状态为init, 提交后,状态变为submit, hr和leader申批过后,状态变为hr_pass和leader_pass。最开始,我的代码是这样写的。
先写一个代表申批单的对象:
@Data
public class Certificate {
private String name;
private String status;
private String otherInfo;
}
再定义逻辑处理:
public class CertificateService {
public void save(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
c.setStatus("save");
}
public void submit(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
c.setStatus("submit");
}
public void hrPass(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
c.setStatus("hr_pass");
}
public void leaderPass(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
c.setStatus("leader_pass");
}
public void hrReject(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
c.setStatus("save");
}
public void leaderReject(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
c.setStatus("save");
}
}
代码写成这样,其实也没有什么不好。 但是在做code reivew的时候,讨厌的老K发话了,“你这样写,逻辑上严谨吗?” 看着我迷惑的眼神, 他又说:“做为后端逻辑,你要检查申请单的状态是否允许当前的操作,明白?” 我恍然大悟,于是赶紧修改代码如下(部分):
public void submit(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
if(c.getStatus().equals("save")) {
c.setStatus("submit");
}
else {
throw new UnsupportedOperationException();
}
}
public void hrPass(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxxx");
if(c.getStatus().equals("submit")) {
c.setStatus("hr_pass");
}
else {
throw new UnsupportedOperationException();
}
}
然后兴冲冲的把代码给老K看。“逻辑上是对的,只是代码比较烂。你现在的每步操作都有对状态的判断处理,是否可以把这块逻辑单独抽出来?这样改状态逻辑的时候不至于影响其他操作?”
“有道理”, 我赶紧把代码抽出这样一个方法:
private String getStuats(String status, String action) {
if(status.equals("save") ) {
if(action.equals("submit")) {
return "submit";
}
else {
throw new UnsupportedOperationException();
}
}
if(status.equals("hr_pass")) {
if(action.equals("leader_pass")) {
return "leader_pass";
}
else {
throw new UnsupportedOperationException();
}
}
// 省略其他代码
throw new UnsupportedOperationException();
}
然后设置状态的时候统一用这种方式:
public void leaderPass(Certificate c) {
// 其他逻辑
c.setOtherInfo("xxxx");
c.setStatus(getStuats(c.getStatus(), "leader_pass"));
}
“完美的抽象!” 我想, “这下老K再挑不出什么毛病了吧?”
“是比以前好多了,不过我又发现了你写代码的另外一个问题”
“您请指教”
“你还记得什么是面向对象编程吗?你这种抽方法的编码方式,和面向过程编程有什么区别?”
“这个,,,,那我要怎么改呢?”
“考虑一下把状态封装成一个对象,不同状态的变化做为状态的操作,操作后设置状态本身状态。这么说吧, 有个设计模式叫状态模式, 你了解一下。写东西不要愣头青一样,优雅,要优雅,懂吗?程序员不懂优雅,和咸鱼有什么区别?”
我落荒而逃,尼玛,现在才说, 开始的时候怎么不早说,非等我改这么多了才说。话说状态模式我也懂, 怎么就没想到在这个地方用呢? 要怎么应用状态模式呢?我打开百度,又看了一遍状态模式的定义:
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
我又想了一下当前这个功能, 要怎么把现在这个申请单的功能和状态模式结合起来呢?毫无疑问,定义中的“对象”指的就是申请单,“内在状态”指的就是申请单的状态。行为嘛,指的就是用户的操作,可以把这些操作都放在申请单对象上。当申请单执行某个操作的时候,实际上可以把这个操作由当前的状态对象代理。状态变化时,执行的操作逻辑也相应变化,但是对调用者来说,它还是调用的申请单的方法。下面是具体的实现过程:
- 声明一个申请单, 这个申请单里面有一个“状态”对象。申请单的所有操作都交给当前的“状态”来处理。changeStatus这个方法是用来改变“状态”的,改变状态后,再执行的操作就是新状态的逻辑了。
public class CertificateInfo {
CertificateStatus status ;
void changeStatus(CertificateStatus status) {
this.status = status;
}
void submit() {
status.submit();
}
void hrPass() {
status.hrPass();
}
void leaderPass() {
status.leaderPass();
}
CertificateStatus getCurrentStatus() {
return status;
}
- 声明一个申请单状态接口. 这里面要注意:因为要把申请的操作由状态对象来代理, 所以状态接口的操作要实现申请相关的操作。
public interface CertificateStatus {
void submit();
void hrPass();
void leaderPass();
String getCurrentStatus();
}
- 声明一个抽象状态类。为什么要先设计一个抽象类,而不是直接写各个状态的实现类呢,主要是为了给实现类添加一些默认的方法。说白了就是代码重用。因为你马上就会知道 ,状态不同, 可以执行的操作也不同。譬如说,当现在的申请单是“submit"状态时,是不能执行leaderPass操作的,必须是"hrPass"的时候才能执行。抽象类的实现全部抛出UnsupportedOperationException。如果一个状态可以执行某个操作,只override这个操作就行啦。
public class AbstractCertificateStatus implements CertificateStatus{
CertificateInfo certificateInfo;
public AbstractCertificateStatus(CertificateInfo certificateInfo) {
this.certificateInfo = certificateInfo;
}
@Override
public void submit() {
throw new UnsupportedOperationException();
}
@Override
public void hrPass() {
throw new UnsupportedOperationException();
}
@Override
public void leaderPass() {
throw new UnsupportedOperationException();
}
@Override
public String getCurrentStatus() {
return "";
}
}
- 具体的实现类-SubmitStatus。submiStatus状态下只能执行hrPass操作,执行完成后把申请单设置为HrPassStatus。如果submitStatus执行别的操作,都会抛出UnsupportedOperationException。
public class SubmitStatus extends AbstractCertificateStatus{
public SubmitStatus(CertificateInfo certificateInfo) {
super(certificateInfo);
}
@Override
public void hrPass() {
certificateInfo.changeStatus(new HrPassStatus(certificateInfo));
}
@Override
public String getCurrentStatus() {
return "submit";
}
}
- 具体的实现类-类似SubmitStatus,不同的是它只override了leaderPass操作,别的操作不让执行。
public class HrPassStatus extends AbstractCertificateStatus{
public HrPassStatus(CertificateInfo certificateInfo) {
super(certificateInfo);
}
@Override
public void leaderPass() {
certificateInfo.changeStatus(new LeaderPassStatus(certificateInfo));
}
@Override
public String getCurrentStatus() {
return "hrPass";
}
- 用状态模式模拟一下申请状态变化过程
public class CertificateApplication {
public static void main(String[] args) {
CertificateInfo certificateInfo = new CertificateInfo();
CertificateStatus start = new InitStatus(certificateInfo);
certificateInfo.changeStatus(start);
// certificateInfo.leaderPass(); // 会抛异常
certificateInfo.submit(); // 执行完本操作后,状态变为SubmitStatus
// certificateInfo.submit(); // 会抛异常,因为已经是SubmitStats了,只能执行hrPass
certificateInfo.hrPass();
certificateInfo.leaderPass();
//.......
System.out.println("申请单状态为:" + certificateInfo.getState().getCurrentStatus());
}
}
结果如下:
由InitStatus代为执行,执行完成变为提交状态
由SubmitStatus代为执行,执行完成变为hrPass状态
由HrPassStatus代为执行,执行完成变为leaderPass状态, 申请单处理完成。
申请单状态为:leaderPass
总结一下,这次代码优化做了两点:
- 首先把面积过程的设计方式改为面向对象的设计方式。(长期贫血导致的编码习惯的改变)
- 使用了状态模式,让代码看起来更正宗。
写完后,感觉到对状态模式的介绍还是太潦草了,这个等以后开设计模式专栏的时候再细说吧。今天主要还是想告诉大家: 不要相当然的认为:写代码,处理逻辑就是写一堆POJO,然后再加一个service类。还是有很多情况可以用到设计模式的。平时多看源码,打开思路。
文章里面的代码可以访问 代码的github地址
如果认为我写的文章不错,可以添加我的微信公众号,我会每周发一篇原创文章,和大家共同探讨编程,学习编程。