[TOC]
## 6、面向對象的編程思維理解interface。
### 一、 interface接口
interface 是GO語言的基礎特性之一。可以理解為一種類型的規范或者約定。它跟java,C# 不太一樣,不需要顯示說明實現了某個接口,它沒有繼承或子類或“implements”關鍵字,只是通過約定的形式,隱式的實現interface 中的方法即可。因此,Golang 中的 interface 讓編碼更靈活、易擴展。
如何理解go 語言中的interface ? 只需記住以下三點即可:
1. interface 是方法聲明的集合
2. 任何類型的對象實現了在interface 接口中聲明的全部方法,則表明該類型實現了該接口。
3. interface 可以作為一種數據類型,實現了該接口的任何對象都可以給對應的接口類型變量賦值。
>注意:
> a. interface 可以被任意對象實現,一個類型/對象也可以實現多個 interface
> b. 方法不能重載,如 `eat(), eat(s string)` 不能同時存在
```go
package main
import "fmt"
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type ApplePhone struct {
}
func (iPhone ApplePhone) call() {
fmt.Println("I am Apple Phone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(ApplePhone)
phone.call()
}
```
上述中體現了`interface`接口的語法,在`main`函數中,也體現了`多態`的特性。
同樣一個`phone`的抽象接口,分別指向不同的實體對象,調用的call()方法,打印的效果不同,那么就是體現出了多態的特性。
### 二、 面向對象中的開閉原則
#### 2.1 平鋪式的模塊設計
那么作為`interface`數據類型,他存在的意義在哪呢? 實際上是為了滿足一些面向對象的編程思想。我們知道,軟件設計的最高目標就是`高內聚,低耦合`。那么其中有一個設計原則叫`開閉原則`。什么是開閉原則呢,接下來我們看一個例子:
```go
package main
import "fmt"
//我們要寫一個類,Banker銀行業務員
type Banker struct {
}
//存款業務
func (this *Banker) Save() {
fmt.Println( "進行了 存款業務...")
}
//轉賬業務
func (this *Banker) Transfer() {
fmt.Println( "進行了 轉賬業務...")
}
//支付業務
func (this *Banker) Pay() {
fmt.Println( "進行了 支付業務...")
}
func main() {
banker := &Banker{}
banker.Save()
banker.Transfer()
banker.Pay()
}
```
代碼很簡單,就是一個銀行業務員,他可能擁有很多的業務,比如`Save()`存款、`Transfer()`轉賬、`Pay()`支付等。那么如果這個業務員模塊只有這幾個方法還好,但是隨著我們的程序寫的越來越復雜,銀行業務員可能就要增加方法,會導致業務員模塊越來越臃腫。

? 這樣的設計會導致,當我們去給Banker添加新的業務的時候,會直接修改原有的Banker代碼,那么Banker模塊的功能會越來越多,出現問題的幾率也就越來越大,假如此時Banker已經有99個業務了,現在我們要添加第100個業務,可能由于一次的不小心,導致之前99個業務也一起崩潰,因為所有的業務都在一個Banker類里,他們的耦合度太高,Banker的職責也不夠單一,代碼的維護成本隨著業務的復雜正比成倍增大。
#### 2.2 開閉原則設計
那么,如果我們擁有接口, `interface`這個東西,那么我們就可以抽象一層出來,制作一個抽象的Banker模塊,然后提供一個抽象的方法。 分別根據這個抽象模塊,去實現`支付Banker(實現支付方法)`,`轉賬Banker(實現轉賬方法)`
如下:

