面向對象編程三大特點:封裝、繼承、多態。
1. 構造函數
Go中結構體實現封裝。
Go不支持構造器。如果某類型的零值不可用,需要提供NewT(parameters)函數,用來初始化T類型的變量。按照Go的慣例,應該把創建T類型變量的函數命名為NewT(parameters),若一個包中只含有一種類型,則函數名為New(parameters)。
包含NewT()函數的包的結構體應該首字母小寫,以使結構體對外不可引用,只能通過NewT()創建結構體。相應的,結構體內所有字段也應該小寫,被隱藏,方法要根據實際情況確認。
// oop/employee/employee.go package employee import "fmt" type employee struct { firstName string lastName string totalLeaves int leavesTaken int } func New(firstName string, lastName string, totalLeaves int, leavesTaken int) employee{ e := employee{firstName, lastName, totalLeaves, leavesTaken} return e } func (e employee) LeavesRemaining(){ fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, e.totalLeaves-e.leavesTaken) } //oop/main.go package main import "oop/employee" func main(){ /* e := employee.Employee{ FirstName: "wang", LastName: "qing", TotalLeaves: 30, LeavesTaken: 20, } */ e := employee.New("wang", "qing", 30, 20) e.LeavesRemaining() }
2. 繼承
Go不支持繼承,但他支持組合(composition)。組合的一般含義定義為“合並在一起”。
一般通過嵌套結構體進行組合,特別是匿名結構體。
3. 多態
Go通過接口來實現多態。在Go中,一個類型如果定義了接口所聲明的全部方法,那該類型就實現了該接口。
所有實現了接口的類型,都可以把它的值保存在一個接口類型的變量中。在 Go 中,我們使用接口的這種特性來實現多態。
4.面向接口
golang中面向對象編程更多的體現為面向接口。
接口 的作用其實就是為不同層級的模塊提供了一個定義好的中間層,上游不再需要依賴下游的具體實現,充分地對上下游進行了解耦。
它為我們的程序提供了非常強的靈活性,想要構建一個穩定、健壯的 Go 語言項目,不使用接口是完全無法做到的。
單元測試是一個項目保證工程質量最有效並且投資回報率最高的方法之一,作為靜態語言的 Go,想要寫出覆蓋率足夠(最少覆蓋核心邏輯)的單元測試本身就比較困難,因為我們不能像動態語言一樣隨意修改函數和方法的行為,而接口就成了我們的救命稻草,寫出抽象良好的接口並通過接口隔離依賴能夠幫助我們有效地提升項目的質量和可測試性。
如下代碼其實就不是一個設計良好的代碼,它不僅在 init
函數中隱式地初始化了 grpc 連接這種全局變量,而且沒有將 ListPosts
通過接口的方式暴露出去,這會讓依賴 ListPosts
的上層模塊難以測試。
package post var client *grpc.ClientConn func init() { var err error client, err = grpc.Dial(...) if err != nil { panic(err) } } func ListPosts() ([]*Post, error) { posts, err := client.ListPosts(...) if err != nil { return []*Post{}, err } return posts, nil }
可以使用下面的代碼改寫原有的邏輯,使得同樣地邏輯變得更容易測試和維護:
package post type Service interface { ListPosts() ([]*Post, error) } type service struct { conn *grpc.ClientConn } func NewService(conn *grpc.ClientConn) Service { return &service{ conn: conn, } } func (s *service) ListPosts() ([]*Post, error) { posts, err := s.conn.ListPosts(...) if err != nil { return []*Post{}, err } return posts, nil }
- 通過接口
Service
暴露對外的ListPosts
方法; - 使用
NewService
函數初始化Service
接口的實現並通過私有的結構體service
持有 grpc 連接; ListPosts
不再依賴全局變量,而是依賴接口體service
持有的連接;
當我們使用這種方式重構代碼之后,就可以在 main
函數中顯式的初始化 grpc 連接、創建 Service
接口的實現並調用 ListPosts
方法:
package main import ... func main() { conn, err = grpc.Dial(...) if err != nil { panic(err) } svc := post.NewService(conn) posts, err := svc.ListPosts() if err != nil { panic(err) } fmt.Println(posts) }
這種使用接口組織代碼的方式在 Go 語言中非常常見,我們應該在代碼中盡可能地使用這種思想和模式對外提供功能:
- 使用大寫的
Service
對外暴露方法; - 使用小寫的
service
實現接口中定義的方法; - 通過
NewService
函數初始化Service
接口;
當我們使用上述方法組織代碼之后,其實就對不同模塊的依賴進行了解耦,也正遵循了軟件設計中經常被提到的一句話 — 『依賴接口,不要依賴實現』,也就是面向接口編程。
參考: