概述(Summary)

1

定义

  设计模式(Design Pattern)在百度百科中的定义:是一套被反复利用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序重用性。有的小伙伴听了(比如我)可能就会认为,不对呀,我感觉设计模式没啥用,代码反而不容易被人理解。如果产生这种想法,就说明我们还没有经历过大项目的摧残、或者设计模式还没有学到精髓。这也就是我写这一类博客的原因,我从网络上找到了韩顺平老师的一些教学资源,其中的代码和设计思想部分是根据韩老师的内容吸收整合而来,希望小伙伴们能够跟着我一起学习,一起进步。

起源

1987年Kent Beck和Ward Cunningham利用克里斯托佛·亚历山大在建筑设计领域里的思想开发了设计模式并把此思想应用在Smalltalk中的图形用户接口的生成中。

一年后Erich Gamma在他的苏黎世大学博士毕业论文中开始尝试把这种思想改写为适用于软件开发。与此同时James Coplien 在1989年至1991 年也在利用相同的思想致力于C++的开发。而后于1991年发表了他的著作Advanced C++ Idioms。就在这一年Erich Gamma 得到了博士学位,然后去了美国。

1994年,在美国与Richard Helm, Ralph Johnson ,John Vlissides合作出版了Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素)一书,在此书中共收录了23个设计模式。因为此书的风靡,这四位作者在软件开发领域里也被称为Gang of Four(四人帮,简称GoF)。

目的

提高代码的重用性

重用性:相同功能的代码,可以重复使用,不需要再次编写。

提高代码的可读性

可读性:编程的规范性,便于其他程序员阅读和理解。很多小伙伴们有这种感觉,看到别人写的代码,觉得像屎山一样,逻辑不通顺,晦涩难懂,这可能就是可读性不好。

提高代码的可扩展性

可扩展性:很好理解,当需要扩展新的功能时,非常方便。

提高代码的可靠性

可靠性:代码可靠,当某个功能变化时,对其他的功能没有影响。

七大原则

单一职责原则(SRP: Single Responsibility Principle)

单一职责:一个类应该只负责一项职责。如果某类负责多个职责,那么当一个职责变化时,可能会导致其他职责产生错误,应该将该类分解为多个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Animal {
String name;

Animal(String name) {
this.name = name;
}

public void catchMouse() {
System.out.println("我是" + name + ",我的本领是抓老鼠");
}

public void watchHouse() {
System.out.println("我是" + name + ",我的本领是看家");
}
}

例如一个动物类,既用来创建狗的实例,也用来创建猫的实例,虽然没有太大问题,但是添加了狗类特有的看家方法,如果创建猫的对象,调用看家方法就会产生问题。正确的做法是为猫和狗分别构建一个类来创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Cat {
String name;

Cat(String name) {
this.name = name;
}

public void catchMouse() {
System.out.println("我是" + name + ",我的本领是抓老鼠");
}
}

public class Dog {
String name;

Dog(String name) {
this.name = name;
}

public void watchHouse() {
System.out.println("我是" + name + ",我的本领是看家");
}
}

