设计模式

设计模式的六大原则

1、开闭原则(Open Close Principle)

开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。

通俗来讲就是一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。软件实体指的就是代码,例如一个模块、类、方法。

用例子说明开闭原则,需求是销售饮料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.principle.OCP.version1;

public interface Water {
public String getName();
public String getColor();
public int getPrice();
}

package org.principle.OCP.version1;

public class Cola implements Water{

private String name = "可口可乐";
private String color = "黑色";
private int price = 4;

public Cola(String name, String color, int price){
this.name = name;
this.color = color;
this.price = price;
}
public String getName() {
return name;
}

public String getColor() {
return color;
}

public int getPrice() {
return price;
}

@Override
public String toString() {
return "【" + getName() + "】是【" + getColor() + "】颜色的,价格为【" + getPrice() + "】元";
}
}

public class Customer {
public static void main(String[] args) {
Water cola = new Cola("可口可乐","黑色",4);
System.out.println(cola.toString());

Water baishi = new Cola("百事可乐","黑色",5);
System.out.println(baishi.toString());
}
}

上述代码是Customer类创建可乐对象,从而获得价格,这是一个很简单的代码。但需求变了,需要打8折销售,请问如何实现?

  1. 修改Cola的getPrice方法代码,加入打八折功能,这是最直接的,但有问题,我想看原价怎么办?
  2. 在Cola类中,添加打折方法discountPrice,也可以,但需要修改Water接口,这带来的影响就太大了,如果Water接口被10个类实现,那么就会影响10个类,这显然是不现实的,接口是约束、契约,是稳定的,不能随意修改
  3. 增加一个DiscountCola类,继承Cola,然后重写getPirce方法,这样无需改动已有代码只需要增加类即可,这就满足了**【扩展开发,对修改关闭】**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.principle.OCP.version1;

public class DiscountCola extends Cola{
public DiscountCola(String name, String color, int price) {
super(name, color, price);
}

@Override
public int getPrice() {
return (int) (super.getPrice() * 0.8);
}
}

package org.principle.OCP.version1;

public class Customer {
public static void main(String[] args) {
Water cola = new Cola("可口可乐","黑色",4);
System.out.println(cola.toString());

Water baishi = new Cola("百事可乐","黑色",5);
System.out.println(baishi.toString());

// 打八折
Water discountCola = new DiscountCola("百事可乐","黑色",5);
System.out.println(discountCola.toString());
}
}

使用开闭原则有什么好处呢?

  1. 对测试友好:上线之后的代码都是有意义的,而且经过了严格的测试,如果因为业务的增加,却修改了低层次模块(接口、抽象类),或者原有代码,就意味着原有的测试代码需要重新写,如果这个代码历史悠久,请问,你敢动吗?
  2. 提高复用性:代码功能越是单一,越能提高复用性,单一职责、接口隔离都是这样的。这避免了修改A功能时,对B功能产生影响
  3. 提高维护性:你退出项目组后,接手你工作的同事更喜欢扩展类,而不是修改原类,无论你写的多优秀,让别人读懂都是很痛苦的事情
    面向对象的开发要求:万物皆对象,每个对象都有变化的特点,而如何应对变化就需要策略,这就需要再设计之初考虑所有变化因素,然后留下接口,等待“可能”变为“现实”
    ————————————————

2、里氏代换原则(Liskov Substitution Principle)

里式替换原则有两层定义:
定义一:

所有引用其父类对象方法的地方,都可以透明的替换为其子类对象

定义二:

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1都替换为o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型

第一种定义:

基类是父类、子类通过extends关键字完成继承,从而建立父子类关系,以下面代码为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 车的抽象类,都有行驶的功能
*/
public interface Car {
public void drive();
}

public class Jingji implements Car{
public void drive() {
System.out.println("经济型");
}
}

public class Vip implements Car{
public void drive() {
System.out.println("VIP车型" + ",额外收费10元");
}
}

public class Driver {
private Car car;
public void setCar(Car car){
this.car = car;
}

public void drive(){
this.car.drive();
}
}

public class Test {
public static void main(String[] args) {
Car car = new Vip();
Driver driver = new Driver();
driver.setCar(car);
driver.drive();
}
}

父类:Car

子类:Jingji、Vip

引用基类的地方:Driver

