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

go string to int 字符串與整數(shù)型的互換方式

Golang編程實現(xiàn)生成n個從a到b不重復(fù)隨機數(shù)的方法