一起聊聊设计原则(上)

简介: 一起聊聊设计原则(上)

今晚我们一起来聊聊关于设计原则相关的知识点。


SOLID五大原则是什么



SRP 单一责任原则


单一责任原则,从名字上我们就能比较好的去理解它。这项原则主张一个对象只专注于单个方面的逻辑,强调了职责的专一性。


举个例子:


学生管理系统中,我们需要提交一些学生的基本资料,那么学生信息相关的程序都交给了StudentService负责,如果我们要实现一个保存教师基本资料的功能就应该新建一个TeacherService去处理,而不应该写在StudentService当中。


OCP开放封闭原则


这项原则从我个人的角度去理解,它更加强调的是对于扩展的开放性,例如当我们需要调整某些实现逻辑的时候,尽量不要直接改动到原有的实现点。


但是这里面有几个点容易被人们误解:


第一点


开放封闭原则虽然强调的是不要随意改动代原先代码到逻辑结构,但是并没有要求一定不能对代码进行改动!


第二点


同样是代码改动,如果我们可以从功能,模块的角度去看,实际上代码的改动更多地可以被认作为是一种“扩展”。


关于如何做到开放封闭原则,下文我会专门用一个案例来进行介绍。


LSP里氏替换原则


里氏替换原则强调的是不能破坏一个原有类设计的原始设计体系。强调了子类可以对父类程序进行继承。但是有几个点需要注意下:


如果父类定义的规则最好是最基础,必须遵守的法则。如果子类继承了父类之后,在某个方法的实现上违背了初衷,那么这样的设计就是违背了里氏替换法则。


例如:


父类的设计是希望实现商品库存扣减的功能,但是子类的实现却是实现了库存+1的功能,这就很明显是牛头不对马嘴了。


子类不要违背父类对于入参,出参,异常方面的约定。例如:父类对于异常的抛出指定的是 NullPointException ,但是子类却在实现的时候声明了会出 illegalArgumentException,那么此时就需要注意到设计已经违背了LSP原则。


同样,具体的案例我在下文会列举出来和大家进行代码分享。


ISP接口隔离原则


理解“接口隔离原则”的重点是理解其中的“接口”二字。


这里有三种不同的理解。如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。


如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。


DIP依赖倒置原则


比较经典的例子,例如说Spring框架的IOC控制反转,将bean的管理交给了Spring容器去托管。依赖注入则是指不通过明确的new对象的方式来在类中创建类,而是提前将类创建好,然后通过构造函数,setter函数等方式将对应的类注入到所需使用的对象当中。

DIP的英文解释大致为:


High-level modules shouldn’t depend on low-level modules.

Both modules should depend on abstractions. In addition,

abstractions shouldn’t depend on details. Details depend on

abstractions.


解释过来就是,高层次的模块不应该依赖低层次的模块,不同的模块之间应该通过接口来互相访问,而并非直接访问到对方的具体实现。


清楚了这么多理论知识之后,接下来我们通过一些代码实战案例来进行更加深入的了解吧。


代码实战理解设计原则


单一责任原则案例



我们来看这么一个类,简单的一个用户信息类中,包含了一个叫做home的字段,这个字段主要用于记录用户所居住的位置。


/**
 * @Author linhao
 * @Date created in 7:22 上午 2021/9/3
 */
public class UserInfo {
    private String username;
    private short age;
    private short height;
    private String phone;
    private String home;
}
复制代码


慢慢地随着业务的发展,这个实体类中的home字段开始进行了扩展,UserINfo类变成了以下模式:


/**
 * @Author linhao
 * @Date created in 7:22 上午 2021/9/3
 */
public class UserInfo {
    private String username;
    private short age;
    private short height;
    private String phone;
    private String home;
    /**
     * 省份
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 地区
     */
    private String region;
    /**
     * 街道
     */
    private String street;
}
复制代码


此时对于这个实体类的设计就会有了新的观点:


这个类中关于居住部分的字段开始渐渐增加,应该将住址部分抽象出来成一个Address字段,拆分后变成如下所示:


/**
 * @Author linhao
 * @Date created in 7:22 上午 2021/9/3
 */
public class UserInfo {
    private String username;
    private short age;
    private short height;
    private String phone;
    private String home;
    /**地址信息**/
    private Address address;
}
复制代码


这样的拆分可以确保UserInfo对象的职责单一,类似的扩展还可以蔓延到后续的email,tel相关属性。


举这个例子只是想简单说明,我们在对一些类进行设计的时候,其实就已经使用到了单一责任原则。另外还有可能在以下场景中也有运用到该原则:


类中的属性字段特别多,一个bean中充斥了几十个属性。此时也可以尝试使用单一责任原则,将不同属性的字段归纳为一个bean进行收拢。


一个大对象,例如XXXManager或者XXXContext这种名词定义的对象中,可能引入了一大堆的外部依赖,此时可以按照依赖的类别来进行拆分。


业务代码块中,我们定义了一个UserService类,然后这个类里面写了一坨的用户密码,手机号,身份证号解密加密相关的私有函数,这时候可以不妨尝试将这些私有方法统统抽象成为一个独立的Util当中,从而减少UserService中的代码量。


