程序设计原则之SOLID原则
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类应该只做用户相关的操作:
- 补充点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,然后由控制器方法统一处理返回
根据这个例子,我们可以大致的了解到控制器方法中的单一职责
,以下是一个详细的说明图:
为什么需要单一职责.
我们可以试着通过反证法来进行推理.首先我们要清楚,强调一下,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;
}
}
- 点赞
- 收藏
- 关注作者
评论(0)