美文网首页DDD
贫血之殇

贫血之殇

作者: 刀藏水 | 来源:发表于2020-08-08 20:47 被阅读0次

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

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

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


    刀藏水

    相关文章

      网友评论

        本文标题:贫血之殇

        本文链接:https://www.haomeiwen.com/subject/hpcwrktx.html