引用基类的地方必须透明的使用其子类对象,在代码中我们可以使用Car的Jingji、Vip两个子类,只需要修改类名,而无需修改其它代码。

第二种定义:

S类型对象o1:Vip

T类型对象o2:Car

程序P:Driver

当对象o1替换为o2:就是Vip变为Car

程序P的行为没有发生变化:都在调用drive方法,不需要修改代码

那么此时VIP是Car的子类

通过拆解发现,上面的代码是使用了里氏替换原则,使用extends、implements实现父子关系

里氏替换原则的优缺点,具有继承的所有优缺点:

优点:

  1. 提高代码重用性
  2. 子类可扩展自我特性

缺点:

  1. 具有侵入性,必须具有父类的所有属性和方法
  2. 降低灵活性,必须拥有父类的属性和方法,让子类多了约束
  3. 增强耦合性,如果修改父类的属性和方法,会对所有的子类产生影响

里氏替换原则为继承定义的规范,包含4层含义:

  1. 子类必须完全实现父类的方法
  2. 子类可以有自己个性
  3. 重载或者实现父类的方法时,输入参数可以放大
  4. 重写或者实现父类的方法时,输出结果可以被缩小

3、依赖倒转原则(Dependence Inversion Principle)

这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

我们先来看一个例子,某天项目经理要求写一个羊了个羊游戏,在线养羊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package org.principle.DIP;

class Sheep{
// 宠物的名字
String name = "HappyShepp";
// 无参构造
public Sheep(){
}
// 构造方法传递名字
public Sheep(String name){
this.name = name;
}
public void eat(String food){
System.out.println(name + "在吃" + food);
}
}
/**
*饲养员
*/
class Breeder{
Sheep sheep;
public void setSheep(Sheep sheep){
this.sheep = sheep;
}
public void feed(String food){
System.out.println("饲养员在喂HappyShepp食物:" + food);
}
}
public class Test {
public static void main(String[] args) {
Sheep sheep = new Sheep();
Breeder breeder = new Breeder();
breeder.setSheep(sheep);
breeder.feed("青草");

}
}
// 运行结果
饲养员在喂HappyShepp食物:青草

但是后来游戏火爆,有客户反应想养哈士奇,这时候怎么办?

  1. 增加一个哈士奇类
  2. 给饲养员类增加setHashiqi方法

有人会说这还不简单,但不要忘了,我们现在写的功能及其简单,而且修改原本代码后,要重新测试Breeder与Sheep,这显然是重复工作,是绝对不允许的。

这就是典型的依赖正置,说白了这就是典型的面向实现编程,你要什么,我实现什么,不考虑事情的本质和扩展性,如果从依赖倒置的角度去写,应该将Sheep抽象化,将Breeder也抽象化,抽象化是依赖倒置的基础条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package org.principle.DIP;

/**
* 动物的抽象类,因为饲养员饲养的不只是羊,后续可能还有很多动物
*/
abstract class Animal{
String name = "Animal";
// 无参构造
public Animal(){
}
// 构造方法传递名字
public Animal(String name){
this.name = name;
}
public abstract void eat(String food);
}

class Hashiqi extends Animal{
public Hashiqi(String name){
super(name);
}

public void eat(String food) {
System.out.println(name + "在吃" + food);
}
}

/**
* 饲养员的抽象类,后面所有的饲养员都是People的子类
* 哪怕后面饲养员变为驯兽员都没问题
*/
abstract class People{
String name = "快乐饲养员";
Animal animal;

public People(String name){
this.name = name;
}

public void setAnimal(Animal animal){
this.animal = animal;
}
}

class HappyBreeder extends People{
public HappyBreeder(String name) {
super(name);
}

public void feed(String food){
System.out.println(name + "在喂" + this.animal.name +"食物:" + food);
}
}

public class Test2 {
public static void main(String[] args) {
Hashiqi hashiqi = new Hashiqi("哈士奇");
HappyBreeder breeder = new HappyBreeder("快乐饲养员");
breeder.setAnimal(hashiqi);
breeder.feed("青草");

}
}

// 运行结果
快乐饲养员在喂哈士奇食物:青草

上面的代码用到了典型的依赖倒置,他将最终实现的功能进行抽象为Animal与People,而且setAnimal、setPeople参数也都是父类,这就保证了该类的稳定性,无论传什么子类,我都可以接受,这是不是就是里氏替换原则?

