詳解golang中接口使用的最佳時(shí)機(jī)
1. 引言
接口在系統(tǒng)設(shè)計(jì)中,以及代碼重構(gòu)優(yōu)化中,是一個(gè)不可或缺的工具,能夠幫助我們寫(xiě)出可擴(kuò)展,可維護(hù)性更強(qiáng)的程序。
在本文,我們將介紹什么是接口,在此基礎(chǔ)上,通過(guò)一個(gè)例子來(lái)介紹接口的優(yōu)點(diǎn)。但是接口也不是任何場(chǎng)景都可以隨意使用的,我們會(huì)介紹接口使用的常見(jiàn)場(chǎng)景,同時(shí)也介紹了接口濫用可能帶來(lái)的問(wèn)題,以及一些接口濫用的特征,幫助我們及早發(fā)現(xiàn)接口濫用的情況。
2. 什么是接口
接口是一種工具,在識(shí)別出系統(tǒng)中變化部分時(shí),幫助從系統(tǒng)模塊中抽取出變化的部分,從而保證系統(tǒng)的穩(wěn)定性,可維護(hù)性和可擴(kuò)展性。接口充當(dāng)了一種契約或規(guī)范,規(guī)定了類或模塊應(yīng)該提供的方法和行為,而不關(guān)心具體的實(shí)現(xiàn)細(xì)節(jié)。
接口通常用于面向?qū)ο缶幊陶Z(yǔ)言中,如 Java 和 Go 等。在這些語(yǔ)言中,類可以實(shí)現(xiàn)一個(gè)或多個(gè)接口,并提供接口定義的方法的具體實(shí)現(xiàn)。通過(guò)使用接口,我們可以編寫(xiě)更靈活、可維護(hù)和可擴(kuò)展的代碼,同時(shí)將系統(tǒng)中的變化隔離開(kāi)來(lái)。
接口的實(shí)現(xiàn)在不同的編程語(yǔ)言中可能會(huì)有所不同。以下簡(jiǎn)單展示接口在Java 和 Go 語(yǔ)言中的示例。在Go 語(yǔ)言中,接口是一組方法簽名的集合。實(shí)現(xiàn)接口時(shí),類不需要顯式聲明實(shí)現(xiàn)了哪個(gè)接口,只要一個(gè)類型實(shí)現(xiàn)了接口中的所有方法,就被視為實(shí)現(xiàn)了該接口。
// 定義一個(gè)接口
type Shape interface {
Area() float64
Perimeter() float64
}
// 實(shí)現(xiàn)接口的類型
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}在Java 語(yǔ)言中,接口使用 interface 定義,同時(shí)包含所有的方法簽名。類需要通過(guò)使用 implements 關(guān)鍵字來(lái)實(shí)現(xiàn)接口,并提供接口中定義的方法的具體實(shí)現(xiàn)。
// 定義一個(gè)接口
interface Shape {
double area();
double perimeter();
}
// 實(shí)現(xiàn)接口的類
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}上面示例展示了Java 和 Go語(yǔ)言中接口的定義方式以及接口的實(shí)現(xiàn)方式,雖然具體實(shí)現(xiàn)方式各不相同,但它們都遵循了相似的概念,接口用于定義規(guī)范和契約,實(shí)現(xiàn)類則提供方法的具體實(shí)現(xiàn)來(lái)滿足接口的要求。
3. 接口的優(yōu)點(diǎn)
在識(shí)別出系統(tǒng)變化的部分后,接口能夠幫助我們將系統(tǒng)中變化的部分抽取出來(lái),基于此能夠降低了模塊間的耦合度,能夠提高代碼的可維護(hù)性和代碼的模塊化程度,有助于創(chuàng)建更靈活、可擴(kuò)展和易于維護(hù)的代碼。下面我們通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)進(jìn)行說(shuō)明,詳細(xì)討論這些好處。
3.1 初始需求
假設(shè)我們?cè)跇?gòu)建一個(gè)商城系統(tǒng),其中一個(gè)相對(duì)復(fù)雜且重要的模塊為商品價(jià)格的計(jì)算,計(jì)算購(gòu)物車中各種商品的總價(jià)格。價(jià)格計(jì)算過(guò)程相對(duì)復(fù)雜,包括了基礎(chǔ)價(jià)格、折扣、運(yùn)費(fèi)的計(jì)算,然后每一塊內(nèi)容都會(huì)有比較復(fù)雜的業(yè)務(wù)邏輯。
基于此設(shè)計(jì)了OrderProcessor結(jié)構(gòu)體,其中的CalculateTotalPrice 實(shí)現(xiàn)商品價(jià)格的計(jì)算,設(shè)計(jì)了ShippingCalculator 來(lái)計(jì)算運(yùn)費(fèi),同時(shí)還設(shè)計(jì)DiscountCalculator 來(lái)計(jì)算商品的折扣信息,通過(guò)這幾部分的交互配合,共同來(lái)完成商家價(jià)格的計(jì)算。