那么依然可以搞定程序的需求。 然后,當我們想要給Banker添加額外功能的時候,之前我們是直接修改Banker的內容,現在我們可以單獨定義一個`股票Banker(實現股票方法)`,到這個系統中。 而且股票Banker的實現成功或者失敗都不會影響之前的穩定系統,他很單一,而且獨立。
所以以上,當我們給一個系統添加一個功能的時候,不是通過修改代碼,而是通過增添代碼來完成,那么就是開閉原則的核心思想了。所以要想滿足上面的要求,是一定需要interface來提供一層抽象的接口的。
golang代碼實現如下:
```go
package main
import "fmt"
//抽象的銀行業務員
type AbstractBanker interface{
DoBusi() //抽象的處理業務接口
}
//存款的業務員
type SaveBanker struct {
//AbstractBanker
}
func (sb *SaveBanker) DoBusi() {
fmt.Println("進行了存款")
}
//轉賬的業務員
type TransferBanker struct {
//AbstractBanker
}
func (tb *TransferBanker) DoBusi() {
fmt.Println("進行了轉賬")
}
//支付的業務員
type PayBanker struct {
//AbstractBanker
}
func (pb *PayBanker) DoBusi() {
fmt.Println("進行了支付")
}
func main() {
//進行存款
sb := &SaveBanker{}
sb.DoBusi()
//進行轉賬
tb := &TransferBanker{}
tb.DoBusi()
//進行支付
pb := &PayBanker{}
pb.DoBusi()
}
```
當然我們也可以根據`AbstractBanker`設計一個小框架
```go
//實現架構層(基于抽象層進行業務封裝-針對interface接口進行封裝)
func BankerBusiness(banker AbstractBanker) {
//通過接口來向下調用,(多態現象)
banker.DoBusi()
}
```
那么main中可以如下實現業務調用:
```go
func main() {
//進行存款
BankerBusiness(&SaveBanker{})
//進行存款
BankerBusiness(&TransferBanker{})
//進行存款
BankerBusiness(&PayBanker{})
}
```
>再看開閉原則定義:
>開閉原則:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。
>簡單的說就是在修改需求的時候,應該盡量通過擴展來實現變化,而不是通過修改已有代碼來實現變化。
### 三、 接口的意義
好了,現在interface已經基本了解,那么接口的意義最終在哪里呢,想必現在你已經有了一個初步的認知,實際上接口的最大的意義就是實現多態的思想,就是我們可以根據interface類型來設計API接口,那么這種API接口的適應能力不僅能適應當下所實現的全部模塊,也適應未來實現的模塊來進行調用。 `調用未來`可能就是接口的最大意義所在吧,這也是為什么架構師那么值錢,因為良好的架構師是可以針對interface設計一套框架,在未來許多年卻依然適用。
### 四、 面向對象中的依賴倒轉原則
#### 4.1 耦合度極高的模塊關系設計

```go
package main
import "fmt"
// === > 奔馳汽車 <===
type Benz struct {
}
func (this *Benz) Run() {
fmt.Println("Benz is running...")
}
// === > 寶馬汽車 <===
type BMW struct {
}
func (this *BMW) Run() {
fmt.Println("BMW is running ...")
}
//===> 司機張三 <===
type Zhang3 struct {
//...
}
func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
fmt.Println("zhang3 Drive Benz")
benz.Run()
}
func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
fmt.Println("zhang3 drive BMW")
bmw.Run()
}
//===> 司機李四 <===
type Li4 struct {
//...
}
func (li4 *Li4) DriveBenZ(benz *Benz) {
fmt.Println("li4 Drive Benz")
benz.Run()
}
func (li4 *Li4) DriveBMW(bmw *BMW) {
fmt.Println("li4 drive BMW")
bmw.Run()
}
func main() {
//業務1 張3開奔馳
benz := &Benz{}
zhang3 := &Zhang3{}
zhang3.DriveBenZ(benz)
//業務2 李四開寶馬
bmw := &BMW{}
li4 := &Li4{}
li4.DriveBMW(bmw)
}
```
我們來看上面的代碼和圖中每個模塊之間的依賴關系,實際上并沒有用到任何的`interface`接口層的代碼,顯然最后我們的兩個業務 `張三開奔馳`, `李四開寶馬`,程序中也都實現了。但是這種設計的問題就在于,小規模沒什么問題,但是一旦程序需要擴展,比如我現在要增加一個`豐田汽車` 或者 司機`王五`, 那么模塊和模塊的依賴關系將成指數級遞增,想蜘蛛網一樣越來越難維護和捋順。
#### 4.2 面向抽象層依賴倒轉

