0%

聊聊面向对象和其设计原则

重温面向对象

  • 封装:隐藏内部实现
  • 继承:复用现有代码
  • 多态:改写对象行为

研究何使用面向对象,我们究竟想做什么?

面向对象的目的

现实问题(复杂度) -> 类 -> 实例 (?)

1
2
3

ObjectmyObj = new Object();

封装

假设你是军队指挥官,你手下统帅了数十种上万人的军队。当你命令部下向地方发起进攻的时候

你是会

A 营的小陆向前走三步。向你的 11 点钟方向射击

B 连的小宋向左转 35 度,前进 5 步,3 分钟瞄准时间,炮口倾角 15 度,开火 3 分钟。

还是

A 营向甲点缓慢推进

B 连火力掩护

还是

给老子啃下***高地(乐

显然,身为一军之长的你(作为最高层调用者)对军队中每个单位进行原子层级的微操是不合适的,军中的陆军单位,炮军单位,侦察单位所构成的网络的复杂度显然会超出你的想象,甚至于对于第二段对话中的战术决策,你也能够交给中层决策者去处理,你只需要做战略层面的决定即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

intmain()

{

move("小陆",3);

fire("小陆",-30);

rotate("小宋"-35);

move("小宋5");

artillery_fire(3,15,3);

......

}

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

publicclassArmy

{

......

publicenumAdvanceSpeed

{

Slow, Quick, Normal

}

publicenumArmyType

{

Infantry, Artillery

}

publicvoidAdvance(AdvanceSpeed speed, City targetPos)

{

......

}

publicvoidShield(Army target)

{

......

}

publicArmy(...)

{

......

}

}


intMain()

{

ArmyA = new Army(...);

ArmyB = new Army(...);

A.Advance(AdvanceSpeed.Normal, city_jia);

B.Shield(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

publicclassCollectiveArmy

{

List<Army> Armys = new List();

......

publicvoidAttack(Area targetPos)

{

......

}

publicCollectiveArmy(...)

{

......

}

}

intMain()

{

CollectiveArmycollectiveArmy = new CollectiveArmy(...);

collectiveArmy.Attack(area)

}

事实上,现实业务的复杂度并没有减少,他只是被一个个对象包裹了起来,但是,新的代码显然易读,易拓展,易维护

代码是写给人看的

面向过程的“时序性”

面向对象作为一种编程范式,在面对超复杂问题时能顺应人脑的思考方式将逻辑层层包装成高级指令来帮助人寄存逻辑减少重复作业。事实上,每一道指令本质上仍然由程序员发出,只是这些低级或者说底层指令被包裹成了高级指令,当我发出 A 军前进的时候,A 军中的每个士兵都接到了我前进的命令,这些命令由中层军官转达,而身为高层调用者的我只需要说一句话就可以了。这道前进的命令显然是在长期训练中我和士兵们约定好的(我实现的方法),比如我说前进 1 的时候,士兵们快速前进,我说前进 2 的时候士兵缓慢前进。

我命令每个士兵的逻辑被寄存在了我的高级指令前进 num 中 Advance(Num value), 所以我真正战斗的时候不需要记住小陆的脚怎么迈,小宋的枪怎么端,这都是我提前约定好的,在战斗(调用)时我只需要发布一道高级指令前进 num 就行。又好记,又节约时间

再者,当我需要调整前进 1 指令下小陆的作战姿态时,我也只需要在训练时做更正就可以(修改函数实现),而在战斗时(调用时),我仍然只需要喊:Advance(1)

哪里能用到面向对象

面向对象真好使啊,怪不得用了这么多年,怪不得学校还要教,那我们做啥都用面向对象,这样肯定没错——吗?

大部分同学在上程序设计周的时候老师都会布置那么道非常经典(土掉渣)的题目:学生管理系统

很多同学框框整几个类然后开始写,如果从应试的角度来看,没错,你不用“面向对象”老师不给高分啊,可是同学们现在可以转过头来想象,你写的/你将要写的那些个学生管理系统有复杂到需要上面向对象吗?请大家从应试的角度转到应付事儿(合适)的角度上来().你在规定时间内去完成一个没什么人用的逻辑相当简单的控制台小系统,完全没有必要上面向对象这种大杀器。如果不理解我们可以采用"极值法"(听起来像高中数学解题方法),你只去打印一个"hello world", 肯定不会先 class person, 然后 person 里面声明并实现一个 talk, 然后你再 new 一个自己出来,最后 me.Talk("hello world"); 闲的嘛这不是

希望大家通过这个小栗子可以明白面向对象乃至任意一种编程范式都不是绝对的在任何时候都能百分百发挥效能的,面向对象适合复杂系统,面向过程基于时序关系,当然在非常多的情况下我们会结合起来用。

他在这里合适才用他

一言以蔽之

No Silver Bullet 『没有银弹』

– · – · – 中场休息 5 分钟 – · – · –

面向对象的设计原则

单一责任原则

定义

一个类只允许有一个职责,即只有一个导致该类变更的原因。

也就是说一个类只负责一项业务,只做一件事情,只有一个职责

不仅仅是类,函数也是一个道理

不好的设计

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

publicclassCardDisplay : MonoBehaviour

{

publicCard card;

publicTextMeshProUGUI nameText;

publicTextMeshProUGUI descriptionText;

publicTextMeshProUGUI attackText;

publicTextMeshProUGUI healthText;

publicImage artworkImage;


voidStart()

{

Draw();

}


publicvoidDraw()

{

nameText.text = card.name;

descriptionText.text = card.description;

attackText.text = card.attack.ToString();

healthText.text = card.health.ToString();

artworkImage.sprite = card.artwork;

}

publicvoidChangeCardHP(int hp)

{

card.health = hp;

healthText.text = card.health.ToString();

}

}

更好的设计

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
67
68
69
70
71
72
73

publicclassCardDisplay : MonoBehaviour

{

publicTextMeshProUGUI nameText;

publicTextMeshProUGUI descriptionText;

publicTextMeshProUGUI attackText;

publicTextMeshProUGUI healthText;

publicImage artworkImage;


publicvoidDraw(Card card)

{

nameText.text = card.name;

descriptionText.text = card.description;

attackText.text = card.attack.ToString();

healthText.text = card.health.ToString();

artworkImage.sprite = card.artwork;

}

publicvoidChangeHealthText(int amount)

{

healthText.text = amount.ToString();

}

}


publicclassCardController : MonoBehaviour

{

publicCard card;

publicCardDisplay cardDisplay;


publicvoidStart()

{

cardDisplay.Draw(card);

}


publicvoidChangeHealth(int amount)

{

card.health+=amount;

cardDisplay.ChangeHealthText(card.health);

}

}

如果需要,还可以抽离出卡牌的状态管理类并注册到卡牌控制器中。

总结

Do one thing, and do it well.

开闭原则

定义

软件实体应该对扩展开放,对修改关闭。

开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。

不好的设计

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

publicclassHealthPotion

{

publicvoidUse()

{

Debug.Log("生命值恢复了100点!");

}

}


publicclassManaPotion

{

publicvoidUse()

{

Debug.Log("魔法值恢复了100点!");

}

}


publicclassPlayer

{

publicvoidUseHealthPotion()

{

HealthPotionhealthPotion = new HealthPotion();

healthPotion.Use();

}


publicvoidUseManaPotion()

{

ManaPotionmanaPotion = new ManaPotion();

manaPotion.Use();

}

}

更好的设计

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

publicinterfaceIPotion

{

voidUse();

}


publicclassHealthPotion : IPotion

{

publicvoidUse()

{

Debug.Log("生命值恢复了100点!");

}

}


publicclassManaPotion : IPotion

{

publicvoidUse()

{

Debug.Log("魔法值恢复了100点!");

}

}


publicclassPlayer

{

privateIPotion potion;


publicvoidUsePotion()

{

potion.Use();

}

}

总结

使用抽象(接口和抽象类)、继承和组合等实现

面对新需求,可以通过扩展来实现变化。

在拓展新功能的时候,尽量不要修改原有代码。

接口隔离原则

定义

不应强迫客户端依赖它不使用的方法,类间的依赖关系应该建立在最小的接口上。

上例子

不好的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

publicinterfaceIEquipment

{

voidAttack();

voidDefend();

voidRepair();

voidUpgrade();

}

更好的设计

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

publicinterfaceIAttackable

{

voidAttack();

}


publicinterfaceIDefendable

{

voidDefend();

}


publicinterfaceIRepairable

{

voidRepair();

}


publicinterfaceIUpgradeable

{

voidUpgrade();

}

总结

接口隔离原则强调客户端不应该依赖它不需要的接口

里氏替换原则(继承)

定义

子类的实例必须能替换掉所有父类的实例。

预防继承导致的侵入性问题。父类更改任意方法都必须考虑在修改这个方法后其子类的功能会不会发生异常。

不好的设计

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

publicclassRectangle

{

publicvirtualint Width { get; set; }

publicvirtualint Height { get; set; }


publicvirtualintArea() => Width*Height;

}


publicclassSquare : Rectangle

{

publicoverrideint Width { get; set; }


publicoverrideint Height

{

get => Width;

set => Width = value;

}


publicoverrideintArea() => Width*Width;


publicclassProgram

{


staticvoidMain(string[] args)

{

Squarerectangle = new Square();

rectangle.Width = 5;

rectangle.Height = 10;


intarea = rectangle.Area();

Console.WriteLine($"Area: {area}");

}

}

}

更好的设计

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

publicabstractclassQuadrangle

{

publicabstractint Width { get; set; }

publicabstractint Height { get; set; }


publicabstractintArea();

}


publicclassRectangle : Quadrangle

{

publicoverrideint Width { get; set; }

publicoverrideint Height { get; set; }


publicoverrideintArea() => Width*Height;

}


publicclassSquare : Quadrangle

{

publicoverrideint Width { get; set; }


publicoverrideint Height

{

get => Width;

set => Width = value;

}


publicoverrideintArea() => Width*Width;


publicclassProgram

{


staticvoidMain(string[] args)

{

Quadranglerectangle = new Rectangle();

rectangle.Width = 5;

rectangle.Height = 10;


intarea = rectangle.Area();

Console.WriteLine($"Area: {area}");


Quadranglesquare = new Square();

square.Width = 5;

square.Height = 10;


area = square.Area();

Console.WriteLine($"Area: {area}");

}

}

}

总结

检验是否真正符合继承关系,以避免继承的滥用。

从 behavior 出发的设计

正方形是一种特殊的长方形,但是从行为的角度来看,一个 Square 不是一个 Rectangle.

Rectangle 的 Width 和 Height 是无依赖关系,但是 Square 的 Width 和 Height 是相互依赖的

如何解决?

abstract

子类无需重写父类方法而是实现其抽象方法

最少知识原则

定义

每个单元对其他单元只拥有有限的知识,只了解与当前单元紧密联系的单元;

对于对象 O 中的一个方法 F, F 方法仅能访问如下这些类型的对象:

  1. O 对象自身;
  2. F 方法的参数对象;
  3. 任何在 F 方法内创建的对象;
  4. O 对象直接依赖的对象;

不好的设计

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

publicclassPlayer

{

publicvoidJoinTeam(Guild guild)

{

Teamteam = guild.FindTeam();

team.AddPlayer(this);

}

}


publicclassGuild

{

publicTeam team = new Team();


publicTeamFindTeam()

{

returnteam;

}

}


publicclassTeam

{

publicstring name;

List<Player> members = new List<Player>();


publicvoidAddPlayer(Player player)

{

members.Add(player);

}

}

更好的设计

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

publicclassPlayer

{

publicvoidJoinTeam(Guild guild)

{

guild.AddPlayer(this);

}

}


publicclassGuild

{

publicTeam team = new Team();


publicTeamFindTeam()

{

returnteam;

}

publicvoidAddPlayer(Player player)

{

team.AddPlayer(player);

}

}


publicclassTeam

{

publicstring name;

List<Player> members = new List<Player>();


publicvoidAddPlayer(Player player)

{

members.Add(player);

}

}

依赖倒置原则

定义

高层模块不应该依赖于低层模块,二者都应该依赖于抽象;

不好的设计

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

publicclassPlayer

{

publicvoidSpellFireBall()

{

Console.WriteLine("Fireball!");

}

publicvoidSpellFrostBall()

{

Console.WriteLine("Frostball!");

}

}


publicvoidGame

{

publicstaticvoidMain()

{

Playerplayer = new Player();

player.SpellFireBall();

player.SpellFrostBall();

}

}

更好的设计

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
67
68
69
70
71
72
73

publicinterfaceISpell

{

voidCast();

}


publicclassFireBall : ISpell

{

publicvoidCast()

{

Console.WriteLine("Fireball!");

}

}


publicclassFrostBall : ISpell

{

publicvoidCast()

{

Console.WriteLine("Frostball!");

}

}


publicclassPlayer

{

publicvoidSpell(ISpell spell)

{

spell.Cast();

}

}


publicvoidGame

{

publicstaticvoidMain()

{

Playerplayer = new Player();

player.Spell(new FireBall());

player.Spell(new FrostBall());

}

}

雪中送炭和锦上添花——设计模式

参考资料

1.设计模式 - 可复用面向对象软件的基础

  1. 游戏设计模式
  2. 设计模式与完美游戏开发

欢迎关注我的其它发布渠道