所以最终你会发现,单一责任原则还是一个比较需要依靠主观意识去拿捏的一项技巧。随着我们实践开发经验的逐渐提升,自然就会明白什么样的代码该进行良好的抽象与优化了。


开放封闭原则案例



关于这条原则我个人感觉要想较好地理解它,需要有具体的实战案例代码,所以接下来我打算用一个自己曾经在工作中遇到的实际场景和你分享:


我做的一款社交小程序应用当中,当一个用户注册完信息之后,需要通知到系统下游,主要是修改某些后台数据,分配对应的员工去跟进这个用户。


所以大体的代码设计可能如下所示:


public class RegisterHandler {
    public void postProcessorAfterRegister(long userId){
        //通知员工
        notifyWorker(userId);
    }
    private void notifyWorker(long userId){
        //通知部分的逻辑
    }
}
public interface IRegisterHandler {
    /**
     * 用户注册之后处理函数
     *
     * @param userId 用户渠道ID
     */
    void postProcessorAfterRegister(long userId);
}
复制代码


但是注册的渠道类型有许多种,例如公众号,小程序二维码传播,小程序的分享链接,其他App渠道等等。所以代码结构需要做部分调整:


首先需要修改一开始设计的接口模型:


/**
 * @Author linhao
 * @Date created in 7:56 上午 2021/9/3
 */
public interface IRegisterHandler {
    /**
     * 用户注册之后处理函数
     *
     * @param userId 用户ID
     * @param sourceId 注册渠道ID
     */
    void postProcessorAfterRegister(long userId,int sourceId);
}
复制代码


然后还需要修改实际的实现规则:


/**
 * @Author linhao
 * @Date created in 7:48 上午 2021/9/3
 */
public class RegisterHandler implements IRegisterHandler {
    @Override
    public void postProcessorAfterRegister(long userId, int sourceId) {
        //通知员工
        if (sourceId == 1) {
            //doSth
        } else if (sourceId == 2) {
            //doSth
        } else if (sourceId == 3) {
            //doSth
        } else {
            //doSth
        }
        notifyWorker(userId, sourceId);
    }
    private void notifyWorker(long userId, int sourceId) {
        //通知部分的逻辑
    }
}
复制代码


这样的代码扩展就会对原先定义好的结构造成破坏,也就不满足我们所认识的开放封闭原则了。(虽然我在上文中有提及过对于开放封闭原则来说,并不是强制要求不对代码进行修改,但是现在的这种扩展模式已经对内部结构造成了较大的伤害。)


所以我们可以换一种设计思路去实现。


首先我们需要将注册的传入参数定义为一个对象类型,这样在后续新增参数的时候只需调整对象内部的字段即可,不会对原有接口的设计造成影响:


/**
 * @Author linhao
 * @Date created in 8:07 上午 2021/9/3
 */
public class RegisterInputParam {
    private long userId;
    private int source;
    public long getUserId() {
        return userId;
    }
    public void setUserId(long userId) {
        this.userId = userId;
    }
    public int getSource() {
        return source;
    }
    public void setSource(int source) {
        this.source = source;
    }
}
复制代码


接着可以将注册逻辑拆解为注册处理器和使用注册处理器的service模块:


/**
 * @Author linhao
 * @Date created in 7:56 上午 2021/9/3
 */
public interface IRegisterService {
    /**
     * 用户注册之后处理函数
     *
     * @param registerInputParam 用户注册之后的传入参数
     */
    void postProcessorAfterRegister(RegisterInputParam registerInputParam);
}
复制代码


注册处理器内部才是真正的核心部分:


/**
 * @Author linhao
 * @Date created in 8:10 上午 2021/9/3
 */
public abstract class AbstractRegisterHandler {
    /**
     * 获取注册渠道ID
     *
     * @return
     */
    public abstract int getSource();
    /**
     * 注册之后的核心通知模块程序
     *
     * @param registerInputParam
     * @return
     */
    public abstract boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam);
}
复制代码


具体的实现交给了各个Handler组件:


公众号注册渠道的后置处理器


/**
 * @Author linhao
 * @Date created in 8:16 上午 2021/9/3
 */
public class GZHRegisterHandler  extends AbstractRegisterHandler {
    @Override
    public int getSource() {
        return RegisterConstants.RegisterEnum.GZH_CHANNEL.getCode();
    }
    @Override
    public boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam) {
        System.out.println("公众号处理逻辑");
        return true;
    }
}
复制代码


app注册渠道的后置处理器


/**
 * @Author linhao
 * @Date created in 8:16 上午 2021/9/3
 */
public class AppRegisterHandler extends AbstractRegisterHandler {
    @Override
    public int getSource() {
        return RegisterConstants.RegisterEnum.APP_CHANNEL.getCode();
    }
    @Override
    public boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam) {
        System.out.println("app处理逻辑");
        return true;
    }
}
复制代码


不同的注册渠道号通过一个枚举来进行管理:


