Java项⽬中常⽤的的五⼤设计原则
今天我们⼀起来聊聊关于设计原则相关的知识点。
SOLID五⼤原则是什么
SRP 单⼀责任原则
单⼀责任原则,从名字上我们就能⽐较好的去理解它。这项原则主张⼀个对象只专注于单个⽅⾯的逻辑,强调了职责的专⼀性。
举个例⼦:
学⽣管理系统中,我们需要提交⼀些学⽣的基本资料,那么学⽣信息相关的程序都交给了StudentService负责,如果我们要实现⼀个保存教师基本资料的功能就应该新建⼀个TeacherService去处理,⽽不应该写在StudentService当中。
OCP开放封闭原则
这项原则从我个⼈的⾓度去理解,它更加强调的是对于扩展的开放性,例如当我们需要调整某些实现逻辑的时候,尽量不要直接改动到原有的实现点。
register的名词
但是这⾥⾯有⼏个点容易被⼈们误解:
第⼀点
开放封闭原则虽然强调的是不要随意改动代原先代码到逻辑结构,但是并没有要求⼀定不能对代码进⾏改动!
第⼆点
同样是代码改动,如果我们可以从功能,模块的⾓度去看,实际上代码的改动更多地可以被认作为是⼀种“扩展”。
关于如何做到开放封闭原则,下⽂我会专门⽤⼀个案例来进⾏介绍。
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的字段,这个字段主要⽤于记录⽤户所居住的位置。
private String username;
private short age;
private short height;
private String phone;
private String home;
}
慢慢地随着业务的发展,这个实体类中的home字段开始进⾏了扩展,UserINfo类变成了以下模式:
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字段,拆分后变成如下所⽰:
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渠道等等。所以代码结构需要做部分调整:
⾸先需要修改⼀开始设计的接⼝模型:
public interface IRegisterHandler {
/**
* ⽤户注册之后处理函数
*
* @param userId ⽤户ID
* @param sourceId 注册渠道ID
*/
void postProcessorAfterRegister(long userId,int sourceId);
}
然后还需要修改实际的实现规则:
public class RegisterHandlerImpl 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) {
//通知部分的逻辑
}
}
这样的代码扩展就会对原先定义好的结构造成破坏,也就不满⾜我们所认识的开放封闭原则了。(虽然我在上⽂中有提及过对于开放封闭原则来说,并不是强制要求不对代码进⾏修改,但是现在的这种扩展模式已经对内部结构造成了较⼤的伤害。)
所以我们可以换⼀种设计思路去实现。
⾸先我们需要将注册的传⼊参数定义为⼀个对象类型,这样在后续新增参数的时候只需调整对象内部的字段即可,不会对原有接⼝的设计造成影响:
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模块:
public interface IRegisterService {
* ⽤户注册之后处理函数
*
* @param registerInputParam ⽤户注册之后的传⼊参数
*/
void postProcessorAfterRegister(RegisterInputParam registerInputParam);
}
注册处理器内部才是真正的核⼼部分:
public abstract class AbstractRegisterHandler {
/**
* 获取注册渠道ID
*
* @return
*/
public abstract int getSource();
/**
* 注册之后的核⼼通知模块程序
*
* @param registerInputParam
* @return
*/
public abstract boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam); }
具体的实现交给了各个Handler组件:
注册渠道的后置处理器
public class GZHRegisterHandler  extends AbstractRegisterHandler {
@Override
public int getSource() {
return RegisterConstants.RegisterEnum.Code();
}
@Override
public boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam) {
System.out.println("处理逻辑");
return true;
}
}
app注册渠道的后置处理器
public class AppRegisterHandler extends AbstractRegisterHandler {
@Override
public int getSource() {
return RegisterConstants.RegisterEnum.Code();
}
@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.desc = desc;
}
int code;
String desc;
public int getCode() {
return code;
}
}
}
接下来,对于注册的后置处理服务接⼝进⾏实现:
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) {
Source()==Source()){
abstractRegisterHandler.doPostProcessorAfterRegister(registerInputParam);
return;
}
}
throw new RuntimeException("未知注册渠道号");
}
}
最后通过简单的⼀段测试程序:
public class TestDesignPrinciple {
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处理器,这种处理器对于原先的业务代码并没有过多的修改,从整体设计的⾓度来看,并没有对原有的代码结构造成影响,⽽且灵活度相⽐之前有所提⾼。这也正好对应了,对扩展开放,对修改关闭。
如果你对设计模式有⼀定了解的话,可能还会发现⼤多数常⽤的设计模式都在遵守这⼀项原则,例如模版模式,策略模式,责任链模式等等。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。