如上圖所示,如果我們在設計一個系統的時候,將模塊分為3個層次,抽象層、實現層、業務邏輯層。那么,我們首先將抽象層的模塊和接口定義出來,這里就需要了`interface`接口的設計,然后我們依照抽象層,依次實現每個實現層的模塊,在我們寫實現層代碼的時候,實際上我們只需要參考對應的抽象層實現就好了,實現每個模塊,也和其他的實現的模塊沒有關系,這樣也符合了上面介紹的開閉原則。這樣實現起來每個模塊只依賴對象的接口,而和其他模塊沒關系,依賴關系單一。系統容易擴展和維護。
我們在指定業務邏輯也是一樣,只需要參考抽象層的接口來業務就好了,抽象層暴露出來的接口就是我們業務層可以使用的方法,然后可以通過多態的線下,接口指針指向哪個實現模塊,調用了就是具體的實現方法,這樣我們業務邏輯層也是依賴抽象成編程。
我們就將這種的設計原則叫做`依賴倒轉原則`。
來一起看一下修改的代碼:
```go
package main
import "fmt"
// ===== > 抽象層 < ========
type Car interface {
Run()
}
type Driver interface {
Drive(car Car)
}
// ===== > 實現層 < ========
type BenZ struct {
//...
}
func (benz * BenZ) Run() {
fmt.Println("Benz is running...")
}
type Bmw struct {
//...
}
func (bmw * Bmw) Run() {
fmt.Println("Bmw is running...")
}
type Zhang_3 struct {
//...
}
func (zhang3 *Zhang_3) Drive(car Car) {
fmt.Println("Zhang3 drive car")
car.Run()
}
type Li_4 struct {
//...
}
func (li4 *Li_4) Drive(car Car) {
fmt.Println("li4 drive car")
car.Run()
}
// ===== > 業務邏輯層 < ========
func main() {
//張3 開 寶馬
var bmw Car
bmw = &Bmw{}
var zhang3 Driver
zhang3 = &Zhang_3{}
zhang3.Drive(bmw)
//李4 開 奔馳
var benz Car
benz = &BenZ{}
var li4 Driver
li4 = &Li_4{}
li4.Drive(benz)
}
```
#### 4.3 依賴倒轉小練習
> 模擬組裝2臺電腦,
> --- 抽象層 ---有顯卡Card 方法display,有內存Memory 方法storage,有處理器CPU 方法calculate
> --- 實現層層 ---有 Intel因特爾公司 、產品有(顯卡、內存、CPU),有 Kingston 公司, 產品有(內存3),有 NVIDIA 公司, 產品有(顯卡)
> --- 邏輯層 ---1. 組裝一臺Intel系列的電腦,并運行,2. 組裝一臺 Intel CPU Kingston內存 NVIDIA顯卡的電腦,并運行
```go
/*
模擬組裝2臺電腦
--- 抽象層 ---
有顯卡Card 方法display
有內存Memory 方法storage
有處理器CPU 方法calculate
--- 實現層層 ---
有 Intel因特爾公司 、產品有(顯卡、內存、CPU)
有 Kingston 公司, 產品有(內存3)
有 NVIDIA 公司, 產品有(顯卡)
--- 邏輯層 ---
1. 組裝一臺Intel系列的電腦,并運行
2. 組裝一臺 Intel CPU Kingston內存 NVIDIA顯卡的電腦,并運行
*/
package main
import "fmt"
//------ 抽象層 -----
type Card interface{
Display()
}
type Memory interface {
Storage()
}
type CPU interface {
Calculate()
}
type Computer struct {
cpu CPU
mem Memory
card Card
}
func NewComputer(cpu CPU, mem Memory, card Card) *Computer{
return &Computer{
cpu:cpu,
mem:mem,
card:card,
}
}
func (this *Computer) DoWork() {
this.cpu.Calculate()
this.mem.Storage()
this.card.Display()
}
//------ 實現層 -----
//intel
type IntelCPU struct {
CPU
}
func (this *IntelCPU) Calculate() {
fmt.Println("Intel CPU 開始計算了...")
}
type IntelMemory struct {
Memory
}
func (this *IntelMemory) Storage() {
fmt.Println("Intel Memory 開始存儲了...")
}
type IntelCard struct {
Card
}
func (this *IntelCard) Display() {
fmt.Println("Intel Card 開始顯示了...")
}
//kingston
type KingstonMemory struct {
Memory
}
func (this *KingstonMemory) Storage() {
fmt.Println("Kingston memory storage...")
}
//nvidia
type NvidiaCard struct {
Card
}
func (this *NvidiaCard) Display() {
fmt.Println("Nvidia card display...")
}
//------ 業務邏輯層 -----
func main() {
//intel系列的電腦
com1 := NewComputer(&IntelCPU{}, &IntelMemory{}, &IntelCard{})
com1.DoWork()
//雜牌子
com2 := NewComputer(&IntelCPU{}, &KingstonMemory{}, &NvidiaCard{})
com2.DoWork()
}
```
- 封面
- 第一篇:Golang修養必經之路
- 1、最常用的調試 golang 的 bug 以及性能問題的實踐方法?
- 2、Golang的協程調度器原理及GMP設計思想?
- 3、Golang中逃逸現象, 變量“何時棧?何時堆?”
- 4、Golang中make與new有何區別?
- 5、Golang三色標記+混合寫屏障GC模式全分析
- 6、面向對象的編程思維理解interface
- 7、Golang中的Defer必掌握的7知識點
- 8、精通Golang項目依賴Go modules
- 9、一站式精通Golang內存管理
- 第二篇:Golang面試之路
- 1、數據定義
- 2、數組和切片
- 3、Map
- 4、interface
- 5、channel
- 6、WaitGroup
- 第三篇、Golang編程設計與通用之路
- 1、流?I/O操作?阻塞?epoll?
- 2、分布式從ACID、CAP、BASE的理論推進
- 3、對于操作系統而言進程、線程以及Goroutine協程的區別
- 4、Go是否可以無限go? 如何限定數量?
- 5、單點Server的N種并發模型匯總
- 6、TCP中TIME_WAIT狀態意義詳解
- 7、動態保活Worker工作池設計