六大设计原则

设计模式(Design Pattern,DP)能指导你如何去设计一个优秀的架构、编写一段健壮的代码、解决一个复杂的需求。

为了便于理解,先回顾下类图、对象图和包图中的主要建模元素:

单一职责原则

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。
单一职责原则的定义是:应该有且仅有一个原因引起类的变更。用“职责”或“变化原因”来衡量接口或类设计得是否优良

单一职责适用于接口、类,同时也适用于方法

看一个例子:
RBAC模型(Role-Based Access Control,基于角色的访问控制),通过分配和取消角色来完成用户权限的授予和取消,把用户的信息抽取成一个BO(Business Object,业务对象),把行为抽取成一个Biz(Business Logic,业务逻辑)


IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。

在实际的使用中,我们更倾向于使用两个不同的类或接口:一个是IUserBO,一个是IUserBiz

对于接口,我们在设计的时候一定要做到单一,但是对于实现类就需要多方面考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。

纯理论地来讲,这个原则是非常优秀的,但是现实有现实的难处,你必须去考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。

里氏替换原则

继承的缺点:
▪ 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
▪ 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
▪ 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

里氏替换原则(Liskov Substitution Principle,LSP),所有引用基类的地方必须能透明地使用其子类的对象。只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑

里氏替换原则为良好的继承定义了规范:

1.子类必须完全实现父类的方法。在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承

2.子类可以有自己的方法和属性。有子类出现的地方父类未必就可以出现,向下转型(downcast)是不安全的

3.覆盖或实现父类的方法时输入参数可以被放大。子类要重载父类方法,参数范围必须大于父类(符合里氏替换原则),否则就是重写父类方法,使用override标识,直接调用子类方法。此时由于方法的输入参数不同,是重载(Overload)而不是覆写(Override)
重载是为了增强适应性,以前的参数还可以运行,而覆写是将之前的彻底改写。如果一个子类对象传入的参数对应的方法只在父类实现,而未再子类重写,就会调用父类的方法,根据里氏替换原则,现在这个子类被当作父类来用了。
我们再反过来想一下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务逻辑混乱子类的输入参数宽于或等于父类的输入参数,就是为了避免调用子类方法,优先调用子类方法

关于重载参数匹配,重载时(Overload)时的参数匹配规则如下:

  • 符合基本类型的赋值规则,即低精度的值可以赋值给高精度或同精度的变量,而高精度的不可以赋值给低精度;
  • 赋值规则匹配出多条时,选精度最小的。

4.覆写或实现父类的方法时输出结果可以被缩小。父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP)在Java语言中的表现就是:
▪ 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
▪ 接口或抽象类不依赖于实现类;
▪ 实现类依赖接口或抽象类。

更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计),被依赖者的变更竟然让依赖者来承担修改的成本,这是不合理的。采用依赖倒置原则可以减少类间的耦合性,降低并行开发引起的风险

在IDriver中,通过传入ICar接口实现了抽象之间的依赖关系,Driver实现类也传入了ICar接口,至于到底是哪个型号的Car,需要在高层模块中声明。

TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。先写好单元测试类,然后再写实现类,

可以引入一个JMock工具,其最基本的功能是根据抽象虚拟一个对象进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DriverTest extends TestCase{
Mockery context = new JUnit4Mockery();
@Test
public void testDriver() {
//根据接口虚拟一个对象
final ICar car = context.mock(ICar.class);
IDriver driver = new Driver();
//内部类
context.checking(new Expectations(){{
oneOf (car).run();
}});
driver.drive(car);
}
}

对象的依赖关系有三种方式来传递:

1.构造函数传递依赖对象。在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入

2.Setter方法传递依赖对象。在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入

3.接口声明依赖对象,该方法也叫做接口注入。

我们怎么在项目中使用这个规则呢?
●每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
●变量的表面类型尽量是接口或者是抽象类
●尽量不要覆写基类的方法
●结合里氏替换原则使用,接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

依赖倒置原则是6个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,只要记住是“面向接口编程”就基本上抓住了依赖倒置原则的核心。

接口隔离原则

  • 接口尽量细化,同时接口中的方法尽量少。但同时根据接口隔离原则拆分接口时,首先必须满足单一职责原则。即最小的业务单位不能再继续分下去了
  • 接口要高内聚,在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本
  • 定制服务,只提供访问者需要的方法

决定接口的粒度大小只能根据经验和常识

迪米特法则

迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP),一个类应该对自己需要耦合或调用的类知道得最少。

迪米特法则还有一个英文解释是:Only talk to your immediate friends(只与直接的朋友通信。)类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API提供的类除外。
朋友类的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类

一个类应该对自己需要耦合或调用的类知道得最少。朋友关系太亲密了,耦合关系会变得异常牢固,从而把修改变更的风险扩散开了。
在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private、package-private(包类型,在类、方法、变量前不加访问权限,则默认为包类型)、protected等访问权限,是否可以加上final关键字等。

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。

开闭原则

依照Java语言的称谓,开闭原则是抽象类,其他五大原则是具体的实现类,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。开闭原则是Java世界里最基础的设计原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OffNovelBook extends NovelBook {
public OffNovelBook(String _name,int _price,String _author){
super(_name,_price,_author);
}
//覆写销售价格
@Override
public int getPrice(){
//原价
int selfPrice = super.getPrice();
int offPrice=0;
if(selfPrice>4000){ //原价大于40元,则打9折
offPrice = selfPrice * 90 /100;
}else{
offPrice = selfPrice * 80 /100;
}
return offPrice;
}
}

开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。

开闭原则的运用:

1.抽象约束

  • 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
  • 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
  • 抽象层尽量保持稳定,一旦确定即不允许修改。
1
2
3
4
public interface IComputerBook extends IBook{
//计算机书籍是有一个范围
public String getScope();
}

2.元数据(metadata)控制模块行为
什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。举个非常简单的例子,login方法中提供了这样的逻辑:先检查IP地址是否在允许访问的列表中,然后再决定是否需要到数据库中验证密码(如果采用SSH架构,则可以通过Struts的拦截器来实现),该行为就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control),使用最多的就是Spring容器