设计模式七大原则
单一职责原则:
单一职责原则(Single Responsibility Principle, SRP)的定义是:指一个类或者模块应该有且只有一个改变的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
含义:对于一个类,只有一个引起该类变化的原因;该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。
个人理解:一个类应该只负责一项职责,最满足的情况是每个类除构造方法外只有个方法,但是这种情况太极端,我们可以根据实际情况满足单一职责原则。在实际工作中,有一个经常会用到的设计模式,DAO模式,又叫数据访问对象,里面定义了数据库中表的增、删、改、查操作,按照单一职责原则,为什么不把增、删、改、查分别定义成四种接口?这是因为数据库的表操作,基本上就是这四种类型,不可能变化,所以没有必要分开定义,反而经常变化的是数据库的表结构,表结构一变,这四种操作都要跟着变。所以通常我们会针对一张表实现一个DAO,一张表就代表一种类型的职责。
**解决:**将不同的职责封装到不同的类或者模块中。 当有新的需求将现有的职责分为颗粒度更小的职责的时候,应该及时对现有代码进行重构。当系统逻辑足够简单,方法足够少,子类够少或后续关联够少时,也可以不必严格遵循你SRP原则,避免过度设计、颗粒化过于严重。
例子:
这个例子比较简单,主要是为了反映不遵循单一职责原则的危害。
在run方法中,违背了单一职责原则,使得轮船在公路上跑。
package com.pro.princle.singleresponsibility;
public class SingleResponsibility1 {
public static void main(String[] args) {
Vehicle vehicle=new Vehicle();
vehicle.run("汽车");
vehicle.run("轮船");
vehicle.run("飞机");
}
}
class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在公路上跑....");
}
}
修改方案:
方案一:将类进行分解,将Vehicle分解为三个类,分别为RoadVehicle、AirVehicle和WaterVehicle,但是需要对客户端进行较大程度的修改。(遵循了单一职责)
package com.pro.princle.singleresponsibility;
public class SingleResponsibility2 {
public static void main(String[] args) {
RoadVehicle roadVehicle =new RoadVehicle();
roadVehicle.run("汽车");
AirVehicle airVehicle=new AirVehicle();
airVehicle.run("飞机");
WaterVehicle waterVehicle =new WaterVehicle();
waterVehicle.run("轮船");
}
}
class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在公路上跑....");
}
}
class AirVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在天空中跑....");
}
}
class WaterVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在水上跑....");
}
}
方案二:将方法进行分解,将Vehicle的run方法分解成runRoad()、runAir()和runWater(),也需要对客户端进行修改,修改程度小。这里虽然没有在类的级别遵循单一职责原则,但是在方法级别上还是遵循的单一职责原则。
package com.pro.princle.singleresponsibility;
public class SingleResponsibility3 {
public static void main(String[] args) {
Vehicle2 vehicle = new Vehicle2();
vehicle.roadRun("汽车");
vehicle.AirRun("轮船");
vehicle.WaterRun("飞机");
}
}
//这种方法没有对原来的类进行了较大的修改,只是增加方法
//这里虽然没有在类这个级别上遵守单一原则,但在方法级别上任然是遵守单一原则的
class Vehicle2 {
public void roadRun(String vehicle) {
System.out.println(vehicle + "在公路上跑....");
}
public void AirRun(String vehicle) {
System.out.println(vehicle + "在天上跑....");
}
public void WaterRun(String vehicle) {
System.out.println(vehicle + "在水上跑....");
}
}
接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP)的定义:是客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少。
含义:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
个人理解:尽可能的将接口细化。
解决:适度细化接口,将臃肿的接口拆分为独立的几个接口。
例子:
巨好理解,不做解释
接口细化
依赖倒转原则
依赖倒转原则(Dependency Inversion Principle,DIP)的定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
依赖倒置原则的包含如下的三层含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
含义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
解决:面向接口编程,使用接口或者抽象类制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
例子:
遵循依赖倒转原则
里氏替换原则
里氏代换原则(Liskov Substitution Principle,LSP)的定义是:所有引用基类的地方必须能透明地使用其子类的对象,也可以简单理解为任何基类可以出现的地方,子类一定可以出现。
含义:所有引用基类的地方必须能透明地使用其子类的对象。
如果一个方法接收Map类型参数,那么它一定可以接收Map的子类参数例如HashMap、LinkedHashMap、ConcurrentHashMap类型的参数;但是返过来,如果另一个方法只接收HashMap类型的参数,那么它一定不能接收所有Map类型的参数,否则它可以接收LinkedHashMap、ConcurrentHashMap类型的参数。
解决:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法;子类中可以增加自己特有的方法;当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸变,则建议断开继承关系,采用依赖,聚合,组合等关系代替继承。
例子:
鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”
程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。正确的做法是:取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。
开闭原则
开闭原则(Open Closed Principle,OCP)的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原(是"原",指原来的代码)代码的情况下进行扩展。
含义:对扩展开放,对修改封闭。即系统进行扩展是被鼓励的,对现有系统代码进行修改是不被支持的。也就是说,当软件有新的需求变化的时候,只需要通过对软件框架进行扩展来适应新的需求,而不是对框架内部的代码进行修改。
开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
例子:
以书店销售书籍为例,其类图如下:
项目投产生,书籍正常销售,但是我们经常因为各种原因,要打折来销售书籍,这是一个变化,我们要如何应对这样一个需求变化呢?
我们有下面三种方法可以解决此问题:
- 修改接口
在IBook接口中,增加一个方法getOffPrice(),专门用于进行打折处理,所有的实现类实现此方法。但是这样的一个修改方式,实现类NovelBook要修改,同时IBook接口应该是稳定且可靠,不应该经常发生改变,否则接口作为契约的作用就失去了。因此,此方案否定。 - 修改实现类
修改NovelBook类的方法,直接在getPrice()方法中实现打折处理。此方法是有问题的,例如我们如果getPrice()方法中只需要读取书籍的打折前的价格呢?这不是有问题吗?当然我们也可以再增加getOffPrice()方法,这也是可以实现其需求,但是这就有二个读取价格的方法,因此,该方案也不是一个最优方案。 - 通过扩展实现变化
我们可以增加一个子类OffNovelBook,覆写getPrice方法。此方法修改少,对现有的代码没有影响,风险少,是个好办法。
下面是修改后的类图:
迪米特法则
迪米特法则(Law of Demeter,LOD),有时候也叫做最少知识原则(Least Knowledge Principle,LKP),它的定义是:一个软件实体应当尽可能少地与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类(中间类或者跳转类)来转达。
**含义:**每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
个人理解:类对外除了提供public方法,不要对外泄露任何信息。
解决:不发生依赖、关联、组合、聚合等耦合关系的陌生类不要作为局部变量的形式出现在类的内部。
直接的朋友:每个对象都会与其他对象有***耦合关系***,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现***成员变量,方法参数,方法返回值***中的类为直接的朋友,而出现在***局部变量中的类不是直接的朋友***。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
例子:
明星与经纪人的关系实例。
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则
合成复用原则
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)一般也叫合成复用原则(Composite Reuse Principle, CRP),定义是:尽量使用合成/聚合,而不是通过继承达到复用的目的。
合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向内部持有的这些对象的委派达到复用已有功能的目的,而不是通过继承来获得已有的功能。
含义:当要扩展类的功能时,优先考虑使用合成/聚合,而不是继承。
解决:当类与类之间的关系是"Is-A"时,用继承;当类与类之间的关系是"Has-A"时,用组合。
例子:
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。图 1 所示是用继承:关系实现的汽车分类的类图。
可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如图