所以,依赖倒置的定义就是:

高级模块不应该依赖于低级模块。两者都应该依赖抽象。
抽象不应该依赖细节。
细节应该依赖于抽象。

依赖倒置应该遵循下面规则

  • 每个类都尽量有接口或者抽象类,或者两者都具备
  • 变量的表面类型尽量是接口或者抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要重写父类的方法,以及特殊的重载(例如违反里氏替换原则)

4、接口隔离原则(Interface Segregation Principle)

什么是接口?

  1. 实例接口(Object Interface):Person zhangsan = new Person,Person类就是zhangSan的接口,Java中类也是一种接口
  2. 类接口(Class Interface):Java中经常用interface定义接口

什么是接口隔离?

  1. 客户端不需要依赖它不需要的接口
  2. 类的依赖关系应该建立在最小的接口上

总结来说:建立单一的接口,不要建立臃肿庞大的接口,因为实现类中可能只需要其中的某几个方法,不需要的方法,不要实现。

例如对于汽车的描述,汽车有座椅加热、涡轮增压、自动驾驶这些功能,代码可以这样实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.principle.ISP;

interface Car{
public void seatHeating();
public void autoPilot();
public void turbo();
}

/**
* 奔驰比较牛,三个功能都有
*/
class Benz implements Car {

public void seatHeating() {
System.out.println("座椅加热");
}

public void autoPilot() {
System.out.println("自动驾驶");
}

public void turbo() {
System.out.println("涡轮增压");
}
}

/**
* 三菱价格低,没有座椅加热
*/
class SanLing implements Car {

public void seatHeating() {

}

public void autoPilot() {
System.out.println("自动驾驶");
}

public void turbo() {
System.out.println("涡轮增压");
}
}

Car的三个功能奔驰都有,但三菱只有两个,但受限于语法问题,三个方法都得实现,只不过SeatHeating是个空方法,是不是很奇怪?太多余了!不优雅

所以应当拆分Car接口,例如下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.principle.ISP;

interface SeatHeating{
public void seatHeating();
}

interface AutoPilot{
public void autoPilot();
}

interface Turbo{
public void turbo();
}

class BenzX implements SeatHeating,AutoPilot,Turbo{

public void seatHeating() {
System.out.println("座椅加热");
}

public void autoPilot() {
System.out.println("自动驾驶");
}

public void turbo() {
System.out.println("涡轮增压");
}
}

class SanLingX implements SeatHeating,AutoPilot{

public void seatHeating() {
System.out.println("座椅加热");
}

public void autoPilot() {
System.out.println("自动驾驶");
}
}

现在就变的非常优雅,非常清晰,没有多余,但并不是所有接口都只有一个方法,一个接口可以多个方法,这里只是演示使用。
那接口的隔离做的粒度越小越好吗?当然不是。虽然增加了灵活度,但降低了维护性,所以接口的设计要因项目而已,把握好”度“是关键。

5、迪米特法则,又称最少知道原则(Demeter Principle)

也称为最少知识原则(Least Knowledge Principle , LKP),对于初学者来说,这样的定义实在是太晦涩了。

  • 一个类对自己耦合的类或者调用的类知道的越少越好,你内部如何复杂和我没关系
  • 只和直接朋友交流(直接朋友是:耦合关系、组合、聚合、依赖)

朋友的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为朋友类,而方法内部的类不属于朋友类,迪米特法则认为,一个对象或方法,只能调用以下对象:

  • 该对象本身
  • 方法的参数对象

当A类的方法调用了B类,B类调用C类完成业务,如果A类中也出现了C类,那么三者之间的耦合性将大大增加,例如C类出现代码变更,那么可能会影响A类和B类的代码,这无疑是增加了代码风险。

模拟一个超时购物的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package org.principle.LOD;

// 钱包
class Wallet {

// 钱包里装的钱
private Float value;

// 构造器
public Wallet(Float value) {
this.value = value;
}

// 获得钱包里的钱的金额
public Float getMoney(){
return this.value;
}

// 付账时 减钱
public void reduceMoney(Float money){
this.value -= money;
}
}

// 顾客
class Customer {

private Wallet wallet = new Wallet(50f);

public Wallet getWallet() {
return wallet;
}
}

