Go 介面-契約介紹

来源:https://www.cnblogs.com/taoxiaoxin/archive/2023/11/06/17814068.html
-Advertisement-
Play Games

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 介面類型介紹

介面是一種抽象類型,它定義了一組方法的契約,它規定了需要實現的所有方法。是由 typeinterface 關鍵字定義的一組方法集合,其中,方法集合唯一確定了這個介面類型所表示的介面。

一個介面類型通常由一組方法簽名組成,這些方法定義了對象必須實現的操作。介面的方法簽名包括方法的名稱、輸入參數、返回值等信息,但不包括方法的實際實現。例如:

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 所表示的介面的方法集合,包含兩個方法 M1M2之所以稱 M1M2 為“方法”,更多是從這個介面的實現者的角度考慮的。但從上面介面類型聲明中各個“方法”的形式上來看,這更像是不帶有 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 嵌入了 Interface1Interface2,但後兩者交集中的 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 版本)中定義的介面類型方法集合中方法數量,你可以看下:

img

從圖中我們可以看到,無論是 Go 標準庫,還是 Go 社區知名項目,它們基本都遵循了“儘量定義小介面”的慣例,介面方法數量在 1~3 範圍內的介面占了絕大多數。那麼在編碼層面,小介面究竟有哪些優勢呢?

3.2 小介面優勢

3.2.1 第一點:介面越小,抽象程度越高

電腦程式本身就是對真實世界的抽象與再建構。抽象就是對同類事物去除它具體的、次要的方面,抽取它相同的、主要的方面。不同的抽象程度,會導致抽象出的概念對應的事物的集合不同。抽象程度越高,對應的集合空間就越大;抽象程度越低,也就是越具像化,更接近事物真實面貌,對應的集合空間越小。

我們舉一個生活中的簡單例子。你可以看下這張示意圖,它是對生活中不同抽象程度的形象詮釋:

img

這張圖中我們分別建立了三個抽象:

  • 會飛的。這個抽象對應的事物集合包括:蝴蝶、蜜蜂、麻雀、天鵝、鴛鴦、海鷗和信天翁;
  • 會游泳的。它對應的事物集合包括:鴨子、海豚、人類、天鵝、鴛鴦、海鷗和信天翁;
  • 會飛且會游泳的。這個抽象對應的事物集合包括:天鵝、鴛鴦、海鷗和信天翁。

我們看到,“會飛的”、“會游泳的”這兩個抽象對應的事物集合,要大於“會飛且會游泳的”所對應的事物集合空間,也就是說“會飛的”、“會游泳的”這兩個抽象程度更高。

我們將上面的抽象轉換為 Go 代碼看看:

// 會飛的
type Flyable interface {
  Fly()
}

// 會游泳的
type Swimable interface {
  Swim()
}

// 會飛且會游泳的
type FlySwimable interface {
  Flyable
  Swimable
}

我們用上述定義的介面替換上圖中的抽象,再得到這張示意圖:

img

我們可以直觀地看到,這張圖中的 Flyable 只有一個 Fly 方法,FlySwimable 則包含兩個方法 FlySwim。我們看到,具有更少方法的 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 代碼的關鍵。因此,在定義小介面之前,我們需要先針對問題領域進行深入理解,聚焦抽象併發現介面,就像下圖所展示的那樣,先針對領域對象的行為進行抽象,形成一個介面集合:

WechatIMG267

初期,我們先不要介意這個介面集合中方法的數量,因為對問題域的理解是循序漸進的,在第一版代碼中直接定義出小介面可能並不現實。而且,標準庫中的 io.Readerio.Writer 也不是在 Go 剛誕生時就有的,而是在發現對網路、文件、其他位元組數據處理的實現十分相似之後才抽象出來的。並且越偏向業務層,抽象難度就越高,這或許也是前面圖中 Go 標準庫小介面(1~3 個方法)占比略高於 Docker 和 Kubernetes 的原因。

4.2 第二,將大介面拆分為小介面

有了介面後,我們就會看到介面被用在了代碼的各個地方。一段時間後,我們就來分析哪些場合使用了介面的哪些方法,是否可以將這些場合使用的介面的方法提取出來,放入一個新的小介面中,就像下麵圖示中的那樣:

WechatIMG268

這張圖中的大介面 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 那樣。如果需要,就可以進一步拆分,提升抽象程度。

分享是一種快樂,開心是一種態度!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 進行支付寶開發的第一步就是:配置密鑰。 但是有很多小伙伴都不知道怎麼配置,這篇文章將手把手幫你從頭開始捋清如何配置支付寶密鑰~ ...
  • 在"zookeeper源碼(03)集群啟動流程"中介紹了leader選舉的入口,本文將詳細分析leader選舉組件和流程。 leader選舉流程(重要) quorumPeer的start階段使用startLeaderElection()方法啟動選舉 LOOKING狀態,投自己一票 createEle ...
  • 準備 我是小C同學編寫得一個java文件,如何實現我的功能呢?需要去JVM(Java Virtual Machine)這個地方旅行。 變身 我高高興興的來到JVM,想要開始JVM之旅,它確說:“現在的我還不能進去,需要做一次轉換,生成class文件才行”。為什麼這樣呢? JVM不能直接載入java文 ...
  • 配置文件yml # phantomjs的位置地址 phantomjs: binPath: windows: binPath-win linux: binPath-linux jsPath: windows: jsPath-win linux: jsPath-linux imagePath: wind ...
  • 定時任務簡介 定時任務是指按照預定的時間間隔或特定時間點自動執行的計劃任務或操作。這些任務通常用於自動化重覆性的工作,以減輕人工操作的負擔,提高效率。在電腦編程和應用程式開發中,定時任務是一種常見的編程模式,用於周期性地執行某些操作、處理數據或觸發事件。 以下是一些關於定時任務的重要概念: 時間間 ...
  • 插值運算是一種數據處理方法,主要用來填補數據之間的空白或缺失值。因為在實際應用中,數據往往不是完整的,而是存在著空白或缺失值,這些空白或缺失值可能是由於數據採集困難、數據丟失或數據處理錯誤等原因造成的。如果直接使用這些空白或缺失值進行分析和預測,將會對結果造成很大的影響。 插值運算可以用來填補這些空 ...
  • 是極致魅惑、灑脫自由的Java heap space?是知性柔情、溫婉大氣的GC overhead limit exceeded?是純真無邪、活潑可愛的Metaspace?如果以上不是你的菜,那還有……刁蠻任性,無跡可尋的CodeCache!性感火辣、心思細膩的Direct Memory高貴冷艷,獨... ...
  • JMM 請你談談對Volatile的理解 Volatile是java虛擬機提供的輕量級的同步機制 1、保證可見性 2、不保證原子性 3、禁止指令重排 什麼是JMM JVM->java虛擬機 JMM->java記憶體模型,不存在的東西,概念!約定 關於JMM的一些同步的約定: 線程解鎖前,必須把共用變數 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...