面向对象设计原则

名称 定义
单一职责原则 (Single Responsibility Principle, SRP) ★★★★☆ 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
开闭原则 (Open-Closed Principle, OCP) ★★★★★ 类的改动是通过增加代码进行的,而不是修改源代码。
里氏代换原则 (Liskov Substitution Principle, LSP ★★★★★ 任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。
依赖倒转原则 (Dependence Inversion Principle, DIP) ★★★★★ 依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。
接口隔离原则 (Interface Segregation Principle, ISP ★★☆☆☆ 不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。
合成复用原则 (Composite Reuse Principle, CRP) ★★★★☆ 如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。
迪米特法则 (Law of Demeter, LoD ★★★☆☆ 一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)

1.单一职责原则

定义:一个类应该只有一个引起它变化的原因。

含义:即每个类只负责完成一个功能或者任务,不包罗万象,这样可以使得这个类各自独立,内部高内聚,彼此之间低耦合,方便拓展和复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 // 定义一个结构体,表示一个汽车
 type Car struct {
     color string
     brand string
 }
 
 // 定义一个方法,表示汽车的行驶功能,这是汽车的单一职责
 func (c *Car) Drive() {
     fmt.Println("The car is driving.")
 }
 
 // 定义一个方法,表示汽车的喇叭功能,这是汽车的单一职责
 func (c *Car) Honk() {
     fmt.Println("The car is honking.")
 }
 
 // 错误的部分:汽车类不应该负责维修汽车的功能,这违反了单一职责原则
 func (c *Car) Repair() {
     fmt.Println("The car is being repaired.")
 }
 

在这个例子中,Repair 方法不仅负责修理汽车,还改变了汽车的颜色。这意味着如果我们想修改汽车修理的方式,可能会影响到汽车的颜色,这违反了单一职责原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 // 定义一个结构体,表示一个汽车
 type Car struct {
     color string
     brand string
 }
 
 // 定义一个方法,表示汽车的行驶功能,这是汽车的单一职责
 func (c *Car) Drive() {
     fmt.Println("The car is driving.")
 }
 
 // 定义一个方法,表示汽车的喇叭功能,这是汽车的单一职责
 func (c *Car) Honk() {
     fmt.Println("The car is honking.")
 }
 
 // 定义一个新的结构体,表示一个汽车修理工
 type Mechanic struct {}
 
 // 定义一个方法,表示汽车修理工的修理汽车的功能,这是汽车修理工的单一职责
 func (m *Mechanic) Repair(c *Car) {
     fmt.Println("The car is being repaired.")
 }

在改正的示例中,我们创建了一个新的 Mechanic 类,它的职责是修理汽车,这样就遵守了单一职责原则。如果我们想修改汽车修理的方式,不会影响到汽车的其他功能。

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
 // 定义一个接口,表示一个银行业务员
 type AbstractBanker interface {
     DoBusi()
 }
 
 // 定义一个结构体,表示一个存款业务员
 type SaveBanker struct{}
 
 // 实现存款业务员的业务方法
 func (sb *SaveBanker) DoBusi() {
     fmt.Println("Performed deposit business.")
 }
 
 // 定义一个结构体,表示一个转账业务员
 type TransferBanker struct{}
 
 // 实现转账业务员的业务方法
 func (tb *TransferBanker) DoBusi() {
     fmt.Println("Performed transfer business.")
 }
 
 // 定义一个结构体,表示一个股票业务员
 type SharesBanker struct{}
 
 // 实现股票业务员的业务方法
 func (sb *SharesBanker) DoBusi() {
     fmt.Println("Performed shares business.")
 }
 
 // 定义一个函数,表示一个银行业务
 func BankBusiness(banker AbstractBanker) {
     // 通过接口向下调用(多态的现象)
     banker.DoBusi()
 }
 
 func main() {
     // 存款的业务
     BankBusiness(&SaveBanker{})
     // 转账的业务
     BankBusiness(&TransferBanker{})
     // 股票的业务
     BankBusiness(&SharesBanker{})
 }

**在这个例子中,如果我们想要添加一个新的银行业务,比如贷款业务,我们只需要定义一个新的结构体 **LoanBanker,实现 AbstractBanker 接口的 DoBusi 方法,然后在 main 函数中调用 BankBusiness 函数即可,而不需要修改 BankBusiness 函数。这就是开闭原则的体现。

3.里氏替换原则

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

含义:即所有使用基类的地方都能够快乐(无痛)地接受子类的实例作为基类对象,而且保证使用后不会对原有代码造成任何问题或改变。

注意点:

1
2
3
4
5
6
7
8
 1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
 
 2.子类中可以增加自己特有的方法
 
 3.当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
 
 4.当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等
 

4.依赖倒转原则

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

含义:即要尽量减少类之间的依赖关系,使得系统更加稳定,同时采用高层模块调低层模块的方式进行设计,通过抽象进行通信,达到解耦的目的。

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
 // 定义一个接口,表示一个汽车
 type Car interface {
     Drive()
 }
 
 // 定义一个结构体,表示一个奔驰汽车
 type BenZ struct{}
 
 // 实现奔驰汽车的驾驶方法
 func (b *BenZ) Drive() {
     fmt.Println("Driving a BenZ.")
 }
 
 // 定义一个结构体,表示一个宝马汽车
 type Bmw struct{}
 
 // 实现宝马汽车的驾驶方法
 func (b *Bmw) Drive() {
     fmt.Println("Driving a Bmw.")
 }
 
 // 定义一个接口,表示一个驾驶员
 type Driver interface {
     Drive(car Car)
 }
 
 // 定义一个结构体,表示一个驾驶员张三
 type Zhangsan struct{}
 
 // 实现张三的驾驶方法
 func (z *Zhangsan) Drive(car Car) {
     fmt.Println("Zhangsan is driving.")
     car.Drive()
 }
 
 // 定义一个结构体,表示一个驾驶员李四
 type Lisi struct{}
 
 // 实现李四的驾驶方法
 func (l *Lisi) Drive(car Car) {
     fmt.Println("Lisi is driving.")
     car.Drive()
 }
 
 func main() {
     // 张三驾驶奔驰
     var benz Car
     benz = new(BenZ)
     var zhang3 Driver
     zhang3 = new(Zhangsan)
     zhang3.Drive(benz)
 
     // 李四驾驶宝马
     var bmw Car
     bmw = new(Bmw)
     var li4 Driver
     li4 = new(Lisi)
     li4.Drive(bmw)
 }

在这个例子中,Driver 接口依赖于 Car 接口,而不是依赖于具体的 BenZBmw 结构体。这就是依赖倒转原则的体现。

5.接口隔离原则

定义:客户端不应该强制依赖它不需要的接口。

含义:即尽量将接口拆分成更小更具体的接口,让客户端只需关心自己需要的接口,避免出现无用接口污染的情况。同时,还要注意接口的灵活性和可扩展性,方便后期拓展与维护。

比如动物可以fly、run、eat,但是狗不能fly,bird不能run。

这时候不应该实现一个Animal总接口,应该分为FlyAnimalRunAnimal

6.合成复用原则

合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)或聚合(contanis-a),而不是继承关系达到软件复用的目的。 **可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。 **

继承又叫白箱复用,相当于把所有的实现细节暴露给子类 组合/聚合也称为黑箱复用,对类以外的对象是无法获取到实现细节的

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
 package main
 
 import "fmt"
 
 type Cat struct {}
 
 func (c *Cat) Eat() {
  fmt.Println("小猫吃饭")
 }
 
 //给小猫添加一个 可以睡觉的方法 (使用继承来实现)
 type CatB struct {
  Cat
 }
 
 func (cb *CatB) Sleep() {
  fmt.Println("小猫睡觉")
 }
 
 //给小猫添加一个 可以睡觉的方法 (使用组合的方式)
 type CatC struct {
  C *Cat
 }
 
 func (cc *CatC) Sleep() {
  fmt.Println("小猫睡觉")
 }
 
 
 func main() {
  //通过继承增加的功能,可以正常使用
  cb := new(CatB)
  cb.Eat()
  cb.Sleep()
 
  //通过组合增加的功能,可以正常使用
  cc := new(CatC)
  cc.C = new(Cat)
  cc.C.Eat()
  cc.Sleep()
 }

7.迪米特法则

迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合。主要强调只和朋友交流,不和陌生人说话。

朋友的定义:

出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类

陌生人的定义:

出现在方法体内部的类不属于朋友类

举例:校长需要知道学生的信息,就需要由老师进行统计报告

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
 // 定义一个结构体,表示一个学生
 type Student struct {
     name string
     score int
 }
 
 // 定义一个结构体,表示一个老师
 type Teacher struct {
     students []Student
 }
 
 // 定义一个方法,表示老师统计学生的分数
 func (t *Teacher) ReportScores() int {
     totalScore := 0
     for _, student := range t.students {
         totalScore += student.score
    }
     return totalScore
 }
 
 // 定义一个结构体,表示一个校长
 type Principal struct {}
 
 // 定义一个方法,表示校长获取学生的分数报告
 func (p *Principal) GetReport(teacher *Teacher) {
     totalScore := teacher.ReportScores()
     fmt.Printf("The total score of the students is %d.\n", totalScore)
 }
 
 func main() {
     students := []Student{
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 92},
    }
     teacher := Teacher{students}
     principal := Principal{}
     principal.GetReport(&teacher)
 }

在这个例子中,校长需要知道学生的分数,但是他并不直接与学生交互,而是通过老师来获取。这就是迪米特法则的体现。