public class RegisterConstants {
    public enum RegisterEnum{
        GZH_CHANNEL(0,"公众号渠道"),
        APP_CHANNEL(1,"app渠道");
        RegisterEnum(int code, String desc) {
            this.code = code;
            this.desc = desc;
        }
        int code;
        String desc;
        public int getCode() {
            return code;
        }
    }
}
复制代码


接下来,对于注册的后置处理服务接口进行实现:


/**
 * @Author linhao
 * @Date created in 7:48 上午 2021/9/3
 */
public class RegisterServiceImpl implements IRegisterService {
    private static List<AbstractRegisterHandler> registerHandlerList = new ArrayList<>();
    static {
        registerHandlerList.add(new GZHRegisterHandler());
        registerHandlerList.add(new AppRegisterHandler());
    }
    @Override
    public void postProcessorAfterRegister(RegisterInputParam registerInputParam) {
        for (AbstractRegisterHandler abstractRegisterHandler : registerHandlerList) {
            if(abstractRegisterHandler.getSource()==registerInputParam.getSource()){
                abstractRegisterHandler.doPostProcessorAfterRegister(registerInputParam);
                return;
            }
        }
        throw new RuntimeException("未知注册渠道号");
    }
}
复制代码


最后通过简单的一段测试程序:


public class Test {
    public static void main(String[] args) {
        RegisterInputParam registerInputParam = new RegisterInputParam();
        registerInputParam.setUserId(10012);
        registerInputParam.setSource(0);
        IRegisterService registerService = new RegisterServiceImpl();
        registerService.postProcessorAfterRegister(registerInputParam);
        RegisterInputParam registerInputParam2 = new RegisterInputParam();
        registerInputParam2.setUserId(10013);
        registerInputParam2.setSource(1);
        registerService.postProcessorAfterRegister(registerInputParam2);
        System.out.println("=======");
    }
}
复制代码


这样的设计和起初最先前的设计相比有几处不同的完善点:


新增不同注册渠道的时候,只需要关心注册渠道的source参数。


同时对于后续业务的拓展,新增不同的注册渠道的时候,RegisterServiceImpl只需要添加新编写的注册处理器类即可。


网络异常,图片无法展示
|


再回过头来看,这样的一段代码设计是否满足了开放封闭原则呢?


每次新增不同的注册类型处理逻辑之后,程序中都只需要新增一种Handler处理器,这种处理器对于原先的业务代码并没有过多的修改,从整体设计的角度来看,并没有对原有的代码结构造成影响,而且灵活度相比之前有所提高。这也正好对应了,对扩展开放,对修改关闭。


如果你对设计模式有一定了解的话,可能还会发现大多数常用的设计模式都在遵守这一项原则,例如模版模式,策略模式,责任链模式等等。

目录
相关文章
|
数据安全/隐私保护
七大设计原则之单一职责原则应用
七大设计原则之单一职责原则应用
69 0
|
设计模式 关系型数据库 数据安全/隐私保护
软件架构设计原则之单一职责原则
单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假设我们有一个类负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能导致另一个职责的功能发生故障。这样一来,这个类就存在两个导致类变更的原因。如何解决这个问题呢?将两个职责用两个类来实现,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说,就是一个类、接口或方法只负责一项职责。
118 0
软件架构设计原则之单一职责原则
|
设计模式 人工智能 Java
软件架构设计原则之依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并且能够降低修改程序所造成的风险。接下来看一个案例,还是以Course(课程)为例,先来创建一个类Tom:
100 0
浅谈设计原则
什么是单一职责原则,在我理解看来就是一个东西如果发生问题那么就有且仅有一个原因导致它发生问题。它的准确解释就是,就一个类而言,应该仅有一个引起它变化的原因。如果一个类承担的职责过多,就等于耦合度加大,当变化发生时,设计会受到破坏。最好的例子就是将界面和业务进行分离。做设计应该让类只有一个职责。
|
设计模式 关系型数据库
软件架构设计原则之迪米特法则
迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,而出现在方法体内部的类不属于朋友类。
110 1
|
设计模式 人工智能 前端开发
软件架构设计原则之开闭原则
开闭原则(Open-Closed Principle,OCP)是指一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性及可维护性。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定、灵活的系统。例如版本更新,我们尽可能不修改源代码,但是可以增加新功能。
136 0
|
设计模式 关系型数据库
软件架构设计原则之接口隔离原则
接口隔离原则符合我们常说的高内聚、低耦合的设计思想,可以使类具有很好的可读性、可扩展性和可维护性。我们在设计接口的时候,要多花时间去思考,要考虑业务模型,包括对以后有可能发生变更的地方还要做一些预判。所以,对于抽象、对于业务模型的理解是非常重要的。下面我们来看一段代码,对一个动物行为进行抽象描述。
106 0
|
设计模式 关系型数据库
软件架构设计原则之里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为T1的对象o1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
81 0
|
设计模式 关系型数据库
|
数据安全/隐私保护
软件架构设计原则--单一职责原则
> 本专栏内容参考自:咕泡学院Tom老师的《Spring5核心原理与30个类手写实战》,仅作个人学习记录使用,如有侵权,联系速删
软件架构设计原则--单一职责原则