目录
一、单一职责原则
单一职责原则的英文名称是:Single Responsibility Principle,简称SRP。它的内容是:应该有且只有一个引起类变化的原因。
例如下面这个类,它的设计就违反了单一职责原则:
经过职责划分后的类图如下:
重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。
分清职责后的代码示例:
......
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();
......
采用SRP的类图如下:
单一职责原则的好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义
- 可读性提高,复杂性降低
- 可维护性提高,可读性提高
- 变更引起的风险降低
单一职责适用于接口、类,同时也适用于方法。
二、里氏替换原则
里式替换原则的英文名称是:Liskov Substitution Principle,简称LSP。它的内容是:所有引用基类的地方必须能透明地使用其子类的对象。通俗地讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常。
里式替换原则为良好的继承定义了一个规范,它包含了4层含义:
(1)子类必须完全实现父类的方法
例如下面这个例子,模拟射击游戏的枪支类:
设计一个枪支抽象类,然后具体的枪支继承这个抽象类,并实现对应的shoot方法。
public abstract class AbstractGun {
public abstract void shoot();
}
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("手枪设计...");
}
}
//Rifle、MachineGun省略
在士兵类中,使用枪来杀敌,但是这个枪是抽象的,具体是什么类型的枪需要通过setGun方法来确定。
public class Soldier {
private AbstractGun gun;
public void setGun(AbstractGun gun) {
this.gun = gun;
}
public void killEnemy() {
System.out.println("士兵开始杀敌人...");
gun.shoot();
}
}
public class Client {
public static void main(String[] args) {
Soldier soldier = new Soldier();
soldier.setGun(new HandGun());
soldier.killEnemy();
}
}
在类中调用其他类时务必使用父类或者接口,如果不能使用父类或者接口,则说明类的设计已经违背了LSP原则。
下面定义一个玩具枪类:
如果还是使用原来的继承方式,由于玩具枪是不能射击的,所以此时的shoot方法就“没用”了。这就导致了正常的业务逻辑不能运行了。此时,有两种解决方法:
1)在Soldier类中增加instanceof的判断,如果是玩具枪就不能用来杀敌人。但是,这种修改每增加一个类就必须修改,所以不提倡。
2)将ToyGun脱离继承,建立一个独立的父类,并与AbstractGun建立关联委托关系。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
(2)子类可以有自己的个性
还是上面的例子,假设增加AUG狙击枪和狙击手类,此时除了实现shoot方法之外,还可以增加一些自己特有的方法。
public class AUG extends Rifle {
public void shoot() {
System.out.println("AUG射击...");
}
public void zoomOut() {
System.out.println("通过瞄准镜观察敌人...");
}
}
public class Snipper {
private AUG aug;
public void setGun(AUG aug) {
this.aug = aug;
}
public void killEnemy() {
aug.zoomOut();
aug.shoot();
}
}
(3)重写或者实现父类的方法时输入参数可以被放大
当我们重载父类中的方法的时候(注意:重载(Overload)父类的方法而不是重写(Override)),必须要满足子类中方法的前置条件(参数)必须与超类中被重载的方法的前置条件相同或者更宽松。
public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父类被执行...");
return map.values();
}
}
public class Son extends Father {
//放大输入参数类型
public Collection doSomething(Map map) {
System.out.println("子类被执行...");
return map.values();
}
}
最终的结果和使用父类时的打印结果是一样的。
public class Client {
public static void invoker() {
//父类存在的地方,子类就应该存在
//Father f = new Father();
Son f = new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void mian(String[] args) {
invoker();
}
}
//结果:父类被执行...
除非重写父类中的方法,否则,子类代替父类传递到调用者中,子类的方法永远不会执行。
(4)重写或者实现父类的方法时输出结果可以被缩小
同理,当我们重载父类中的方法的时候,必须要满足子类中方法的后置条件(返回值)必须与超类中被重载的方法的后置条件相同或者更狭窄。
总而言之,采用里式替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。
三、依赖倒转原则
依赖倒转原则的英文名称是:Dependence Inversion Principle,简称DIP。它主要有三层含义:
- 高层模块不应该依赖于底层模块,两者都用该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
依赖倒转原则在Java语言中的表现就是:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口和抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类。
对象的依赖关系有三种写法来传递:
(1)构造函数传递依赖对象
(2)Setter方法传递依赖对象
(3)接口声明依赖对象
如下面这个例子,按照下面这种设计就不符合依赖倒转原则,具体的汽车类应该依赖一个抽象类。
经过修改后的类图如下:
四、接口隔离原则
接口隔离原则的定义如下:
(1)客户端不应该依赖它不需要的接口;
(2)类间的依赖关系应该建立在最小的接口上。
概括为一句话为:建立单一接口,不要建立臃肿庞大的接口。同时,接口尽量细化,同时接口中的方法尽量少。
例如下面这个例子,定义了一个IPetteyGirl接口,声明所有的PetteyGirl的标准是goodLooking、niceFigure和greatTemperament,然而实际的情况并不是这样,显然这个接口设计的过于庞大了。
修改后的类图如下:
接口隔离原则和单一职责原则的区别:
- 单一职责原则要求的是类和接口的职责要一致
- 接口隔离原则要求接口中的方法尽量少
接口隔离原则对接口进行规范约束,包含了以下4层含义:
(1)接口要尽量小
但是“小”是有限度的,首先必须满足单一职责原则。
(2)接口要高内聚
高内聚就是提高接口、类、模块的处理能力,减少对外的交互。
(3)定制服务
定制服务就是单独为一个个体提供优良的服务,因此在接口设计时要求:只提供访问者需要的方法。
(4)接口的设计是有限度的
接口的设计粒度越小,系统越灵活。但是,灵活的同时也带来了结构的复杂性,开发难度增加,可维护性降低。
五、迪米特法则
迪米特法则的英文名称是:Law of Demeter,简称LoD,也称最少知识原则。它的规则是:一个对象应该对其他的对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或调用的类知道得最少。
如下面这个例子:老师希望体育委员统计女生的数量,在这个场景下,显然老师不需要与女生产生依赖关系,只需要与体育委员依赖即可。而在原来的类的设计中,老师和体育委员与女生同时存在依赖关系,这显然不符合迪米特法则。
public class Teacher {
public void commond(GroupLeader groupLeader) {
List<Girl> listGirls = new ArrayList();
//初始化女生
for(int i = 0; i < 20; i++) {
listGirls.add(new Girl());
}
groupLeader.countGirls(listGirls);
}
}
public class GroupLeader {
public void countGirls(List<Girl> listGirls) {
System.out.println("女生数量是:" + listGirls.size());
}
}
根据迪米特法则进行如下修改:
public class Teacher {
public void commond(GroupLeader) {
groupLeader.countGirls(listGirls);
}
}
public class GroupLeader {
private List<Girl> listGirls;
public GroupLeader(List<Girl> listGirls) {
this.listGirls = listGirls;
}
public void countGirls() {
System.out.println("女生数量是:" + listGirls.size());
}
}
迪米特法则对类的低耦合提出了明确的要求,包含以下4中含义:
(1)“只和朋友交流”
如果两个对象耦合,那么它们就是“朋友”关系。一个类只和朋友交流,不与陌生类交流。不要出现getA().getB()这种情况,类与类之间的关系是建立在类间的,而不是方法间。因此,一个方法尽量不引入一个类中不存在的对象(JDK API提供的方法除外)。
(2)“朋友间也是有距离的”
尽量不要对外公布太多public方法和非静态的public变量,多使用private、package-private、protected等访问权限。
(3)“是自己的就是自己的”
如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
(4)谨慎使用Serializable
六、开闭原则
开闭原则是六大设计原则中最基础的设计原则。它的定义是:一个实体(如类、模块或者函数)的设计应该对扩展开放,对修改关闭。
开闭原则的核心思想是面向抽象编程。通过接口或抽象类来约束一组可能变化的行为,从而实现对扩展开放,任何时候我们都不会直接去修改原来类中的内容,从而实现了对修改关闭。
如下面这个例子,如果我们希望再想实现图书“打折”的效果,我们不应该直接去增加一个setPrice的方法或者再原有的getPrice方法中进行修改,正确的做法是:创建一个打折类,并且这个类继承自NovelBook类,通过重写打折类中的getPrice方法来实现。
public class OffNovelBook extends NovelBook{
public OffNovelBook(String name,int price,String author){
super(name,price,author);
}
//覆写价格方法,当价格大于40,就打8析,其他价格就打9析
public int getPrice(){
if(this.price > 40){
return this.price * 0.8;
}else{
return this.price * 0.9;
}
}
}
开闭原则包含三层含义:
(1)通过接口或抽象类来约束扩展,对扩展进行边界限定,不允许出现在接口或者抽象类中不存在的public方法;
(2)参数类型、引用类型尽量使用接口或者抽象类,而不是实现类;
(3)抽象类尽量保持稳定,一旦确定即不允许修改。
开闭原则的重要性:
- 面向对象的要求
- 提高代码的复用性
- 提高代码的可维护性
- 便于进行单元测试