贫血之殇

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

从十几年前开始,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地址

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


刀藏水
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354