在实际的软件编程过程中,代码设计的合理性会直接决定项目的开发复杂程度,一个好的设计模型,会让你的软件开发过程顺畅无比!
一、介绍
在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书,该书首次提到了软件开发中设计模式的概念,四位作者合称 GOF(全拼 Gang of Four),简称四人帮!
书中共说到 23 种设计模式,这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)。
当然,随着软件的快速发展,还出现另一类我们所熟知的设计模式:J2EE 设计模式。
首先要跟大家说明的是,设计模式其实不是一项新技术,而是众多软件开发人员经过相当长的一段时间的试用和试错总结出来的一套软件设计开发实践方案,学习这些知识有助于经验不足的开发人员通过一种简单快捷的方式来学习软件设计。
也不BB了,下面我们就一起来深入了解一下大神们总结的这套方法论!
二、方法论
2.1、单一职责原则
单一职责原则,顾名思义,一个类负责一个功能的处理,比如我们在代码中经常会涉及到调用第三方的接口,通常我们往往会将一个接口请求方法封装成一个http请求的工具类,这样其他的调用方能非常方便的进行调用。
单一职责原则,主要的目的是将一个功能划分到合适的粒度,让这些各自执行单一职责的类,各司其职。
还有我们常常说的mvc模式,将数据操作与业务逻辑处理进行分离,也是属于单一职责原则的一种。
2.2、开闭原则
开闭原则,指的是:对扩展开放,对修改关闭。
这里的意思是在增加新功能的时候,能不改代码就尽量不要改,如果只增加代码就完成了新功能,那是最好的。
下面我举个简单的例子,大家可能就懂了。
在实际的项目开发过程中,尤其是与第三方公司进行接口对接的时候,例如 a 公司,他的接口协议要求采用rsa加密;b 公司,他的接口协议采用aes加密,c公司,他的接口协议要求采用md5就可以了...
在一开始的时候,可能你没有想那么多,设想所有其他公司跟你对接的时候,全部采用rsa加密,那么当第二家公司来了时候,要求采用aes,这个时候,你必须得改代码才能支持这种功能,假如又来了其他公司呢?
在这种情况下,我们可以采用面向接口编程的思想,定义一个抽象的方法,然后针对不同的加密算法,编写不同的实现类,当新来了一家公司要求采用不同的加密算法时,我们只需要扩展一个新的实现类,在逻辑处理层进行路由即可快速完成开发和对接!
实现开闭原则的主要步骤就是将业务方法里面的逻辑进行抽象化,从而实现逻辑层面代码的解耦。
2.3、里氏代换原则
里氏替换原则,通俗的说就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
当子类在重写父类的方法时,如果使用不当,很可能会改变父类方法中的逻辑语义,进而造成逻辑处理混乱。
在实际的业务开发中,尤其是大家在使用类继承操作时,应当谨慎重新父类方法,方法定义的时候,名称也尽量不要发生重名。
同时,应该注意类过多的继承,例如 A 类继承自 B 类,B 类 继承自 C 类,C 类继承 D 类,这种场景下,第一:逻辑很可能非常复杂,代码可读性可能很差,第二种可能会出现方法名或者属性名冲突。
我曾经修改一个老项目 bug 的时候,就出现过这种情况,一个实体类出现三层关系的继承,当我把子类进行序列化成json的时候,突然报错,原因就是因为里面有个属性,在基类里面也定义了,但是类型却不一样,也就是说在这个子类里面有两个相同的属性,只是字段类型不同而已,从而导致这个序列化报错。
因此,大家在使用类继承操作时,一定要谨慎处理,最好的情况下是一层关系继承,这样即使出现问题了,也好排查,范围可控;继承关系多了,反而代码变的非常复杂,每个方法逻辑都需要一一梳理清楚,你才敢去下手,这种情况下,反而为难了自己!
2.4、依赖倒转原则
依赖倒转原则,简单的说就是面向接口编程,依赖于抽象而不依赖于具体,他是实现开闭原则的一个途径。
依赖倒转原则主要有以下几点:
- 高层的模块不应该依赖于低层的模块,这两者都应该依赖于其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
换句话说,就是将具体类里面的方法进行逻辑抽象化,提升到接口或者抽象类里面,由具体的实现类去实现具体的业务逻辑,抽象层不关注细节。
也就是我们俗称的,不管遇到啥事情,先看大局,在看小点。
举个例子,例如我们常常使用的MVC框架,在Service层通常会先编写接口类,然后在编写服务实现类,这就是典型的面向接口编程,这种方案有哪些好处呢?
- 可以减少研发人员并行开发引起的风险
- 可以显著提高代码的可读性和可维护性
- 可以降低类间的耦合性
当一个大项目开发的时候,试想一下,如果2个以上研发人员在编写同一个类的同一个方法,没有定义接口的情况下,随着时间的流逝,这个类大概率会非常混乱,同时,里面的方法估计阅读起来也很困难,一眼望去基本上不知道这个方法是干嘛的,尤其是重载方法很多的时候,尤其困难。
当我们引入接口之后,将所有的核心方法全部抽象定义,当我们对其进行二次开发的时,也会能快速定位到以前的和谐方法,然后进行快速定位和维护。
面向接口编程,还有一个很大的好处,就是可以降低类之间的耦合度。
2.5、接口隔离原则
接口隔离原则的意思是:尽量将一个接口拆的更细,使用多个隔离的接口,比使用单个接口要好。
这个原则基本上是从大型软件架构出发、便于升级和维护的思想诞生出来的。
比如,一个第三方保险服务系统里面,如果是小项目,你可以在一个接口里面定义投保方法、取消方法、 退保方法、 理赔方法、 保单方法这5种方法。
当你只跟一家保险公司合作的时候,这套接口定义是没问题的,但是假如现在公司的业务发展的非常快,已经引入10几家保险公司了,你会发现这个接口基本上顶不住了,在此有两种方法可以解决这种技术难题:
- 针对不同的保险公司,又重新定义一个接口,然后还是里面的5种方法
- 针对不同的操作行为,比如投保,单独定义一套接口,然后针对不同的保险公司编写不同的实现类
这两种其实本质都是将一个接口,拆成多个颗粒度更细的接口,然后从行为上,让各种的实现类更加独立,进而降低类之间的耦合度,开发起来层次清晰,有利于系统的稳定!
2.6、组合/聚合复用原则
合成复用原则的意思就是:尽量使用组合/聚合的方式,而不是使用继承。
所谓组合/聚合的方式,其实就是我们在springMVC里面使用的ioc依赖注入方式,
@Component
public class AService {
@Autowired
private BService bService;
//业务代码...
}
AService类调用BService类,有两种方式可以实现,第一种就是通过继承方式,可以直接调用;第二种方式就是通过ioc依赖注入的方式,在类里面进行组合操作,然后进行调用。
很明显,第二种方式,我们使用的爱不释手!
这种方式有个好处就是,代码层次清晰,编写优雅,用上之后容易上瘾,推荐使用!
2.7、迪米特法则
迪米特法则,又称最少知道原则,意思就是说:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
类似单一原则的思想,例如我们编写实体类的时候,基本上就是属性名称,加get/set。
public class User {
private Long userId;
private String userName;
public Long getUserId(){
return this.userId;
}
public void setUserId(Long userId){
this.userId = userId;
}
public String getUserName(){
return this.userName;
}
public void setUserName(String userName){
this.userName = userName;
}
}
当一个实体里面出现跟自己没太大关系的代码时,例如数据运算逻辑处理,这个时候应当将这部分代码全部清理出去,交由业务处理层来出来。
从实体类的定义来说,它主要的功能就是承担数据展示,当多个业务处理层在使用这个实体类的时候,如果你们嵌套了很多的数据运算逻辑,可能在返回的时候,客户端得到的实体类的属性值,并不是正确的结果!
三、小结
上面一共介绍了 7 种设计原则,基本上都是大神们经过很多次血的教训,总结出来的一套方法论。
这些设计原则,可以给同行在实际的软件设计过程中,提供一些思路,具体的应用,还需要结合实际的业务场景进一步思考,怎么让系统变的更加可靠,开发更佳迅速,代码阅读起来更佳轻松,关键在于灵活运用!