贫血之殇

本文写写贫血模式对人的误导,顺便提一下状态模式

从十几年前开始,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再挑不出什么毛病了吧?”
“是比以前好多了,不过我又发现了你写代码的另外一个问题”
“您请指教”
“你还记得什么是面向对象编程吗?你这种抽方法的编码方式,和面向过程编程有什么区别?”
“这个,,,,那我要怎么改呢?”
“考虑一下把状态封装成一个对象,不同状态的变化做为状态的操作,操作后设置状态本身状态。这么说吧, 有个设计模式叫状态模式, 你了解一下。写东西不要愣头青一样,优雅,要优雅,懂吗?程序员不懂优雅,和咸鱼有什么区别?”
我落荒而逃,尼玛,现在才说, 开始的时候怎么不早说,非等我改这么多了才说。话说状态模式我也懂, 怎么就没想到在这个地方用呢? 要怎么应用状态模式呢?我打开百度,又看了一遍状态模式的定义:

当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

我又想了一下当前这个功能, 要怎么把现在这个申请单的功能和状态模式结合起来呢?毫无疑问,定义中的“对象”指的就是申请单,“内在状态”指的就是申请单的状态。行为嘛,指的就是用户的操作,可以把这些操作都放在申请单对象上。当申请单执行某个操作的时候,实际上可以把这个操作由当前的状态对象代理。状态变化时,执行的操作逻辑也相应变化,但是对调用者来说,它还是调用的申请单的方法。下面是具体的实现过程:

  1. 声明一个申请单, 这个申请单里面有一个“状态”对象。申请单的所有操作都交给当前的“状态”来处理。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;
    }
  1. 声明一个申请单状态接口. 这里面要注意:因为要把申请的操作由状态对象来代理, 所以状态接口的操作要实现申请相关的操作。
public interface CertificateStatus {
    void submit();
    void hrPass();
    void leaderPass();
    String getCurrentStatus();
}
  1. 声明一个抽象状态类。为什么要先设计一个抽象类,而不是直接写各个状态的实现类呢,主要是为了给实现类添加一些默认的方法。说白了就是代码重用。因为你马上就会知道 ,状态不同, 可以执行的操作也不同。譬如说,当现在的申请单是“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 "";
    }
}
  1. 具体的实现类-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";
    }
}
  1. 具体的实现类-类似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";
    }
  1. 用状态模式模拟一下申请状态变化过程
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

总结一下,这次代码优化做了两点:

  1. 首先把面积过程的设计方式改为面向对象的设计方式。(长期贫血导致的编码习惯的改变)
  2. 使用了状态模式,让代码看起来更正宗。

写完后,感觉到对状态模式的介绍还是太潦草了,这个等以后开设计模式专栏的时候再细说吧。今天主要还是想告诉大家: 不要相当然的认为:写代码,处理逻辑就是写一堆POJO,然后再加一个service类。还是有很多情况可以用到设计模式的。平时多看源码,打开思路。

文章里面的代码可以访问  代码的github地址

如果认为我写的文章不错,可以添加我的微信公众号,我会每周发一篇原创文章,和大家共同探讨编程,学习编程。


刀藏水

你可能感兴趣的:(贫血之殇)