diff --git a/README.md b/README.md index 1c06291..234b040 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ * [WSL Windows Linux 子系统](./windows/README.md)(Windows 用户 强烈建议) * [Brew Unix 包管理工具](./unix/Brew.md)(可选) -* [Git 版本控制工具](./common/git.md) +* [Git 版本控制工具](./git/git.md) * [Goland 最好的Golang IDE](./jetBrains/commonware.md) * [Shell 使用基本逻辑](./unix/shell.md) @@ -18,7 +18,7 @@ ### [git 与版本控制](./git) -### [go 语言相关](./golang) +### [Go 语言相关](./golang) ### [jetBrains ](./jetBrains) @@ -28,4 +28,6 @@ ### [公共 杂项](./common) +### [面向对象](./common/oop) + diff --git a/common/README.md b/common/README.md index 3e98f83..93fa0b8 100644 --- a/common/README.md +++ b/common/README.md @@ -8,4 +8,4 @@ ### [WEB 开发中的前后端分离](./networkKnowledge.md) -### [面向对象开发思想](./oop.md) +### [面向对象开发思想](./oop) diff --git a/common/oop/README.md b/common/oop/README.md new file mode 100644 index 0000000..41b8110 --- /dev/null +++ b/common/oop/README.md @@ -0,0 +1,13 @@ +## 面向对象思想 + +可以说,每个写业务的程序员都需要掌握面向对象思想。这里给出一些面向对象思想的指引。 + +## [1. 面向对象思想入门](./oop_started.md) +## [2. 面向对象标准实现](./oop_standard.md) +## [3. Go 如何实现面向对象](./oop_in_go.md) +## [4. 常用面向对象设计模式](./oop_design_pattern.md) + +oop 并不是灵丹妙药(silver bullet),在合适的时候使用它就好。 + +我也曾在某个地方看到,说,封装、继承、多态很早就被提出了,并不是面向对象独有的。 +函数式编程等思想,都可以一试,适合的就是最好的。不要过度迷恋一种思想。 diff --git a/common/oop/oop_design_pattern.md b/common/oop/oop_design_pattern.md new file mode 100644 index 0000000..228fb65 --- /dev/null +++ b/common/oop/oop_design_pattern.md @@ -0,0 +1 @@ +# 面向对象设计模式 \ No newline at end of file diff --git a/common/oop/oop_in_go.md b/common/oop/oop_in_go.md new file mode 100644 index 0000000..b883be7 --- /dev/null +++ b/common/oop/oop_in_go.md @@ -0,0 +1,170 @@ +# 谈谈Go与面向对象 + +本文面向已经会 Go 的基础语法并基本掌握一门面向对象语言的读者。 + +先抛个问题:Go 是不是面向对象语言? + +官方的回答是:「Yes and no」。 + +Go 语言可以做到绝大多数面向对象语言的特性,但它不是一门「标准」的面向对象语言,它没有「type hierarchy」。 + +一开始,我觉得它以自己的奇怪甚至近乎「妖魔」的方式与面向对象打了个擦边球; +后来,我反而觉得完美面向对象就应该是这样灵活的,现在的所谓的「标准面向对象」,反而是一种不完美的实现。 + +下面来谈谈 Go 如何实现面向对象。因为 Java 是比较「规整」的面向对象实现,所以下文会多次与 Java 进行对比。 + +## 封装 + +封装指隐藏对象的属性和实现细节,仅对外提供公共访问方式。 + +这个应该不用多讲,Go 使用大小写控制可访问性(包外),大写代表导出(public),小写代表私有(private)。 + +方法暴露,使用 `receiver` ,类型、变量、方法的可见性规则都是用大小写。 + +```go +type Person struct { + Name string // 大写,导出,包外可见,public + age int // 小写,私有,仅包内可见,private +} + +// Speak 暴露公有方法 +func (p *Person) Speak() { + fmt.Println("Hello, my name is", p.Name) +} + +// SetAge GetAge 方法略 +``` + +为类增加方法,Go 与 Java 最大的不同是,Java 的方法是在类内的,而 Go 在类外,有点像 struct 上贴了一个个狗皮膏药的感觉。 +起初可能会不习惯,但这让方法的添加变得更灵活,receiver 的设计更使得所有类都能拥有方法。 + +Go 的 receiver 的设计更符合底层的面向对象的思想:为某一类事物,附加一些行为。 + +Go 的任何非内置的「定义类型」,都能拥有方法;而 Java,只有类才能拥有方法,或者说,想在一个东西上施加操作,必须定义一个类,显得臃肿。 + +## 继承 + +我对继承的理解是,子类拥有父类的所有属性和方法。 + +Go 的继承使用类似组合的方式实现,与标准面向对象实现最大的区别在于,基类变量不能引用子类变量。 + +```go +type Man struct { + Person // 内嵌,继承 + otherField string // 组合 +} + +man := Man{"bird"} +_ = man.Person.Name +_ = man.Name // 省略 Person 匿名字段 +man.Speak() +``` + +如上,在 Man 中加入一个只有类型没有名字的 Person 属性,就是内嵌。Go 的一大特性就是,内嵌字段可以被省略。详见代码。 + +所以,Man 可以省略 Person,**直接访问 Person 的所有属性并调用其方法**。所以看起来,就像继承一样。 + +总结一下就是,结构体内嵌匿名变量就是继承,无名就是组合。 + +和 Java 的区别在于, `var p person = Man{}` 是不可行的。父类变量无法引用子类对象(但这并不意味着 Go 没有多态)。 +所以 Go 的继承是不满足「里氏替换」原则的。 + +我觉得这个特性挺好,逼迫开发者少使用继承,多面向抽象编程。 + +如何实现对方法的重写(Override),即子类覆盖父类的同签名方法,以实现不同表现? + +稍微岔开一下,Go 没有「重载」,即同函数名,函数签名却不同。所以这个问题其实是,Go 如何覆盖父类同名方法? + +这时候就体现 Go 的设计哲学了, less is more。你都不需要知道什么是重写: + +没有那么多的术语和要记的东西,也不需要新的关键字,一切都是自然而然: +子类想用父类的,直接继承了不用管;子类想拥有不同的表现形式(行为),那就自己定义一个。 + +怎么自己定义一个方法呢,很简单,定义一个以子类作为 receiver 的方法。正如前面的继承,没有新增任何关键字。 + +简单理一下逻辑,如果调用方法的时候,子类本身拥有该方法,那就直接调用;如果没有,就看看父类有没有,一层层找上去。 + +如何实现多继承?想继承谁,内嵌什么类型就行。 + +至此,Go 非常优雅且简单地实现了继承。 + +这里还有个面向对象的原则,「使用组合而不是继承」,通俗继承的坏处很多,比如父类改了子类就会被动跟着改动。 +所以 Go 使用了组合的方式来实现了继承,是不是天生规避了一些继承的缺点? +以及继承会暴露父类的实现细节,这个问题 Go 也存在。 + +## 多态 + +Go 的多态是用 interface 实现的, interface 是 Go 语言的灵魂之一,为静态的 Go 语言增添了动态性。 + +我理解的接口,是一种「约定」,接口的方法,约定了一系列操作,某样东西能完成这个操作,它就实现了这个接口。 + +下面这段代码演示了许多面向对象的内容,其中,末尾的 `AllSpeak` 函数是多态的展示。 + +```go +type Speaker interface { + Speak() +} +type Person struct { + Name string +} +func (p Person) Speak() { + fmt.Println("Hello, my name is", p.Name) +} +type Dog struct { + Name string +} + +type SingleDog struct { + Dog +} + +func (d Dog) Speak() { + fmt.Println("Wang Wang, my name is", d.Name) +} +func TestInf(t *testing.T) { + + var s Speaker + p := Person{"Tom"} + s = p // 接口变量能指向实现了该接口的对象 + s.Speak() + + person := Person{"Tom"} + dog := Dog{"Jerry"} + singleDog := SingleDog{Dog{Name: "SingleDog"}} + speakers := []Speaker{person, dog, singleDog} + AllSpeak(speakers) +} + +// AllSpeak 多态演示 +func AllSpeak(s []Speaker) { + for _, v := range s { + v.Speak() + } +``` + +Go 语言的接口是非常灵活的,不需要显示声明,Java 需要 `implements`,而 Go 是 DuckType。 + +什么是 DuckType?鸭子会嘎嘎叫,所以会嘎嘎叫的就能当成鸭子。 + +翻译成编程语言,接口是一个约定,遵循了这个约定,就实现了接口。这个遵循,只要某个类型的方法签名是某接口定义的方法签名的超集就行。 + +所以,一定记住,在编码层面,方法先于接口,即先有了方法,才有了是否实现该接口的判断。而不是 Java 的先明确实现什么接口,挖好坑,再去填。完全相反。 + +如果实现多态?接口类型的变量,可以引用实现了该接口的任何类型的变量。 + +在 `AllSpeak` 函数中,形参是 `Speaker` 接口的切片,实参却是 Person、Dog、SingleDog 类型。 +为什么能调用成功呢?因为 `Person` 和 `Dog` 都拥有 `Speak()` 方法,满足了 `Speaker` 接口定义的所有函数的签名。 + +那为什么 SingleDog 类型也能传参成功,是不是可以理解为,SingleDog 继承了 Dog 的接口?似乎变得复杂起来了。 + +Go 没有那么多术语。紧抓两点:①编码时,先看方法,再看接口 ②某类型拥有的方法是某接口方法的超集,就实现了该接口 + +所以,是 SingleDog 先「继承」了Dog的 `Speak()` 方法,而后该方法刚好又满足了 `Speaker()` 的约定,自然就实现了该接口。 + +许多语言的接口与继承,对于开发者而言其实是迷惑的,可能经常分不清到底该用哪个,而 Go 不会。 +许多面向对象语言的继承,是被滥用的,而 Go,压根没开这个门,你只能用接口。 +它以一种非常灵活的方式,实现了多态,并且接口的设计,对「依赖反转(面向抽象编程)」天然友好。 + +interface 真的是个好东西,当你不知道怎么设计更优雅的时候,它总能给你带来惊喜。 + + diff --git a/common/oop/oop_standard.md b/common/oop/oop_standard.md new file mode 100644 index 0000000..036139c --- /dev/null +++ b/common/oop/oop_standard.md @@ -0,0 +1,7 @@ +## 面向对象标准实现 + +这里应该是一个标准的面向对象实现教程,如以 Java 为例。 + +这是一个漫长的过程,网上的资料也非常多,更推荐阅读广泛的资料、线下教学、切身实践来体会。 + +所以这里只贴些优质资源。 \ No newline at end of file diff --git a/common/oop.md b/common/oop/oop_started.md similarity index 100% rename from common/oop.md rename to common/oop/oop_started.md