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