一、面向?qū)ο蟪跆?/h3>
在軟件開(kāi)發(fā)領(lǐng)域,你應(yīng)該聽(tīng)到過(guò)過(guò)程式編程、面向?qū)ο缶幊?、甚至函?shù)式編程等軟件開(kāi)發(fā)方式。而面向?qū)ο缶幊谈窃诂F(xiàn)今大行其道,JAVA就是面向?qū)ο笳Z(yǔ)言的代表,在JAVA中一切皆對(duì)象,它讓編程中的一切元素、甚至設(shè)計(jì)方式都標(biāo)準(zhǔn)化,這更有利于大型應(yīng)用的編寫(xiě)。
1.什么是面向?qū)ο缶幊蹋?/h4>
面向?qū)ο缶幊?,?jiǎn)稱OOP。在OOP的理念下,任何事物無(wú)論簡(jiǎn)單還是復(fù)雜都可以用對(duì)象表示,每個(gè)對(duì)象都包含屬性和方法,屬性表示對(duì)象是什么?有什么特征?方法表示對(duì)象能做什么?有什么能力?任何應(yīng)用的構(gòu)建都轉(zhuǎn)化成對(duì)象關(guān)系的設(shè)計(jì),這演化成一套標(biāo)準(zhǔn)化的面向?qū)ο笤O(shè)計(jì)模式。
類(lèi)和對(duì)象
要理解OOP,首先要理解類(lèi)和對(duì)象的關(guān)系,類(lèi)是設(shè)計(jì)層面的概念,而對(duì)象則是程序運(yùn)行時(shí)的概念,OOP程序設(shè)計(jì)基于類(lèi)的設(shè)計(jì),類(lèi)在程序運(yùn)行時(shí)實(shí)例化為對(duì)象實(shí)現(xiàn)真正的業(yè)務(wù)邏輯。簡(jiǎn)而言之,所謂類(lèi)可以理解成對(duì)象的模板,你編寫(xiě)一個(gè)類(lèi),在運(yùn)行時(shí)需要實(shí)例化才能在程序調(diào)用棧中傳遞。
屬性和方法
在過(guò)程式編程中,我們熟悉變量和函數(shù),使用這些基本元素我們實(shí)現(xiàn)業(yè)務(wù)邏輯。而屬性和方法是對(duì)象內(nèi)部的特征,咋一看他們很像,其實(shí)本質(zhì)上是將實(shí)現(xiàn)特定功能的函數(shù)和變量封裝成一個(gè)整體,即對(duì)象。一個(gè)對(duì)象包含一系列的屬性和方法專(zhuān)注于實(shí)現(xiàn)某種特定功能。
接口
接口是一系列方法的聲明,是一些方法特征的集合,一個(gè)接口只有方法的特征沒(méi)有方法的實(shí)現(xiàn),因此這些方法可以在不同的地方被不同的類(lèi)實(shí)現(xiàn),而這些實(shí)現(xiàn)可以具有不同的行為(功能),接口是多態(tài)實(shí)現(xiàn)的基礎(chǔ)。
2.面向?qū)ο笕筇匦?/h4>
封裝
將實(shí)現(xiàn)特定功能的屬性和方法抽象封裝成類(lèi),提供public/private/protected訪問(wèn)修飾符,控制外部訪問(wèn)的可見(jiàn)性。
.對(duì)業(yè)務(wù)相近的變量和函數(shù)封裝在類(lèi)/結(jié)構(gòu)體中
.變量=>類(lèi)/結(jié)構(gòu)體的屬性
.函數(shù)=>類(lèi)/結(jié)構(gòu)體的方法
繼承
對(duì)已經(jīng)實(shí)現(xiàn)的類(lèi)或接口提供重用或擴(kuò)展的能力,子類(lèi)可完全繼承父類(lèi)的所有能力。繼承的過(guò)程,就是從一般到特殊的過(guò)程,其過(guò)程可以通過(guò)繼承和組合來(lái)實(shí)現(xiàn)。
.繼承的類(lèi)擁有父類(lèi)/父結(jié)構(gòu)體的全部屬性和方法
.接口繼承則是一行代碼擁有父類(lèi)接口的全部抽象方法
.繼承可以節(jié)約大量重復(fù)代碼
.繼承的目的:
.提高代碼的復(fù)用度
.拓展出新的屬性和方法
.改進(jìn)父類(lèi)/結(jié)構(gòu)體的方法
.以覆寫(xiě)父類(lèi)/結(jié)構(gòu)體的方法類(lèi)實(shí)現(xiàn)
多態(tài)
多態(tài)是指一個(gè)父類(lèi)/接口可以擁有多種具體的子類(lèi)實(shí)現(xiàn)形態(tài);多態(tài)的好處是可以根據(jù)業(yè)務(wù)需要去方便地調(diào)度子類(lèi)們的共性和個(gè)性
3.面向?qū)ο缶幊痰膬?yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 高效:面向?qū)ο笤O(shè)計(jì)結(jié)構(gòu)、模塊清晰的應(yīng)用,有利于大型應(yīng)用的開(kāi)發(fā),團(tuán)隊(duì)成員各自維護(hù)局部模塊,降低了成員開(kāi)發(fā)的復(fù)雜性。
- 易維護(hù):由于高內(nèi)聚低耦合,各個(gè)模塊的維護(hù)都是局部的,這非常方便定位問(wèn)題。
- 易擴(kuò)展:繼承、封裝、多態(tài)的特性,以及標(biāo)準(zhǔn)化的設(shè)計(jì)模式設(shè)計(jì)出高內(nèi)聚、低耦合的系統(tǒng)結(jié)構(gòu),使得系統(tǒng)更靈活、更容易擴(kuò)展,而且成本較低。
缺點(diǎn)
將實(shí)現(xiàn)特定功能的屬性和方法抽象封裝成類(lèi),提供public/private/protected訪問(wèn)修飾符,控制外部訪問(wèn)的可見(jiàn)性。
.對(duì)業(yè)務(wù)相近的變量和函數(shù)封裝在類(lèi)/結(jié)構(gòu)體中
.變量=>類(lèi)/結(jié)構(gòu)體的屬性
.函數(shù)=>類(lèi)/結(jié)構(gòu)體的方法
對(duì)已經(jīng)實(shí)現(xiàn)的類(lèi)或接口提供重用或擴(kuò)展的能力,子類(lèi)可完全繼承父類(lèi)的所有能力。繼承的過(guò)程,就是從一般到特殊的過(guò)程,其過(guò)程可以通過(guò)繼承和組合來(lái)實(shí)現(xiàn)。
.繼承的類(lèi)擁有父類(lèi)/父結(jié)構(gòu)體的全部屬性和方法
.接口繼承則是一行代碼擁有父類(lèi)接口的全部抽象方法
.繼承可以節(jié)約大量重復(fù)代碼
.繼承的目的:
.提高代碼的復(fù)用度
.拓展出新的屬性和方法
.改進(jìn)父類(lèi)/結(jié)構(gòu)體的方法
.以覆寫(xiě)父類(lèi)/結(jié)構(gòu)體的方法類(lèi)實(shí)現(xiàn)
多態(tài)是指一個(gè)父類(lèi)/接口可以擁有多種具體的子類(lèi)實(shí)現(xiàn)形態(tài);多態(tài)的好處是可以根據(jù)業(yè)務(wù)需要去方便地調(diào)度子類(lèi)們的共性和個(gè)性
俗話說(shuō),“如果你手上只有錘子,那么你看什么都是釘子”,OOP把一切都當(dāng)成對(duì)象,但現(xiàn)實(shí)世界是復(fù)雜的,雖然設(shè)計(jì)模式就是解決應(yīng)用中抽象的設(shè)計(jì)問(wèn)題的。這把編碼階段的復(fù)雜性提到設(shè)計(jì)階段。
- 相對(duì)過(guò)程式編程,面向?qū)ο缶幊痰某绦蚪Y(jié)構(gòu)性能有所下降;
- 提高系統(tǒng)設(shè)計(jì)的復(fù)雜度;
二、Go的面向“對(duì)象”
了解了面向?qū)ο缶幊痰乃枷耄覀冊(cè)賮?lái)看Go的面向?qū)ο?,?yán)格來(lái)說(shuō),Go并非面向?qū)ο缶幊陶Z(yǔ)言,Go有自己的設(shè)計(jì)理念,面向?qū)ο笾皇且环N軟件開(kāi)發(fā)方法,Go有自己的支持方式。
1.沒(méi)有類(lèi)和對(duì)象,只有類(lèi)型和值
傳統(tǒng)的JAVA類(lèi)和對(duì)象:
//類(lèi)定義
public class Person {
private String name
...
//構(gòu)造方法
public Person( String name){
this.name = name
}
//公開(kāi)方法
public String getName (){
return this.name
}
...
}
//類(lèi)實(shí)例化成對(duì)象
Person p = new Person("name")
//調(diào)用并打印公開(kāi)方法
System.out.print(p.getName())
Go的類(lèi)型和值:
- 通過(guò)定義結(jié)構(gòu)體類(lèi)型的方式實(shí)現(xiàn)類(lèi)似類(lèi)的結(jié)構(gòu)
- 沒(méi)有構(gòu)造方法,直接使用NewXXX()工廠方法
//類(lèi)型定義
type Preson struct {
name string
}
//類(lèi)型方法
func (p *Person) SetName(name string) {
p.name = name
}
//類(lèi)型方法
func (p *Person) GetName() string {
return p.name
}
//工廠方法
func NewPerson(name string) *Person{
p := new(Person)
p.SetName(name)
return p
}
//獲取person類(lèi)型的值指針
p := NewPerson("name")
fmt.Println(p.GetName())
2.聚合和嵌入優(yōu)于繼承
傳統(tǒng)的JAVA繼承:
public class Student extend Person{
private String school
//構(gòu)造方法
public Student(){
super() //直接使用父類(lèi)的構(gòu)造方法
}
public void doSomething(){
//block
}
...
}
Go的聚合和嵌入:
type Preson struct {
Name string
age int
}
func (p *Person) SetAge(age int){
p.age = age
}
//嵌入
type Student1 struct {
Person //匿名字段為嵌入類(lèi)型
School string
}
//聚合
type Student2 struct {
Ps Person //命名字段為聚合類(lèi)型
School string
}
func OOPDemo() {
s1 := new(Student1)
s1.Name = "fun1" //嵌入的類(lèi)型可直接使用其內(nèi)部屬性,更像繼承
s1.School = "Social University1"
s1.SetAge(18) //可以直接使用Person的方法
s2 := new(Student2)
s2.Ps.Name = "fun2" //聚合的類(lèi)型需要先訪問(wèn)屬性值名,在訪問(wèn)屬性值內(nèi)部的屬性
s2.School = "Social University2"
s1.Ps.SetAge(18) //可以間接使用Person的方法
//OUTPUT:
//s1: &{{fun1} Social University1}
//s2: &{{fun2} Social University2}
}
3.自由的結(jié)構(gòu)體屬性類(lèi)型
//接口
type IPerson interface {
SetName(string)
GetName() string
}
//自定義函數(shù)
type MyFuncType func(int) int
//大雜燴結(jié)構(gòu)體
type Something struct {
a int //基本數(shù)據(jù)類(lèi)型
b []byte //切片
p Person //結(jié)構(gòu)體
s *Student //指針
i IPerson //接口
f MyFuncType //自定義函數(shù)類(lèi)型
any interface{} //任意類(lèi)型
}
4.獨(dú)立的方法定義更靈活
Go的類(lèi)型方法在外部任意地方,只要定義的方法接收者為該類(lèi)型,那定義的方法就是該類(lèi)型的方法。
//類(lèi)型定義
type Preson struct {
name string
}
//類(lèi)型方法,接收者為該類(lèi)型的指針
func (p *Person) SetName(name string) {
p.name = name
}
//類(lèi)型方法,接收者為該類(lèi)型的值
func (p Person) GetName() string {
return p.name
}
//調(diào)用示例
p := new(Person)
//SetName()方法接收者為指針,使用指針類(lèi)型或值類(lèi)型去調(diào)用都可以
p.SetName("fun") //可以
*p.SetName("func") //指針取值后再去調(diào)用也可以。
//GetName()方法接收者為值類(lèi)型,所以調(diào)用該方法只能為值
p.GetName() //不可以
*p.GetName() //可以
方法接收者一般有兩種情況:
- 接收者為指針:允許該類(lèi)型的指針和值調(diào)用該方法;
- 接收者為值:只允許該類(lèi)型的值調(diào)用該方法。
一般無(wú)特殊需要,建議把接收者直接設(shè)置為指針類(lèi)型
5.沒(méi)有顯式public/private/protected,只有隱式大小寫(xiě)控制
Go的訪問(wèn)控制基于包,包內(nèi)的成員變量、常量、類(lèi)型、函數(shù)基于命名首字母的大小寫(xiě)控制。
其結(jié)構(gòu)體類(lèi)型的屬性方法也類(lèi)似,基于命名首字母的大小寫(xiě)控制。
type Preson struct {
Name string //外部可見(jiàn)并可修改
age int //僅內(nèi)部可見(jiàn)內(nèi)部方法修改
}
//類(lèi)型方法,外部可見(jiàn)
func (p *Person) SetAge(age int) {
//調(diào)用內(nèi)部方法
p.setage(name)
}
//類(lèi)型方法,內(nèi)部可見(jiàn)
func (p *Person) setage(age int) {
p.age = age
}
三、Go面向接口編程
嚴(yán)格意義講,Go因沒(méi)有對(duì)象概念,所以并非面向?qū)ο缶幊陶Z(yǔ)言,但因其對(duì)OOP的深刻理解,使其設(shè)計(jì)理念更切合“面向接口”,接口的多態(tài)特性使其在設(shè)計(jì)高內(nèi)聚低耦合的系統(tǒng)發(fā)揮更重要的作用。面向接口概念是面向?qū)ο蟮难苌?,在多年的開(kāi)發(fā)積累中,人們發(fā)現(xiàn)針對(duì)接口設(shè)計(jì)系統(tǒng)可以讓系統(tǒng)擴(kuò)展性和維護(hù)性更好。因此,Go的OOP針對(duì)接口設(shè)計(jì),可以說(shuō)接口是頭等的類(lèi)型也不為過(guò)。
在Go語(yǔ)言中,接口擁有舉足輕重的地位,而面向接口編程也是Go語(yǔ)言核心的設(shè)計(jì)理念。接口是高度抽象的概念,它是一種類(lèi)型可由type關(guān)鍵字聲明,接口內(nèi)部聲明一個(gè)或多個(gè)方法簽名,因此不能實(shí)例化,一般創(chuàng)建一個(gè)類(lèi)型為接口的變量,它可以被賦值為任何滿足該接口聲明的實(shí)際類(lèi)型的值,作為類(lèi)型傳遞。
1.接口 —— 實(shí)現(xiàn)鴨子類(lèi)型
當(dāng)它走起來(lái)像鴨子,叫起來(lái)也像鴨子,那它就是鴨子。
接口本身是類(lèi)型,但它卻不關(guān)心類(lèi)型,它只關(guān)心行為,如果類(lèi)型T的行為(實(shí)現(xiàn)的方法)和定義的接口I聲明的方法簽名符合,那么類(lèi)型T就實(shí)現(xiàn)了I的接口。在方法參數(shù)傳遞或各種類(lèi)型校驗(yàn)中,T就是I的實(shí)現(xiàn)。
一個(gè)Go接口也是類(lèi)型定義,其內(nèi)部聲明了其規(guī)定的方法簽名:
//任何實(shí)現(xiàn)了IAnimal簽名方法的類(lèi)型都屬于IAnimal類(lèi)型
type IAnimal interface {
Live()
Dead()
}
//Humen結(jié)構(gòu)體
type Monkey struct {}
func (m *Monkey) Live() {
fmt.Println("猴子活著吃香蕉?。。?)
}
func (m *Monkey) Dead() {
fmt.Println("香蕉有毒,猴子死了?。。?)
}
//Cat結(jié)構(gòu)體
type Cat struct {}
func (c *Cat) Live() {
fmt.Println("貓活著吃魚(yú)?。?!")
}
func (c *Cat) Dead() {
fmt.Println("貓吃了河豚死翹翹?。?!")
}
//演示:
//聲明一個(gè)IAnimal的切片
var zoo []IAnimal
func addAnimal(animal IAnimal) {
zoo = append(zoo,animal)
}
func OOPDemo02(){
m := new(Monkey)
c := new(Cat)
addAnimal(m)
addAnimal(c)
//遍歷
for _,animal := range zoo {
animal.Live()
animal.Dead()
}
}
//OUTPUT:
//猴子活著吃香蕉?。?!
//香蕉有毒,猴子死了?。。?//貓活著吃魚(yú)?。?!
//貓吃了河豚死翹翹!??!
2.接口類(lèi)型限定賦值
類(lèi)型值賦值給接口類(lèi)型
type IPerson interface{
GetName()
SetName()
}
//任何實(shí)現(xiàn)GetName()、SetName()方法的類(lèi)型都可賦值給person
var person IPerson
接口類(lèi)型賦值給另一接口類(lèi)型
type Writer interface{ //父接口
Write(buf []byte) (n int,err error)
}
type ReadWriter interface{ //子接口
Read(buf []byte) (n int,err error)
Write(buf []byte) (n int,err error)
}
var file1 ReadWriter=new(File) //子接口實(shí)例
var file2 Writer=file1 //子接口實(shí)例賦值給父接口
3.非侵入式接口
上面演示我們看到,接口的運(yùn)用在編碼中是非侵入式的,在經(jīng)典的OOP語(yǔ)言中,實(shí)現(xiàn)接口需要類(lèi)顯式實(shí)現(xiàn),例如:
public class Person implements IPerson {
//block
}
而Go并不需要顯式實(shí)現(xiàn),類(lèi)型只需實(shí)現(xiàn)特定接口的方法簽名即可。當(dāng)然這種極度寬松的實(shí)現(xiàn)方式有可能讓你定義的類(lèi)型“不小心”就實(shí)現(xiàn)了某些接口能力,一旦你的類(lèi)型方法和某些接口方法簽名一致時(shí)就會(huì)如此。
//該類(lèi)型實(shí)現(xiàn)了標(biāo)準(zhǔn)包的io.Writer接口
type F struct {}
func (f *F) Write(p []byte) (n int, err error) {
//block
}
4.接口嵌入
和結(jié)構(gòu)體類(lèi)型類(lèi)似,接口也可以嵌入其他接口,接口只能嵌入不能聚合!
以下演示接口嵌入的示例:
//生物接口
type IBeing interface{
Live()
Dead()
}
//動(dòng)物接口嵌入生物接口
type IAnimal interface {
IBeing
Hunting()
}
//植物接口嵌入生物接口
type IPlant interface {
IBeing
Growing()
}
type Tiger struct {}
func (t *Tiger) Live() {
fmt.Println("老虎活著稱大王?。。?)
}
func (t *Tiger) Dead() {
fmt.Println("老虎戰(zhàn)斗死了?。?!")
}
func (t *Tiger) Hunting() {
fmt.Println("老虎捕獵?。。?)
}
type Flower struct{}
func (t *Flower) Live() {
fmt.Println("花兒享受陽(yáng)光?。?!")
}
func (t *Flower) Dead() {
fmt.Println("花兒落下死了?。?!")
}
func (t *Flower) Growing() {
fmt.Println("花兒茁壯成長(zhǎng)!??!")
}
//聲明一個(gè)interface{}的切片
var earth []interface{}
func addBeing(b interface{}) {
earth = append(earth, b)
}
func OOPDemo03() {
tiger := new(Tiger)
flower := new(Flower)
addBeing(tiger)
addBeing(flower)
//遍歷
for _, being := range earth {
if animal, ok := being.(IAnimal); ok {
animal.Live()
//如果是動(dòng)物則捕獵
animal.Hunting()
animal.Dead()
}
if plant, ok := being.(IPlant); ok {
plant.Live()
//如果是植物則成長(zhǎng)
plant.Growing()
plant.Dead()
}
}
}
//OUTPUT:
//老虎活著稱大王?。?!
//老虎捕獵?。?!
//老虎戰(zhàn)斗死了?。?!
//花兒享受陽(yáng)光!??!
//花兒茁壯成長(zhǎng)!??!
//花兒落下死了?。?!
...
5.沒(méi)有泛型
在其他面向?qū)ο笳Z(yǔ)言中,我們都會(huì)接觸到泛型的概念,當(dāng)我們遇到要對(duì)不同類(lèi)型做統(tǒng)一的內(nèi)部實(shí)現(xiàn)是,使用泛型是非常常規(guī)的做法,有了泛型后我們不必對(duì)每種數(shù)據(jù)類(lèi)型做同樣的實(shí)現(xiàn)。但是在Go中沒(méi)有泛型,為什么Go團(tuán)隊(duì)設(shè)計(jì)時(shí)不加入泛型?我們需要了解泛型的本質(zhì)是什么。
無(wú)論是JAVA中的泛型支持還是C++中的模板方法,使用泛型的初衷在于程序編碼時(shí)減少同時(shí)支持多種類(lèi)型相同內(nèi)部實(shí)現(xiàn)的方法的編碼量,把編碼的復(fù)雜性延遲到運(yùn)行時(shí)自動(dòng)創(chuàng)建對(duì)相應(yīng)類(lèi)型的支持,Go團(tuán)隊(duì)認(rèn)為泛型的支持在類(lèi)型系統(tǒng)和運(yùn)行時(shí)的復(fù)雜性花費(fèi)太大,還沒(méi)有找到更優(yōu)化的設(shè)計(jì),這是Go團(tuán)隊(duì)比較猶豫的地方。
Go語(yǔ)言對(duì)泛型有其替代方案:interface{},一般叫空接口,空接口內(nèi)部沒(méi)有聲明任何方法,因此指代Go中的任何類(lèi)型,但聲明為空接口類(lèi)型時(shí),指代接收任何數(shù)據(jù)類(lèi)型,在使用通過(guò)空接口傳遞的數(shù)據(jù)時(shí),通過(guò)類(lèi)型斷言的方式接收數(shù)據(jù)并做進(jìn)一步處理。這種做法把運(yùn)行時(shí)出錯(cuò)的可能性提前到編譯時(shí)發(fā)現(xiàn),如有運(yùn)行時(shí)錯(cuò)誤也能等到友好的處理。
type Person struct {
name string
}
func (p *Person) PrintName() {
fmt.Println(p.name)
}
func main() {
var person interface{} = new(Person)
p, ok := person.(*Person)
if ok {
p.name="fun"
p.PrintName()
}else{
fmt.PrintLn("Type Error!")
}
}