从头捋一遍Java项目中的五大设计原则,就不信你学不会!

学生管理系统中,我们需要提交一些学生的基本资料,那么学生信息相关的程序都交给了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 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处理器,这种处理器对于原先的业务代码并没有过多的修改,从整体设计的角度来看,并没有对原有的代码结构造成影响,而且灵活度相比之前有所提高。这也正好对应了,对扩展开放,对修改关闭。

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

里氏替换原则

我认为,里氏替换原则更多是体现在了父子类继承方面,强调的是子类在继承了父类对象的时候不应该破坏这个父类对象的设计初衷。

举个例子来说:

我们定义了一个提款的服务:

/**

* @Author linhao

* @Date created in 11:21 上午 2021/9/4

*/

public interface DrawMoneyService {

/**

* 提款函数

* @param drawMoneyInputParam

*/

void drawMoney(DrawMoneyInputParam drawMoneyInputParam);

}

对应的是一个抽象实现父类:

/**

* @Author linhao

* @Date created in 11:25 上午 2021/9/4

*/

public abstract class AbstractDrawMoneyServiceImpl implements DrawMoneyService{

/**

* 设计初衷,需要对提现金额进行参数校验

*

* @param drawMoneyInputParam

*/

@Override

public abstract void drawMoney(DrawMoneyInputParam drawMoneyInputParam);

}

正常的子类继承对应父类都应该是对入参进行一个校验判断,如果金额数值小于0,自然就不允许提现了。

/**

* @Author linhao

* @Date created in 11:22 上午 2021/9/4

*/

public class AppDrawMoneyServiceImpl extends AbstractDrawMoneyServiceImpl{

@Override

public void drawMoney(DrawMoneyInputParam drawMoneyInputParam) {

if(drawMoneyInputParam.getMoney()>0){

//执行提款程序

}

System.out.println(“app提款业务”);

}

}

但是如果某个实现的子类当中违背了这一设计原则,例如下边这种:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

89975640)]
[外链图片转存中…(img-zSqAIYcf-1712889975641)]
[外链图片转存中…(img-Db5n22G6-1712889975641)]
[外链图片转存中…(img-x3dlAXXW-1712889975641)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-j0DsEgLj-1712889975642)]

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

[外链图片转存中…(img-Lxko49qp-1712889975642)]

[外链图片转存中…(img-x2edgZ4Z-1712889975642)]

[外链图片转存中…(img-CEhqQb4e-1712889975643)]

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-9ZwiPxLy-1712889975643)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值