原创

程序设计原则之SOLID原则

温馨提示:
本文最后更新于 2023年05月11日,已超过 402 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

SOLID原则

设计模式中的SOLID原则,分别是单一原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。

SOLID原则是由5个设计原则组成,SOLID对应每个原则英文字母的开头:

单一职责原则(Single Responsiblity Principle)
开闭原则(Open Close Principle)
里式替换原则(Liskov Substitution Principle)
接口隔离原则(Interface Segregation Principle)
依赖反转原则(Dependency Inversion Principle)

遵守SOLID可让程序更有健壮性,避免业务耦合,维护困难

为了使得易于理解,本文全部按照web业务后端curd方式说明和理解

单一职责原则(Single Responsiblity Principle)

单一职责指的是一个类或者一个方法只做一件事。
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化就可能抑制或者削弱这个类完成其他职责的能力。

例如,User类应该只做用户相关的操作:
file

  • 补充点1,由于这个类是userController,所以理论上,只能做用户控制器相关的事,这里成为了2个限定职责
    • 职责1:这个类只能做用户相关的操作(用户的增删改查)
    • 职责2:这个类只能做控制器的职责(接收和响应参数)

根据这2点,我们可以完善一下某一个方法:

    /**
     * 获取单个用户
     */
    public UserGetOneResponse getOne(UserGetOneRequest getOneRequest) {
        int userId = getOneRequest.getId();
        UserBean userBean = new UserService().getOne(userId);
        UserGetOneResponse userGetOneResponse = new UserGetOneResponse();
        userGetOneResponse.setUserBean(userBean);
        userGetOneResponse.setCode(200);
        userGetOneResponse.setMsg("success");
        return userGetOneResponse;
    }

在这个例子中,我们可以发现这个控制器方法遵循了以上的2个职责,并且将逻辑处理放到了UserService中.

我们可以对这个方法进行更深层次的控制器职责划分

  • 获取参数,这个方法只获取需要的参数,校验,过滤参数,验权和这个方法和这个类无关,需要上层其他类做处理
  • 调用service, 这个方法理论上只调用 获取用户的逻辑,至于获取用户是否出错,调用用户的逻辑都跟 控制器方法 无关
  • 返回需要的参数, 这个方法理论上只返回这个方法应该返回的参数,由于是控制器方法,所以不可避免的需要有code,msg,等相关的参数返回,但这个并不是userService需要考虑的,所以userService只返回UserBean,然后由控制器方法统一处理返回

根据这个例子,我们可以大致的了解到控制器方法中的单一职责,以下是一个详细的说明图:
file

为什么需要单一职责.

我们可以试着通过反证法来进行推理.首先我们要清楚,强调一下,SOLID原则是为了让程序更健壮,更容易维护,代码清晰,解耦
假设我们试着通过php的万能数组方式写这个代码:

    public function getOne($params){
        $id = $params['id'];
        $userInfo = UserModel::where('id', $id)->first();
        $data = [
            "code"=>200,
            "msg"=>"success",
            "data"=>$userInfo
        ];
        return response()->json($data);
}

我们可以发现以下问题:

  • 参数没有做好验证,可能传也可能不传,也可能传了个数组,传了逗号
    • 如果id传的不符合期望,那整个代码将会出现问题,整个程序命脉控制在了前端传参上,传错了就炸
    • 如果后期改成了其他参数,获取需要增加其他参数,则只能在这个方法里面一直加,控制器方法承受的压力越来越大
  • 直接在控制器中where id 获取用户数据
    • 如果以后需要改成缓存存储,则需要直接改动到控制器的代码
    • 如果以后改为了where account,代码也得改
    • 如果需要复用这个查询方法,没法复用
    • 如果userInfo有额外的扩展信息表,则需要在这边进行第二次的查询
    • 如果userInfo需要屏蔽某些字段,则还得在这边加逻辑
  • 直接在控制器返回中封装了返回数据,并且转为了json
    • 如果后期需要调整code,msg,则只能在一个个控制器方法中调整

开闭原则(Open Close Principle)

开闭原则指的是 :如果一个类独立运行之后,就不应该去进行修改,而是通过扩展的功能去实现它的新功能.
这个可能大多数程序员都会犯的错,也是屎山的造就主要原因.

开闭原则主要的2个点就是: 我们的软件实体(类,方法,模块)对扩展开放,对修改关闭

不管是从代码层面,还是从宏观的模块,需求层面,都应该遵循这个原则

对扩展开放

我们的程序必须要实现可扩展,才能做到后面出现新需求时不需要修改原有的代码.

在面对需求实现时,需求肯定是可变的,我们在实现需求的时候,也得考虑到可变的因素,在这个时候考虑到代码的可扩展性,对所有可能存在的可变因素进行封装.

例如:
对于在判断某个用户是否有权限时,可能一开始的需求是只要是vip用户就算有权限,但是到后面,可能会变为在活动期间普通用户都有权限,或者新增了svip用户,这个时候我们的代码就会往里面越加越多:

    public boolean isPermission(UserBean userBean) {
        //会员等级为1的有权限
        if (userBean.getUserLevel() == 1) {
            return true;
        }
        //当前时间在活动期间内有权限
        if (System.currentTimeMillis() > 1000) {
            return true;
        }
        //会员等级为2,并且名字是admin的有权限
        if (userBean.getUserLevel() == 2 && userBean.getName().equals("admin")) {
            return true;
        }
        return false;
    }