下面我們通過(guò)一段代碼來(lái)展示上面的計(jì)算流程:
type OrderProcessor struct {
discountCalculator DiscountCalculator
taxCalculator TaxCalculator
}
// 計(jì)算總價(jià)格
func (tpc OrderProcessor) CalculateTotalPrice(products []Product) float64 {
total := 0.0
for _, item := range cart {
// 獲取商品的基礎(chǔ)價(jià)格
basePrice := item.BasePrice
// 獲取適用于商品的折扣
discount := tpc.discountCalculator.CalculateDiscount(item)
// 計(jì)算運(yùn)費(fèi)
shippingCost := tpc.shippingCalculator.CalculateShippingCost(item)
// 計(jì)算商品的最終價(jià)格(基礎(chǔ)價(jià)格 - 折扣 + 稅費(fèi) + 運(yùn)費(fèi))
finalPrice := basePrice - discount + shippingCost
total += finalPrice
}
return total
}
// 運(yùn)費(fèi)計(jì)算
type ShippingCalculator struct {}
func (sc ShippingCalculator) CalculateShippingCost(product Product) float64 {
return 0.0
}
// 折扣計(jì)算
type DiscountCalculator struct {}
func (dc DiscountCalculator) CalculateDiscount(product Product) float64 {
return 0.0
}如果這里需求沒(méi)有發(fā)生變化,這個(gè)流程可以很好得運(yùn)轉(zhuǎn)下去。假設(shè)這里需要根據(jù)商品的類型來(lái)應(yīng)用不同的折扣,之后要怎么支持呢,可以對(duì)變化的部分抽取出一個(gè)接口,也可以不抽取,都可以支持,我們比較一下沒(méi)有使用接口和使用接口的兩種實(shí)現(xiàn)方式的區(qū)別。
3.2 不抽象接口
首先是不使用接口的實(shí)現(xiàn),這里我們直接在DiscountCalculator 中疊加邏輯,支持不同類型商品的折扣:
type DiscountCalculator struct{}
func (dc DiscountCalculator) CalculateDiscount(product Product) float64 {
// 根據(jù)商品類型應(yīng)用不同的折扣邏輯
switch product.Type {
case "TypeA":
return dc.calculateTypeADiscount(product)
case "TypeB":
return dc.calculateTypeBDiscount(product)
default:
return dc.calculateDefaultDiscount(product)
}
}
func (dc DiscountCalculator) calculateTypeADiscount(product Product) float64 {
// 計(jì)算 TypeA 商品的折扣
return product.BasePrice * 0.1 // 例如,假設(shè) TypeA 商品有 10% 的折扣
}
func (dc DiscountCalculator) calculateTypeBDiscount(product Product) float64 {
// 計(jì)算 TypeB 商品的折扣
return product.BasePrice * 0.15 // 例如,假設(shè) TypeB 商品有 15% 的折扣
}
func (dc DiscountCalculator) calculateDefaultDiscount(product Product) float64 {
// 默認(rèn)折扣邏輯,如果商品類型未匹配到其他情況
return product.BasePrice // 默認(rèn)不打折
}在這里,我們計(jì)算商品折扣,直接使用DiscountCalculator 來(lái)實(shí)現(xiàn),根據(jù)商品的類型應(yīng)用不同的折扣邏輯。這里使用了 switch 語(yǔ)句來(lái)確定應(yīng)該應(yīng)用哪種折扣。這種實(shí)現(xiàn)方式雖然在一個(gè)類中處理了所有的邏輯,但它可能會(huì)導(dǎo)致 DiscountCalculator 類變得龐大且難以維護(hù),特別是當(dāng)折扣邏輯變得更加復(fù)雜或需要頻繁更改時(shí)。
3.3 抽象接口
下面我們給出一個(gè)使用接口的實(shí)現(xiàn),將不同的折扣邏輯封裝到不同的實(shí)現(xiàn)中,以下是使用接口的示例實(shí)現(xiàn):
type OrderProcessor struct {
// 計(jì)算商品價(jià)格,直接依賴接口
discountCalculator DiscountCalculatorInterface
taxCalculator TaxCalculator
shippingCalculator ShippingCalculator
}
// 定義折扣計(jì)算器接口
type DiscountCalculatorInterface interface {
CalculateDiscount(product Product) float64
}
// 定義一個(gè)具體的折扣計(jì)算器實(shí)現(xiàn)
type TypeADiscountCalculator struct{}
func (dc TypeADiscountCalculator) CalculateDiscount(product Product) float64 {
// 計(jì)算 TypeA 商品的折扣
return product.BasePrice * 0.1 // 例如,假設(shè) TypeA 商品有 10% 的折扣
}
// 定義另一個(gè)具體的折扣計(jì)算器實(shí)現(xiàn)
type TypeBDiscountCalculator struct{}
func (dc TypeBDiscountCalculator) CalculateDiscount(product Product) float64 {
// 計(jì)算 TypeB 商品的折扣
return product.BasePrice * 0.15 // 例如,假設(shè) TypeB 商品有 15% 的折扣
}上述示例中,我們定義了一個(gè) DiscountCalculatorInterface 接口以及兩個(gè)不同的折扣計(jì)算器實(shí)現(xiàn):TypeADiscountCalculator 和 TypeBDiscountCalculator。 OrderProcessorWithInterface 結(jié)構(gòu)體依賴于 DiscountCalculatorInterface 接口,這使得我們可以根據(jù)商品的類型輕松切換不同的折扣策略。
3.4 實(shí)現(xiàn)對(duì)比
下面我們通過(guò)比較上面兩種實(shí)現(xiàn),探討在識(shí)別出系統(tǒng)的變化后,讓系統(tǒng)依賴一個(gè)接口,相對(duì)于依賴一個(gè)具體類的優(yōu)點(diǎn)。
首先是對(duì)于系統(tǒng)的可擴(kuò)展性,假設(shè)現(xiàn)在需要支持新的類型的折扣,如果引入了接口,只需實(shí)現(xiàn)新的折扣計(jì)算器并滿足相同的接口要求,就可以完成預(yù)期的功能。如果我們還是依賴一個(gè)具體的類,此時(shí)要么在DiscountCalculator 中通過(guò)if...else 疊加業(yè)務(wù)邏輯,相對(duì)于接口的引入,代碼的可擴(kuò)展性相比接口的使用就大大降低了。
對(duì)于系統(tǒng)的可測(cè)試性,如果是定義了接口,我們不需要驗(yàn)證其他DiscountCalculator 的實(shí)現(xiàn),只需要驗(yàn)證當(dāng)前新增的處理器即可。如果是依賴一個(gè)具體的類,此時(shí)如果進(jìn)行測(cè)試,就需要對(duì)所有分支進(jìn)行覆蓋,很容易疏漏。其次,我們也可以輕松模擬不同的折扣計(jì)算器實(shí)現(xiàn),驗(yàn)證 OrderProcessor 的行為。
還有代碼可讀性和可維護(hù)性,接口提供了一種清晰的契約,我們可以將DiscountCalculator當(dāng)作一個(gè)小的模塊,OrderProcessor通過(guò)接口與該模塊進(jìn)行交互,這使得代碼更易于理解和維護(hù),因?yàn)榻涌诔洚?dāng)了文檔,明確了每個(gè)模塊的預(yù)期行為。
最后,通過(guò)接口的定義,OrderProcessor將不再依賴具體的類,而是依賴一個(gè)抽象層,降低了系統(tǒng)的耦合度,不再需要關(guān)注折扣的計(jì)算,讓折扣的計(jì)算變得更加靈活。
通過(guò)以上的討論,我們認(rèn)為如果識(shí)別出了系統(tǒng)的變化后,該模塊可能存在多個(gè)不同方向的變化,應(yīng)該盡量抽取出一個(gè)接口,這樣能夠提高系統(tǒng)的可擴(kuò)展性,可測(cè)試性,代碼的可讀性以及可維護(hù)性都有一定程度的提高。
4. 何時(shí)使用接口
接口可以給我們帶來(lái)一系列的優(yōu)點(diǎn),如松耦合,隔絕變化,提高代碼的可擴(kuò)展性等,但是濫用接口的話,反而會(huì)引入不必要的復(fù)雜性,并增加代碼的理解和維護(hù)成本。
有一個(gè)核心的準(zhǔn)則,盡量支持依賴具體的類,而不是抽取接口,不要為了使用接口而創(chuàng)造不必要的抽象,這可能會(huì)使代碼變得混亂和難以理解。
如果真的使用接口,應(yīng)該確定其在系統(tǒng)設(shè)計(jì)中起到促進(jìn)松耦合和可維護(hù)性的作用,而不是增加復(fù)雜性。要在合適的場(chǎng)景下使用接口,并考慮接口設(shè)計(jì)的清晰性和可維護(hù)性。下面基于此,我們討論一些接口可能適用的場(chǎng)景。
4.1 系統(tǒng)中存在變化部分
系統(tǒng)中存在變化的部分是使用接口的最核心場(chǎng)景之一 。 使用接口可以將這些變化部分從系統(tǒng)的其他部分隔離開(kāi)來(lái),使系統(tǒng)更具靈活性和可維護(hù)性。這種設(shè)計(jì)允許我們將變化的部分抽取為一個(gè)單獨(dú)的模塊,在變化時(shí),只需要對(duì)該模塊進(jìn)行修改,而不必修改整個(gè)系統(tǒng)。接口充當(dāng)了變化部分的契約,使不同的實(shí)現(xiàn)可以輕松地替換或添加,從而適應(yīng)新的需求或變化的情況。
比如系統(tǒng)需要向用戶發(fā)送郵件,可能不同的運(yùn)營(yíng)商提供了不同的API,然后我們系統(tǒng)中需要支持多個(gè)不同的運(yùn)營(yíng)商,在不同場(chǎng)景下使用不同運(yùn)營(yíng)商的接口。
此時(shí)我們通過(guò)定義接口,系統(tǒng)通過(guò)與該接口進(jìn)行交互即可,而不需要關(guān)心底層的實(shí)現(xiàn)細(xì)節(jié)。如果將來(lái)要添加新的郵件服務(wù)提供商,只需創(chuàng)建一個(gè)新的類并實(shí)現(xiàn)接口即可,而不需要修改現(xiàn)有的代碼。
這種方式使系統(tǒng)的變化部分與其余部分隔離開(kāi)來(lái),提高了系統(tǒng)的可維護(hù)性和可擴(kuò)展性。此外,通過(guò)使用接口,我們可以創(chuàng)建模擬郵件發(fā)送器來(lái)驗(yàn)證系統(tǒng)的行為,更容易進(jìn)行單元測(cè)試。
4.2 類庫(kù)的可配置性
類庫(kù)對(duì)外擴(kuò)展和提供可配置性也是接口使用的重要場(chǎng)景之一。當(dāng)開(kāi)發(fā)一個(gè)類庫(kù)或框架時(shí),為了讓用戶能夠輕松地?cái)U(kuò)展和自定義其行為,可以通過(guò)接口提供一組可配置的擴(kuò)展點(diǎn)。這些擴(kuò)展點(diǎn)允許用戶提供自己的實(shí)現(xiàn),以適應(yīng)其特定需求。
舉例來(lái)說(shuō),一個(gè)日志庫(kù)可以定義一個(gè)接口 Logger,并允許用戶提供他們自己的 Logger 實(shí)現(xiàn)。用戶可以選擇使用默認(rèn)的日志記錄實(shí)現(xiàn),也可以創(chuàng)建一個(gè)自定義的實(shí)現(xiàn),以將日志信息發(fā)送到不同的地方(例如文件、數(shù)據(jù)庫(kù)、遠(yuǎn)程服務(wù)器等)。這種可配置性使用戶能夠根據(jù)其項(xiàng)目的要求自由選擇和調(diào)整庫(kù)的行為。
通過(guò)提供接口和可配置性,類庫(kù)或框架可以更具通用性和靈活性,使用戶能夠根據(jù)其特定的用例和需求來(lái)定制和擴(kuò)展庫(kù)的功能,從而提高了庫(kù)的可用性和適用性。這種模塊化的設(shè)計(jì)方式有助于減少代碼的重復(fù),促進(jìn)了代碼的復(fù)用,同時(shí)也提供了更好的可擴(kuò)展性和可維護(hù)性。
4.3 模塊間的交互
系統(tǒng)劃分不同模塊并使用接口來(lái)進(jìn)行交互也是一個(gè)重要的場(chǎng)景。當(dāng)將系統(tǒng)劃分為不同的模塊或組件時(shí),使用接口定義模塊之間的契約和互動(dòng)方式是一種良好的實(shí)踐。每個(gè)模塊可以實(shí)現(xiàn)所需的接口,并與其他模塊進(jìn)行交互,這使得模塊之間的界限更加清晰,易于理解和維護(hù)。
使用接口可以降低模塊之間的耦合度。這意味著每個(gè)模塊不需要關(guān)心其他模塊的具體實(shí)現(xiàn)細(xì)節(jié),只需要遵循接口定義的契約。這種模塊化的設(shè)計(jì)方式有助于將復(fù)雜的系統(tǒng)拆分為更小、更易管理的部分,并降低了系統(tǒng)開(kāi)發(fā)和維護(hù)的復(fù)雜性。
4.4 單元測(cè)試的使用
在需要解除一個(gè)龐大的外部系統(tǒng)的依賴時(shí)。有時(shí)候我們并不是需要多個(gè)選擇,而是某個(gè)外部依賴過(guò)重,我們測(cè)試或其他場(chǎng)景可能會(huì)選擇 mock 一個(gè)外部依賴,以便降低測(cè)試系統(tǒng)的依賴。
比如依賴多個(gè)外部rpc,單元測(cè)試時(shí)需要屏蔽外部的依賴,此時(shí)就比較有必要使用接口,通過(guò)框架生成一個(gè)mock的實(shí)現(xiàn),從而解除對(duì)外部的依賴。
5. 潛在的誤用和濫用
5.1 接口濫用帶來(lái)的問(wèn)題
雖然接口在合適的場(chǎng)景中非常有用,但濫用接口可能會(huì)導(dǎo)致代碼變得復(fù)雜、難以理解和難以維護(hù)。引入過(guò)多的接口可能會(huì)增加系統(tǒng)的復(fù)雜性,使代碼難以理解。每個(gè)接口都需要額外的抽象和實(shí)現(xiàn),這可能不是必要的。其次使用接口有時(shí)會(huì)引入額外的性能開(kāi)銷,因?yàn)檫\(yùn)行時(shí)需要進(jìn)行接口解析。在性能敏感的應(yīng)用中,這可能是一個(gè)問(wèn)題。
最重要的一個(gè)問(wèn)題,接口的目標(biāo)是提供一種通用的抽象,給系統(tǒng)提供可配置項(xiàng),但有時(shí)候過(guò)度一般化可能會(huì)導(dǎo)致不必要的復(fù)雜性。在某些情況下,直接使用具體的類可能更加簡(jiǎn)單和清晰。
我們應(yīng)該在確保接口是必要的情況下使用它們,以避免不必要的復(fù)雜性和耦合。接口的設(shè)計(jì)應(yīng)該基于真正的需求和系統(tǒng)架構(gòu),而不是僅僅為了使用接口而使用接口。
5.2 如何識(shí)別接口是否濫用
對(duì)于識(shí)別接口是否濫用,可以通過(guò)下面幾個(gè)方面來(lái)檢查,如果滿足了下面的某一個(gè)條件,此時(shí)大概率就出現(xiàn)了接口濫用的情況。
是否過(guò)早的抽象,在引入該接口時(shí),系統(tǒng)中是否足夠的不同實(shí)現(xiàn)來(lái)正當(dāng)?shù)刂С诌@些接口。如果沒(méi)有的話,此時(shí)大概率過(guò)早接口的引入,增加了復(fù)雜性,而不帶來(lái)真正的好處。
是否所有類之間引入接口,無(wú)論是否有必要,在這種情況下,接口的數(shù)量可能會(huì)急劇增加,導(dǎo)致代碼難以理解和維護(hù),可能還是存在一定濫用的情況。
如果接口經(jīng)常發(fā)生變化,那么實(shí)現(xiàn)這些接口的類可能需要頻繁地進(jìn)行修改,這會(huì)增加維護(hù)的難度,此時(shí)要么接口是不必要的,要么接口的設(shè)計(jì)是不合理的,需要重新設(shè)計(jì)。
總的來(lái)說(shuō), 我們需要確保真正需要接口時(shí)才引入它們。應(yīng)該謹(jǐn)慎考慮每個(gè)接口的設(shè)計(jì),確保它們具有明確的用途(如隔絕變化,模塊間交互的契約,方便單元測(cè)試),并且不引入不必要的復(fù)雜性。根據(jù)實(shí)際需求和系統(tǒng)架構(gòu)來(lái)合理地使用接口,而不是為了使用接口而使用接口。
6. 總結(jié)
在本文,我們介紹了什么是接口,接口是一種契約,一種協(xié)議,用于模塊間的交互。
在此基礎(chǔ)上,通過(guò)一個(gè)例子來(lái)介紹接口的優(yōu)點(diǎn),了解到接口可以提高代碼的可擴(kuò)展性,可維護(hù)性,以及降低系統(tǒng)之間的耦合度。
但是接口也不是任何場(chǎng)景都可以隨意使用的,我們會(huì)介紹接口使用的常見(jiàn)場(chǎng)景,包括隔絕系統(tǒng)的變化部分,以及一些類庫(kù)設(shè)計(jì)時(shí)對(duì)外提供配置項(xiàng)的場(chǎng)景。
最后我們還介紹了接口濫用可能帶來(lái)的問(wèn)題,以及一些比較明顯的特征,幫助我們更早識(shí)別出系統(tǒng)設(shè)計(jì)的壞味道。
以上就是詳解golang中接口使用的最佳時(shí)機(jī)的詳細(xì)內(nèi)容,更多關(guān)于go 接口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
源碼分析Golang?log是如何實(shí)現(xiàn)的
go string to int 字符串與整數(shù)型的互換方式
Go 實(shí)現(xiàn)基于Token 的登錄流程深度分析
Go語(yǔ)言學(xué)習(xí)之Switch語(yǔ)句的使用
Golang編程實(shí)現(xiàn)生成n個(gè)從a到b不重復(fù)隨機(jī)數(shù)的方法
go?語(yǔ)言爬蟲(chóng)庫(kù)goquery的具體使用