// 收银员
class PaperBoy {
// 收银员收钱
public void charge(Customer customer,Float money){
Wallet wallet = customer.getWallet();
if (wallet.getMoney() >= money){
System.out.println("顾客付账:" + money +"元");
// 减去 应付的钱
wallet.reduceMoney(money);
System.out.println("钱包里还剩:"+wallet.getMoney()+"元");
} else {
System.out.println("钱包里的金额不够......");
}
}
}

public class Test {
public static void main(String[] args) {
PaperBoy paperBoy = new PaperBoy();
Customer customer = new Customer();
paperBoy.charge(customer,20f);
}
}

在PaperBoy中的charge方法中出现了Wallet类,在Customer中也出现了Wallet类,而且观察PaperBoy.charge方法,PaperBoy竟然调用了wallet.reduceMoney(money);也就是拿着客户的钱包付钱,这反常态,你怎么可以动我钱包呢。所以他们之间的关系太乱,需要修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 顾客
class Customer {

private Wallet wallet = new Wallet(50f);

// 顾客自己付钱
public void pay(Float money){
if (wallet.getMoney() >= money){
System.out.println("顾客付账:" + money +"元");
// 减去 应付的钱
wallet.reduceMoney(money);
System.out.println("钱包里还剩:"+wallet.getMoney()+"元");
} else {
System.out.println("钱包里的金额不够......");
}
}
}

// 收银员
class PaperBoy {

// 收银员收钱
public void charge(Customer customer,Float money){
customer.pay(money);
}
}

这样PaperBoy只是提出收费的要求,但具体客户怎么付,是刷卡还是现金,这都与我没有关系。通过分析:

  • 顾客Customer和钱包Wallet是朋友
  • 顾客Customer和收银员PaperBoy是朋友
  • 钱包Wallet和收银员PaperBoy是陌生人

这就符合了迪米特法则:只和朋友交流,不和陌生人说话。该法则的观念就是类之间的解耦,只有解耦了,类的复用率才高,但也会导致与单一职责、依赖倒置、接口隔离相同的问题:产生大量的类,提高了系统的复杂性,所以在使用时也需要因项目而异,反复斟酌。

6、单一职责原则(Single Responsibility Principle,SRP)

定义:应该有且仅有同一类原因引起类的变更

理解:类承担的职责要聚焦某一个或者某一类,例如用户管理功能中不应该有订单管理的功能。

来举个例子

业务:要实现一个APP,打车可以选择经济型、快捷性、方便型等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DiDi {
/**
* 选择乘车类型
* @param type 乘车类型
*/
public void drive(String type){
if (type == "jingji"){
System.out.println("经济型");
}
if (type == "kuaijie"){
System.out.println("快捷型");
}
if (type == "fangbian"){
System.out.println("方便型");
}

}
}

public class Test {
public static void main(String[] args) {
DiDi jingji = new DiDi();
jingji.drive("jingji");

DiDi fangbian = new DiDi();
fangbian.drive("fangbian");
}
}

第一个版本的代码完成了主要的业务功能,但如果产品经理要增加VIP乘车类型来完成对VIP的收费模式,就必须要修改DiDi类,而该类可能已经被十几个类使用,并且经过测试,业务上线运行了。此时修改DiDi类,估计测试部门的同事要疯了。这增加了测试的工作量,也增加了业务的风险

所以要使用单一职责的设计模式,看一下改进之后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 车的抽象类,都有行驶的功能
*/
public interface Car {
public void drive();
}

public class Jingji implements Car{
public void drive() {
System.out.println("经济型");
}
}

public class Vip implements Car{
public void drive() {
System.out.println("VIP车型" + ",额外收费10元");
}
}

public class Test {
public static void main(String[] args) {
Jingji jingji = new Jingji();
jingji.drive();

Vip vip = new Vip();
vip.drive();
}
}

代码升级之后,是不是可以灵活的应对需求变更?无论是增加车型,还是修改车型对应的服务,只需要修改对应的类就可以了,也只需要测试该类的相关类,做到了对代码的最小修改。

这里面的Car接口对应现在的需求其实并不是必须的,但对类的抽象是一种“默认的规则”,它在其它设计模式(策略、装饰)中是必须要进行抽象的。