那么这个要如何分装呢,我们可以把判断是否有权限的进行抽离出来,然后根据规则也进行抽离,例如:

    public interface CheckRuleInterface{
        public boolean isPermission(UserBean userBean);
    }

    public boolean isPermission(UserBean userBean,CheckRuleInterface[] checkRuleInterfaces) {
        for (CheckRuleInterface checkRuleInterface:checkRuleInterfaces) {
            if (checkRuleInterface.isPermission(userBean)){
                return true;
            }
        }
        return false;
    }

可以看出,在这个例子中,我们需要传入userBean和一个CheckRuleInterface数组,我们并不需要动到isPermission这个类,而是通过依赖翻转,将规则剥离到了checkRuleInterfaces数组中,通过传入不同的验证类数组,就可以实现不同的验证方式,例如:


    public class CheckUserVip implements CheckRuleInterface{
        public boolean isPermission(UserBean userBean){
            if (userBean.getUserLevel()==1){
                return true;
            }
            return false;
        }
    }

    public class CheckTime implements CheckRuleInterface{
        public boolean isPermission(UserBean userBean){
            //这里判断下活动时间
            if (true){
                return true;
            }
            return false;
        }
    }

我们需要什么验证条件,只需要增加实现这个接口的类就行了,完全不需要动到原来的代码逻辑.

对修改关闭

对修改关闭可以认为是依赖于对修改开放

当一个能够正常运行的业务需要变更或者增加需求时,要做的应该是尽量遵循原有的需求架构,然后额外增加新的逻辑,尽量不动到原来的逻辑
如果真需要动到原有逻辑的情况下,就需要考虑到这部分是不是没有做好对扩展开放?是否需要重构?

例如以上的代码,如果在判断是否存在权限的情况下没有考虑到,那就会一行行的往上加代码,这样明显是不行的,所以需要重构.

里氏替换原则(Liskov Substitution Principle)

里氏替换原则指的是: 继承必须确保父类所拥有的性质在子类中仍然成立,子类可以有额外的新性质,但不能变更父类的性质.
这样才能确保子类在替换父类时,不会出现程序错误.

换句话来说,当一个方法依赖的是一个父类时,所有继承的子类应该都可以替换这个类(能够传父类的子类),保证父类的所有性质都还存在,能够正常运行,例如:
有一个响应的基类:

package response.user;

public class Response<E> {
    protected int code;
    protected String msg;
    protected E data;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public E getData() {
        return data;
    }

    public void setData(E data) {
        this.data = data;
    }
}

我们可以继承这个基类,保证所有响应都是正确的,但是如果有一个类将code进行了重写,改为了string,那很明显,我们在响应的时候就会变成string,导致读取出错.
如果是真需要对外返回一个string的code,那应该继承一个新的基类

接口隔离原则(Interface Segregation Principle)

接口隔离原则是指:类不应该被强迫依赖不需要的方法,类能做的事情越少越好,越专一越好,这样有2个好处

  • 避免了后续需求变更,类由于依赖的事情广泛,而导致一个类所写的代码越来越多,到后面甚至是有点擦边的都被联系上了
  • 在继承这个类的时候,子类可能完全用不到某一些方法和功能,也不需要用到,增加了维护的负担,以及数据隔离的问题.

例如:用户类应该是指负责用户的基础curd,如果出现了其他的用户组,用户授权,用户密码修改,用户金币变更等,全部需要作为一个新的类去进行开发.

例如:一个curl类,应该是只包含请求,如果有加密请求/处理响应,等等,都应该创建新的类去处理,因为当需要继承这个curl类时,可能继承的类完全不需要做加密请求,做处理响应这些逻辑

依赖反转原则(Dependency Inversion Principle)

依赖反转原则是指: 高层模块不应该依赖低层模块,二者都应该依赖于抽象.
1.高层模块只应该包含重要的业务模型和策略选择
2.低层模块则是不同业务和策略的实现
3.高层抽象不依赖高层和底层模块的具体实现,最多依赖低层的抽象
4.低层抽象和实现也只依赖于高层抽象

底层模块:实现具体业务的模块.
高层模块:实现业务的功能和策略.

例如在上面的 开闭原则(Open Close Principle)中的例子,

  • 判断用户权限属于高层模块,它不应该去实现具体是如何判断的,而是只依赖与具体判断的抽象类.
  • 而具体判断的类属于低层模块,在上面的例子中,低层模块依赖与userBean,这样其实是不对的,而是应该去除这个判断,改为不传入数据,而是通过类的实现去进行传入判断.因为在判断时间的时候,其实不需要userBean:

    public interface CheckRuleInterface{
        public boolean isPermission();
    }

    public class CheckUserVip implements CheckRuleInterface{
        UserBean userBean;
        public CheckUserVip(UserBean userBean){
            this.userBean=userBean;
        }
        public boolean isPermission(){
            if (this.userBean.getUserLevel()==1){
                return true;
            }
            return false;
        }
    }

    public class CheckTime implements CheckRuleInterface{
        public boolean isPermission(){
            //这里判断下活动时间
            if (true){
                return true;
            }
            return false;
        }
    }
正文到此结束
本文目录