接口隔离原则(ISP Interface Segregation 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
50
51
52
53
54
55
56
57
58
59
60
61
public interface Animal {
public void eatFood();
public void catchMouse();
public void watchHouse();
}

public class Cat implements Animal{

@Override
public void eatFood() {
System.out.println("我喜欢吃鱼");
}

@Override
public void catchMouse() {
System.out.println("我能抓老鼠");
}

@Override
public void watchHouse() {
System.out.println("我能看家");
}
}

public class Dog implements Animal{

@Override
public void eatFood() {
System.out.println("我喜欢吃肉");
}

@Override
public void catchMouse() {
System.out.println("我能抓老鼠");
}

@Override
public void watchHouse() {
System.out.println("我能看家");
}
}

public class CatFunction {
public void eatFood(Animal animal) {
animal.eatFood();
}

public void catchMouse(Animal animal) {
animal.catchMouse();
}
}

public class DogFunction {
public void eatFood(Animal animal) {
animal.eatFood();
}

public void watchHouse(Animal animal) {
animal.watchHouse();
}
}

类CatFunction通过接口Animal依赖类Cat,类DogFunction通过接口Animal依赖类Dog,但是类CatFunction不需要调用Animal类的watchHouse方法,DogFunction也不需要调用Animal类的catchMouse方法,但是Cat类和Dog类却实现了这两个本不应该拥有方法。

应该将Animal类拆分为几个独立的接口,CatFunction和DogFunction分别与实现对应接口的类建立依赖关系。

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
public interface Animal {
public void eatFood();
}

public interface CatInterface {
public void catchMouse();
}

public interface DogInterface {
public void watchHouse();
}

public class Cat implements Animal, CatInterface{

@Override
public void eatFood() {
System.out.println("我喜欢吃鱼");
}

@Override
public void catchMouse() {
System.out.println("我能抓老鼠");
}
}

public class Dog implements Animal, DogInterface{

@Override
public void eatFood() {
System.out.println("我喜欢吃肉");
}

@Override
public void watchHouse() {
System.out.println("我能看家");
}
}

public class CatFunction {
public void eatFood(Animal animal) {
animal.eatFood();
}

public void catchMouse(CatInterface catInterface) {
catInterface.catchMouse();
}
}

public class DogFunction {
public void eatFood(Animal animal) {
animal.eatFood();
}

public void watchHouse(DogInterface dogInterface) {
dogInterface.watchHouse();
}
}

在测试时,可以发现虽然CatFunction中eatFood和catchMouse方法的参数类型不一样,但是因为Cat实现了这两个接口,因此可以传入Cat类,会自动发生多态效果,DogFunction同理。

依赖倒置原则(DIP Dependence Inversion Principle)

依赖倒置:高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。

原因:细节是变化的,抽象是稳定的,以抽象为基础的架构比以细节为基础的架构要更加稳定。在编程语言中,细节指的是具体的实现类,抽象指的是接口或者抽象类。

1
2
3
4
5
6
7
8
9
10
11
public class Cat{
public void eatFood() {
System.out.println("我喜欢吃鱼");
}
}

public class EatTest {
public void eatFood(Cat cat) {
cat.eatFood();
}
}

在上面这个例子中,运行起来是没用任何问题的,但是高层模块EatTest的eatFood方法依赖于低层模块Cat,导致架构的扩展性不强,如果新增一个Dog类,则需要新增一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Cat{
public void eatFood() {
System.out.println("我喜欢吃鱼");
}
}

public class Dog{
public void eatFood() {
System.out.println("我喜欢吃肉");
}
}

public class EatTest {
public void eatFood(Cat cat) {
cat.eatFood();
}

public void dogEatFood(Dog dog) {
dog.eatFood();
}
}

此时我们需要对这个代码架构进行调整,让高层模块和低层模块都依赖于抽象,新建一个名为Animal的接口,让低层模块Cat和Dog依赖以Animal,让高层模块EatTest的eatFood方法也依赖于Animal。此时再新增其他类X时,只要X类依赖于Animal,则不需要修改高层模块的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Animal{
void eatFood();
}

public class Cat implements Animal{
@Override
public void eatFood() {
System.out.println("我喜欢吃鱼");
}
}

public class Dog implements Animal{
@Override
public void eatFood() {
System.out.println("我喜欢吃肉");
}
}

public class EatTest {
public void eatFood(Animal animal) {
animal.eatFood();
}
}

里氏替换原则(LSP Liskov Substitution Principle)

里氏替换:所有引用父类的地方必须能够透明地使用其子类对象,在子类中尽量不要重写父类的方法。在适当的情况下,可以通过聚合、组合、依赖解决问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Dog extends Animal{
public void say() {
System.out.println("我是一只狗");
}

public void legs() {
System.out.println("我有四条腿");
}
}

public class Cat extends Dog{
public void say() {
System.out.println("我是一只猫");
}
}

在正常的代码编写过程中,如果父类实现了某个方法,那么子类都应该遵守父类指定的规则,否则说明子类不具有父类的某些特征。在上例中,Cat继承自Dog,目的是提高代码的重用性,不需要重写legs方法,但是Cat类和Dog类在某些方法上是不同的,因此Cat类重写了Dog类的say方法。这时在发生多态现象时,无法预期代码的运行结果。

解决上面的问题也很简单,将子类和父类都重写继承一个更基础的类,并且将原有的子父类继承关系删去,采用聚合、组合、依赖等方式替代。

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
public abstract class Animal{
public abstract void say();
}

public class Dog extends Animal{
@Override
public void say() {
System.out.println("我是一只狗");
}

public void legs() {
System.out.println("我有四条腿");
}
}

public class Cat extends Animal{
Dog dog = new Dog();

public void say() {
System.out.println("我是一只猫");
}

public void legs() {
dog.legs();
}
}

在修改后的代码中,我们让Cat类和Dog类的继承关系删除,并且都继承自一个更基础的Animal类,因为Cat和Dog之间没用直接关系,所有不需要考虑多态之间发生的问题。但是此时Cat类想使用Dog类的legs方法,可以创建一个Dog类的成员变量,通过成员变量调用legs方法。

开闭原则(OCP Open Closed Principle)

开闭原则:一个代码架构,应该对扩展开放,对修改关闭。如何理解扩展开放和修改关闭呢?扩展是指功能扩展时对开发者开放,对使用者关闭。因为扩展功能必定导致要增加或者修改某些类或者接口,此时要对开发者是感知和开放的,修改关闭是指对使用方,因为类和接口发生了修改,如果处理不当,则会导致使用该类的用户发生报错,可能这个错误是成千上万的,因此这个修改对于使用者来说应该是不感知、关闭的。

因此当软件需要变化时,尽量通过开发者的扩展来实现变化,而不是通过修改使用者已有的代码来适应变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Animal{
int classID = 0;
}

public class Cat extends Animal{
public Cat() {
classID = 1;
}
}

public class EatTest {
public void eatFood(Animal animal) {
switch (animal.classID) {
case 1:
System.out.println("我喜欢吃鱼");
break;
default:
System.out.println("我什么都不吃");
break;
}
}
}

在本代码中,也是完全可以运行的,但是如果开发者新增一个Dog类,则使用者就需要重新修改switch case语句,新增一个case条件判断。

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
public abstract class Animal{
int classID = 0;
}

public class Cat extends Animal{
public Cat() {
classID = 1;
}
}

public class Dog extends Animal{
public Dog() {
classID = 2;
}
}

public class EatTest {
public void eatFood(Animal animal) {
switch (animal.classID) {
case 1:
System.out.println("我喜欢吃鱼");
break;
case 2:
System.out.println("我喜欢吃肉");
break;
default:
System.out.println("我什么都不吃");
break;
}
}
}

修改方式也很简单,只需要在抽象类Animal中,定义一个抽象方法eatFood,让所有的子类都去重写eatFood方法即可。这样无论新增多少类,只需要开发者增加类,并重写eatFood方法即可,对于使用者而言是不感知的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class Animal{
public abstract void eatFood();
}

public class Cat extends Animal{
@Override
public void eatFood() {
System.out.println("我喜欢吃鱼");
}
}

public class Dog extends Animal{
@Override
public void eatFood() {
System.out.println("我喜欢吃肉");
}
}

public class EatTest {
public void eatFood(Animal animal) {
animal.eatFood();
}
}

迪米特法则(DP Demeter Principle)

迪米特法则:又称为最少知道原则,一个对象应该对自己依赖的类保持最少的了解。也就是说,对于被依赖的类,要尽量将逻辑封装在类的内部,除了public方法以外,不对外泄露任何信息。

这里再引入一个直接朋友的概念:几乎每个类都会与其他类发生耦合关系,只要两个类有耦合关系,我们就称这两个类是朋友关系。如果满足下列条件,称两个类是直接朋友。

  • 一个类是另一个类的成员变量
  • 一个类是另一个类的方法参数
  • 一个类是另一个类的方法返回值

也就是说不是直接朋友尽量不要直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Cat {
String name;

Cat(String name) {
this.name = name;
}
}

public class Test {
public void say() {
Cat cat = new Cat("Hello kitty");
System.out.println("我是" + cat.name);
}
}

上面的代码违反了迪米特法则,Cat类不是Test的直接朋友,但是却在Test类中使用了Cat类的对象。

修改方式非常简单,有三种方法都可以将Cat变成Test的直接朋友。

第一个方法是将Cat从局部变量变成成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Cat {
String name;

Cat(String name) {
this.name = name;
}
}

public class Test {
Cat cat = new Cat("Hello kitty");

public void say() {
System.out.println("我是" + cat.name);
}
}

第二个方法是将Cat作为方法参数

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat {
String name;

Cat(String name) {
this.name = name;
}
}

public class Test {
public void say(Cat cat) {
System.out.println("我是" + cat.name);
}
}

第三个方法是将Cat作为方法返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat {
String name;

Cat(String name) {
this.name = name;
}
}

public class Test {
public Cat getCat() {
return new Cat("Hello kitty");
}
}

合成复用原则(Composite Reuse Principle)

合成复用:尽量使用依赖、聚合、组合的方式代替继承的方式。

继承的耦合性太强,如果一个类继承自一个类,仿佛两个类具有很明显的层级关系,如果只是希望在某个类中使用其他类的方法,则尽量使用依赖、聚合、组合的方式代替。

1
2
3
4
5
6
7
8
9
public class Cat {
public void say() {
System.out.println("我是一只猫");
}
}

public class Test extends Cat{

}

在上例中,Test的对象test可以使用say的方法,但是如果Cat增加了某些方法,如eatFood,但是test也会具有该方法,这是不合理的。

我们可以采用依赖、聚合、组合的方式进行代替。下面先对这三种方式进行介绍。

依赖是指将Cat类的对象作为参数传递进去,这样test可以通过say方法调用Cat的方法,并且降低了类的耦合性。

1
2
3
4
5
6
7
8
9
10
11
public class Cat {
public void say() {
System.out.println("我是一只猫");
}
}

public class Test{
public void say(Cat cat) {
cat.say();
}
}

聚合是指创建一个Cat类的成员变量,并通过setter或者构造函数传进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Cat {
public void say() {
System.out.println("我是一只猫");
}
}

public class Test{
Cat cat;

public Test(Cat cat) {
this.cat = cat;
}

public void setCat(Cat cat) {
this.cat = cat;
}

public void say() {
cat.say();
}
}

组合是指创建一个Cat类的成员变量,并在创建时直接进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat {
public void say() {
System.out.println("我是一只猫");
}
}

public class Test{
Cat cat = new Cat();

public void say() {
cat.say();
}
}

总结

  学习了上面七种原则,心里有什么感觉呢?是不是觉得也没啥牛B的?但是自己写可能又写不出来。这就是设计模式的魅力,可能在小demo上面,感觉不到设计模式带来的价值,但是当你去做一些大型的开发时,才会感觉到设计模式的厉害之处。我们后面会继续介绍23种设计模式,小伙伴们跟我一起学习吧!

-------------本文结束感谢您的阅读-------------
0%