Go 介面-契約介紹 目錄Go 介面-契約介紹一、介面基本介紹1.1 介面類型介紹1.2 為什麼要使用介面1.3 面向介面編程1.4 介面的定義二、空介面2.1 空介面的定義2.2 空介面的應用2.2.1 空介面作為函數的參數2.2.2 空介面作為map的值2.3 介面類型變數2.4 類型斷言三、盡 ...
Go 介面-契約介紹
目錄一、介面基本介紹
1.1 介面類型介紹
介面是一種抽象類型,它定義了一組方法的契約,它規定了需要實現的所有方法。是由 type
和 interface
關鍵字定義的一組方法集合,其中,方法集合唯一確定了這個介面類型所表示的介面。
一個介面類型通常由一組方法簽名組成,這些方法定義了對象必須實現的操作。介面的方法簽名包括方法的名稱、輸入參數、返回值等信息,但不包括方法的實際實現。例如:
type Writer interface {
Write([]byte) (int, error)
}
上面的代碼定義了一個名為 Writer
的介面,它有一個 Write
方法,該方法接受一個 []byte
類型的參數並返回兩個值,一個整數和一個錯誤。任何類型只要實現了這個 Write
方法的簽名,就可以被認為是 Writer
介面的實現。
總之,Go語言提倡面向介面編程。
1.2 為什麼要使用介面
現在假設我們的代碼世界里有很多小動物,下麵的代碼片段定義了貓和狗,它們餓了都會叫。
package main
import "fmt"
type Cat struct{}
func (c Cat) Say() {
fmt.Println("喵喵喵~")
}
type Dog struct{}
func (d Dog) Say() {
fmt.Println("汪汪汪~")
}
func main() {
c := Cat{}
c.Say()
d := Dog{}
d.Say()
}
這個時候又跑來了一隻羊,羊餓了也會發出叫聲。
type Sheep struct{}
func (s Sheep) Say() {
fmt.Println("咩咩咩~")
}
我們接下來定義一個餓肚子的場景。
// MakeCatHungry 貓餓了會喵喵喵~
func MakeCatHungry(c Cat) {
c.Say()
}
// MakeSheepHungry 羊餓了會咩咩咩~
func MakeSheepHungry(s Sheep) {
s.Say()
}
接下來會有越來越多的小動物跑過來,我們的代碼世界該怎麼拓展呢?
在餓肚子這個場景下,我們可不可以把所有動物都當成一個“會叫的類型”來處理呢?當然可以!使用介面類型就可以實現這個目標。 我們的代碼其實並不關心究竟是什麼動物在叫,我們只是在代碼中調用它的Say()
方法,這就足夠了。
我們可以約定一個Sayer
類型,它必須實現一個Say()
方法,只要餓肚子了,我們就調用Say()
方法。
type Sayer interface {
Say()
}
然後我們定義一個通用的MakeHungry
函數,接收Sayer
類型的參數。
// MakeHungry 餓肚子了...
func MakeHungry(s Sayer) {
s.Say()
}
我們通過使用介面類型,把所有會叫的動物當成Sayer
類型來處理,只要實現了Say()
方法都能當成Sayer
類型的變數來處理。
var c cat
MakeHungry(c)
var d dog
MakeHungry(d)
在電商系統中我們允許用戶使用多種支付方式(支付寶支付、微信支付、銀聯支付等),我們的交易流程中可能不太在乎用戶究竟使用什麼支付方式,只要它能提供一個實現支付功能的Pay
方法讓調用方調用就可以了。
再比如我們需要在某個程式中添加一個將某些指標數據向外輸出的功能,根據不同的需求可能要將數據輸出到終端、寫入到文件或者通過網路連接發送出去。在這個場景下我們可以不關註最終輸出的目的地是什麼,只需要它能提供一個Write
方法讓我們把內容寫入就可以了。
Go語言中為瞭解決類似上面的問題引入了介面的概念,介面類型區別於我們之前章節中介紹的那些具體類型,讓我們專註於該類型提供的方法,而不是類型本身。使用介面類型通常能夠讓我們寫出更加通用和靈活的代碼。
1.3 面向介面編程
PHP、Java等語言中也有介面的概念,不過在PHP和Java語言中需要顯式聲明一個類實現了哪些介面,在Go語言中使用隱式聲明的方式實現介面。只要一個類型實現了介面中規定的所有方法,那麼它就實現了這個介面。
Go語言中的這種設計符合程式開發中抽象的一般規律,例如在下麵的代碼示例中,我們的電商系統最開始只設計了支付寶一種支付方式:
type ZhiFuBao struct {
// 支付寶
}
// Pay 支付寶的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
fmt.Printf("使用支付寶付款:%.2f元。\n", float64(amount/100))
}
// Checkout 結賬
func Checkout(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{})
}
隨著業務的發展,根據用戶需求添加支持微信支付。
type WeChat struct {
// 微信
}
// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}
在實際的交易流程中,我們可以根據用戶選擇的支付方式來決定最終調用支付寶的Pay方法還是微信支付的Pay方法。
// Checkout 支付寶結賬
func CheckoutWithZFB(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
// Checkout 微信支付結賬
func CheckoutWithWX(obj *WeChat) {
// 支付100元
obj.Pay(100)
}
實際上,從上面的代碼示例中我們可以看出,我們其實並不怎麼關心用戶選擇的是什麼支付方式,我們只關心調用Pay方法時能否正常運行。這就是典型的“不關心它是什麼,只關心它能做什麼”的場景。
在這種場景下我們可以將具體的支付方式抽象為一個名為Payer
的介面類型,即任何實現了Pay
方法的都可以稱為Payer
類型。
// Payer 包含支付方法的介面類型
type Payer interface {
Pay(int64)
}
此時只需要修改下原始的Checkout
函數,它接收一個Payer
類型的參數。這樣就能夠在不修改既有函數調用的基礎上,支持新的支付方式。
// Checkout 結賬
func Checkout(obj Payer) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{}) // 之前調用支付寶支付
Checkout(&WeChat{}) // 現在支持使用微信支付
}
像類似的例子在我們編程過程中會經常遇到:
- 比如一個網上商城可能使用支付寶、微信、銀聯等方式去線上支付,我們能不能把它們當成“支付方式”來處理呢?
- 比如三角形,四邊形,圓形都能計算周長和麵積,我們能不能把它們當成“圖形”來處理呢?
- 比如滿減券、立減券、打折券都屬於電商場景下常見的優惠方式,我們能不能把它們當成“優惠券”來處理呢?
介面類型是Go語言提供的一種工具,在實際的編碼過程中是否使用它由你自己決定,但是通常使用介面類型可以使代碼更清晰易讀。
1.4 介面的定義
每個介面類型由任意個方法簽名組成,介面的定義格式如下:
type 介面類型名 interface{
方法名1( 參數列表1 ) 返回值列表1
方法名2( 參數列表2 ) 返回值列表2
…
}
其中:
- 介面類型名:Go語言的介面在命名時,一般會在單詞後面添加
er
,如有寫操作的介面叫Writer
,有關閉操作的介面叫closer
等。介面名最好要能突出該介面的類型含義。 - 方法名:當方法名首字母是大寫且這個介面類型名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的代碼訪問。
- 參數列表、返回值列表:參數列表和返回值列表中的參數變數名可以省略。
下麵是一個典型的介面類型 MyInterface
的定義:
type MyInterface interface {
M1(int) error
M2(io.Writer, ...string)
}
通過這個定義,我們可以看到,介面類型 MyInterface
所表示的介面的方法集合,包含兩個方法 M1
和 M2
。之所以稱 M1
和 M2
為“方法”,更多是從這個介面的實現者的角度考慮的。但從上面介面類型聲明中各個“方法”的形式上來看,這更像是不帶有 func
關鍵字的函數名 + 函數簽名(參數列表 + 返回值列表)的組合。
在介面類型的方法集合中聲明的方法,它的參數列表不需要寫出形參名字,返回值列表也是如此。也就是說,方法的參數列表中形參名字與返回值列表中的具名返回值,都不作為區分兩個方法的憑據。
比如下麵的 MyInterface
介面類型的定義與上面的 MyInterface
介面類型定義都是等價的:
type MyInterface interface {
M1(a int) error
M2(w io.Writer, strs ...string)
}
type MyInterface interface {
M1(n int) error
M2(w io.Writer, args ...string)
}
不過,Go 語言要求介面類型聲明中的方法必須是具名的,並且方法名字在這個介面類型的方法集合中是唯一的。前面我們在學習類型嵌入時就學到過:Go 1.14 版本以後,Go 介面類型允許嵌入的不同介面類型的方法集合存在交集,但前提是交集中的方法不僅名字要一樣,它的方法簽名部分也要保持一致,也就是參數列表與返回值列表也要相同,否則 Go 編譯器照樣會報錯。
比如下麵示例中 Interface3
嵌入了 Interface1
和 Interface2
,但後兩者交集中的 M1
方法的函數簽名不同,導致了編譯出錯:
type Interface1 interface {
M1()
}
type Interface2 interface {
M1(string)
M2()
}
type Interface3 interface{
Interface1
Interface2 // 編譯器報錯:duplicate method M1
M3()
}
上面舉的例子中的方法都是首字母大寫的導出方法,所以在 Go 介面類型的方法集合中放入首字母小寫的非導出方法也是合法的,並且我們在 Go 標準庫中也找到了帶有非導出方法的介面類型定義,比如 context
包中的 canceler
介面類型,它的代碼如下:
// $GOROOT/src/context.go
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
但這樣的例子並不多。通過對標準庫這為數不多的例子,我們可以看到,如果介面類型的方法集合中包含非導出方法,那麼這個介面類型自身通常也是非導出的,它的應用範圍也僅局限於包內。不過,在日常實際編碼過程中,我們極少使用這種帶有非導出方法的介面類型,我們簡單瞭解一下就可以了。
二、空介面
除了上面這種常規情況,還有空介面(empty interface)類型這種特殊情況。
2.1 空介面的定義
空介面是指沒有定義任何方法的介面類型。因此任何類型都可以視為實現了空介面。也正是因為空介面類型的這個特性,空介面類型的變數可以存儲任意類型的值。
比如下麵的 EmptyInterface
介面類型:
type EmptyInterface interface {
}
這個方法集合為空的介面類型就被稱為空介面類型,但通常我們不需要自己顯式定義這類空介面類型,我們直接使用 interface{}
這個類型字面值作為所有空介面類型的代表就可以了。
2.2 空介面的應用
2.2.1 空介面作為函數的參數
空介面(interface{}
)作為函數的參數是一種非常靈活的方式,因為它可以接受任何類型的參數。這在處理未知類型的數據或編寫通用函數時非常有用。以下是一個示例,展示瞭如何使用空介面作為函數參數:
package main
import "fmt"
func PrintValue(value interface{}) {
fmt.Println(value)
}
func main() {
PrintValue(42) // 整數
PrintValue("Hello, Go!") // 字元串
PrintValue(3.14159) // 浮點數
PrintValue([]int{1, 2, 3}) // 切片
}
在上面的示例中,PrintValue
函數接受一個空介面類型的參數,這意味著它可以接受任何類型的值。在 main
函數中,我們調用 PrintValue
函數並傳遞不同類型的參數,它們都可以被正確處理和列印。
2.2.2 空介面作為map的值
空介面也可以用作map
的值類型,這使得map
可以存儲不同類型的值。這在需要將各種類型的數據關聯到特定鍵時非常有用。以下是一個示例:
package main
import "fmt"
func main() {
data := make(map[string]interface{})
data["name"] = "Alice"
data["age"] = 30
data["isStudent"] = false
fmt.Println(data["name"]) // 輸出: Alice
fmt.Println(data["age"]) // 輸出: 30
fmt.Println(data["isStudent"]) // 輸出: false
}
在上面的示例中,我們創建了一個map
,其中值的類型是interface{}
,這意味著map
可以存儲不同類型的值。我們使用字元串鍵將字元串、整數和布爾值關聯到map
中,併在後續通過鍵來訪問這些值。
2.3 介面類型變數
介面類型一旦被定義後,它就和其他 Go 類型一樣可以用於聲明變數,比如:
var err error // err是一個error介面類型的實例變數
var r io.Reader // r是一個io.Reader介面類型的實例變數
這些類型為介面類型的變數被稱為介面類型變數,如果沒有被顯式賦予初值,介面類型變數的預設值為 nil
。如果要為介面類型變數顯式賦予初值,我們就要為介面類型變數選擇合法的右值。
Go 規定
:如果一個類型 T
的方法集合是某介面類型 I
的方法集合的等價集合或超集,我們就說類型 T
實現了介面類型 I
,那麼類型 T
的變數就可以作為合法的右值賦值給介面類型 I
的變數。
如果一個變數的類型是空介面類型,由於空介面類型的方法集合為空,這就意味著任何類型都實現了空介面的方法集合,所以我們可以將任何類型的值作為右值,賦值給空介面類型的變數,比如下麵例子:
var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t // ok
i = &t // ok
空介面類型的這一可接受任意類型變數值作為右值的特性,讓它成為 Go 加入泛型語法之前唯一一種具有“泛型”能力的語法元素,包括 Go 標準庫在內的一些通用數據結構與演算法的實現,都使用了空類型
interface{}
作為數據元素的類型,這樣我們就無需為每種支持的元素類型單獨做一份代碼拷貝了。
2.4 類型斷言
Go 語言還支持介面類型變數賦值的“逆操作”,也就是通過介面類型變數“還原”它的右值的類型與值信息,這個過程被稱為“類型斷言(Type Assertion
)”。類型斷言通常使用下麵的語法形式:
v, ok := i.(T)
其中 i
是某一個介面類型變數,如果 T
是一個非介面類型且 T
是想要還原的類型,那麼這句代碼的含義就是斷言存儲在介面類型變數 i
中的值的類型為 T
。
如果介面類型變數 i
之前被賦予的值確為 T
類型的值,那麼這個語句執行後,左側“comma, ok
”語句中的變數 ok
的值將為 true
,變數 v
的類型為 T
,它的值會是之前變數 i
的右值。如果 i
之前被賦予的值不是 T
類型的值,那麼這個語句執行後,變數 ok
的值為 false
,變數 v
的類型還是那個要還原的類型,但它的值是類型 T
的零值。
類型斷言也支持下麵這種語法形式:
v := i.(T)
但在這種形式下,一旦介面變數 i
之前被賦予的值不是 T
類型的值,那麼這個語句將拋出 panic
。如果變數 i
被賦予的值是 T
類型的值,那麼變數 v
的類型為 T
,它的值就會是之前變數 i
的右值。由於可能出現 panic
,所以我們並不推薦使用這種類型斷言的語法形式。
為了加深你的理解,接下來我們通過一個例子來直觀看一下類型斷言的語義:
var a int64 = 13
var i interface{} = a
v1, ok := i.(int64)
fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) // v1=13, the type of v1 is int64, ok=true
v2, ok := i.(string)
fmt.Printf("v2=%s, the type of v2 is %T, ok=%t\n", v2, v2, ok) // v2=, the type of v2 is string, ok=false
v3 := i.(int64)
fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64
v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("the type of v4 is %T\n", v4)
你可以看到,這個例子的輸出結果與我們之前講解的是一致的。
在這段代碼中,如果 v, ok := i.(T)
中的 T
是一個介面類型,那麼類型斷言的語義就會變成:斷言 i
的值實現了介面類型 T
。如果斷言成功,變數 v
的類型為 i
的值的類型,而並非介面類型 T
。如果斷言失敗,v
的類型信息為介面類型 T
,它的值為 nil
,下麵我們再來看一個 T
為介面類型的示例:
type MyInterface interface {
M1()
}
type T int
func (T) M1() {
println("T's M1")
}
func main() {
var t T
var i interface{} = t
v1, ok := i.(MyInterface)
if !ok {
panic("the value of i is not MyInterface")
}
v1.M1()
fmt.Printf("the type of v1 is %T\n", v1) // the type of v1 is main.T
i = int64(13)
v2, ok := i.(MyInterface)
fmt.Printf("the type of v2 is %T\n", v2) // the type of v2 is <nil>
// v2 = 13 // cannot use 1 (type int) as type MyInterface in assignment: int does not implement MyInterface (missing M1 method)
}
我們看到,通過the type of v2 is <nil>
,我們其實是看不出斷言失敗後的變數 v2
的類型的,但通過最後一行代碼的編譯器錯誤提示,我們能清晰地看到 v2
的類型信息為 MyInterface
。
其實,介面類型的類型斷言還有一個變種,那就是 type switch ,這個你可以去看看【go 流程式控制制之switch 語句介紹】。
三、儘量定義“小介面”
3.1 “小介面”介紹
介面類型的背後,是通過把類型的行為抽象成契約,建立雙方共同遵守的約定,這種契約將雙方的耦合降到了最低的程度。和生活工作中的契約有繁有簡,簽署方式多樣一樣,代碼間的契約也有多有少,有大有小,而且達成契約的方式也有所不同。 而 Go 選擇了去繁就簡的形式,這主要體現在以下兩點上:
- 隱式契約,無需簽署,自動生效:Go 語言中介面類型與它的實現者之間的關係是隱式的,不需要像其他語言(比如 Java)那樣要求實現者顯式放置“implements”進行修飾,實現者只需要實現介面方法集合中的全部方法便算是遵守了契約,並立即生效了。
- 更傾向於“小契約”:這點也不難理解。你想,如果契約太繁雜了就會束縛了手腳,缺少了靈活性,抑制了表現力。所以 Go 選擇了使用“小契約”,表現在代碼上就是儘量定義小介面,即方法個數在 1~3 個之間的介面。Go 語言之父 Rob Pike 曾說過的“介面越大,抽象程度越弱”,這也是 Go 社區傾向定義小介面的另外一種表述。
Go 對小介面的青睞在它的標準庫中體現得淋漓盡致,這裡我給出了標準庫中一些我們日常開發中常用的介面的定義:
// $GOROOT/src/builtin/builtin.go
type error interface {
Error() string
}
// $GOROOT/src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
// $GOROOT/src/net/http/server.go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
我們看到,上述這些介面的方法數量在 1~3 個之間,這種“小介面”的 Go 慣例也已經被 Go 社區項目廣泛採用。我統計了早期版本的 Go 標準庫(Go 1.13 版本)、Docker 項目(Docker 19.03 版本)以及 Kubernetes 項目(Kubernetes 1.17 版本)中定義的介面類型方法集合中方法數量,你可以看下:
從圖中我們可以看到,無論是 Go 標準庫,還是 Go 社區知名項目,它們基本都遵循了“儘量定義小介面”的慣例,介面方法數量在 1~3 範圍內的介面占了絕大多數。那麼在編碼層面,小介面究竟有哪些優勢呢?
3.2 小介面優勢
3.2.1 第一點:介面越小,抽象程度越高
電腦程式本身就是對真實世界的抽象與再建構。抽象就是對同類事物去除它具體的、次要的方面,抽取它相同的、主要的方面。不同的抽象程度,會導致抽象出的概念對應的事物的集合不同。抽象程度越高,對應的集合空間就越大;抽象程度越低,也就是越具像化,更接近事物真實面貌,對應的集合空間越小。
我們舉一個生活中的簡單例子。你可以看下這張示意圖,它是對生活中不同抽象程度的形象詮釋:
這張圖中我們分別建立了三個抽象:
- 會飛的。這個抽象對應的事物集合包括:蝴蝶、蜜蜂、麻雀、天鵝、鴛鴦、海鷗和信天翁;
- 會游泳的。它對應的事物集合包括:鴨子、海豚、人類、天鵝、鴛鴦、海鷗和信天翁;
- 會飛且會游泳的。這個抽象對應的事物集合包括:天鵝、鴛鴦、海鷗和信天翁。
我們看到,“會飛的”、“會游泳的”這兩個抽象對應的事物集合,要大於“會飛且會游泳的”所對應的事物集合空間,也就是說“會飛的”、“會游泳的”這兩個抽象程度更高。
我們將上面的抽象轉換為 Go 代碼看看:
// 會飛的
type Flyable interface {
Fly()
}
// 會游泳的
type Swimable interface {
Swim()
}
// 會飛且會游泳的
type FlySwimable interface {
Flyable
Swimable
}
我們用上述定義的介面替換上圖中的抽象,再得到這張示意圖:
我們可以直觀地看到,這張圖中的 Flyable
只有一個 Fly
方法,FlySwimable
則包含兩個方法 Fly
和 Swim
。我們看到,具有更少方法的 Flyable
的抽象程度相對於 FlySwimable
要高,包含的事物集合(7 種動物)也要比 FlySwimable
的事物集合(4 種動物)大。也就是說,介面越小(介面方法少),抽象程度越高,對應的事物集合越大。
而這種情況的極限恰恰就是無方法的空介面 interface{}
,空介面的這個抽象對應的事物集合空間包含了 Go 語言世界的所有事物。
3.2.2 第二點:小介面易於實現和測試
Go 推崇通過組合的方式構建程式。Go 開發人員一般會嘗試通過嵌入其他已有介面類型的方式來構建新介面類型,就像通過嵌入 io.Reader 和 io.Writer 構建 io.ReadWriter 那樣。
那構建時,如果有眾多候選介面類型供我們選擇,我們會怎麼選擇呢?
顯然,我們會選擇那些新介面類型需要的契約職責,同時也要求不要引入我們不需要的契約職責。在這樣的情況下,擁有單一或少數方法的小介面便更有可能成為我們的目標,而那些擁有較多方法的大介面,可能會因引入了諸多不需要的契約職責而被放棄。由此可見,小介面更契合 Go 的組合思想,也更容易發揮出組合的威力。
四、定義小介面,可以遵循的幾點
保持簡單有時候比複雜更難。小介面雖好,但如何定義出小介面是擺在所有 Gopher 面前的一道難題。這道題沒有標準答案,但有一些點可供我們在實踐中考量遵循。
4.1 首先,別管介面大小,先抽象出介面
要設計和定義出小介面,前提是需要先有介面。
Go 語言還比較年輕,它的設計哲學和推崇的編程理念可能還沒被廣大 Gopher 100% 理解、接納和應用於實踐當中,尤其是 Go 所推崇的基於介面的組合思想。
儘管介面不是 Go 獨有的,但專註於介面是編寫強大而靈活的 Go 代碼的關鍵。因此,在定義小介面之前,我們需要先針對問題領域進行深入理解,聚焦抽象併發現介面,就像下圖所展示的那樣,先針對領域對象的行為進行抽象,形成一個介面集合:
初期,我們先不要介意這個介面集合中方法的數量,因為對問題域的理解是循序漸進的,在第一版代碼中直接定義出小介面可能並不現實。而且,標準庫中的 io.Reader
和 io.Writer
也不是在 Go 剛誕生時就有的,而是在發現對網路、文件、其他位元組數據處理的實現十分相似之後才抽象出來的。並且越偏向業務層,抽象難度就越高,這或許也是前面圖中 Go 標準庫小介面(1~3 個方法)占比略高於 Docker 和 Kubernetes 的原因。
4.2 第二,將大介面拆分為小介面
有了介面後,我們就會看到介面被用在了代碼的各個地方。一段時間後,我們就來分析哪些場合使用了介面的哪些方法,是否可以將這些場合使用的介面的方法提取出來,放入一個新的小介面中,就像下麵圖示中的那樣:
這張圖中的大介面 1 定義了多個方法,一段時間後,我們發現方法 1 和方法 2 經常用在場合 1 中,方法 3 和方法 4 經常用在場合 2 中,方法 5 和方法 6 經常用在場合 3 中,大介面 1 的方法呈現出一種按業務邏輯自然分組的狀態。
這個時候我們可以將這三組方法分別提取出來放入三個小介面中,也就是將大介面 1 拆分為三個小介面 A、B 和 C。拆分後,原應用場合 1~3 使用介面 1 的地方就可以無縫替換為介面 A、B、C 了。
4.3 最後,我們要註意介面的單一契約職責
那麼,上面已經被拆分成的小介面是否需要進一步拆分,直至每個介面都只有一個方法呢?這個依然沒有標準答案,不過你依然可以考量一下現有小介面是否需要滿足單一契約職責,就像 io.Reader
那樣。如果需要,就可以進一步拆分,提升抽象程度。