介面使用的最佳時機

2023-09-09 12:01:02

1. 引言

介面在系統設計中,以及程式碼重構優化中,是一個不可或缺的工具,能夠幫助我們寫出可延伸,可維護性更強的程式。

在本文,我們將介紹什麼是介面,在此基礎上,通過一個例子來介紹介面的優點。但是介面也不是任何場景都可以隨意使用的,我們會介紹介面使用的常見場景,同時也介紹了介面濫用可能帶來的問題,以及一些介面濫用的特徵,幫助我們及早發現介面濫用的情況。

2. 什麼是介面

介面是一種工具,在識別出系統中變化部分時,幫助從系統模組中抽取出變化的部分,從而保證系統的穩定性,可維護性和可延伸性。介面充當了一種契約或規範,規定了類或模組應該提供的方法和行為,而不關心具體的實現細節。

介面通常用於物件導向程式語言中,如 JavaGo 等。在這些語言中,類可以實現一個或多個介面,並提供介面定義的方法的具體實現。通過使用介面,我們可以編寫更靈活、可維護和可延伸的程式碼,同時將系統中的變化隔離開來。

介面的實現在不同的程式語言中可能會有所不同。以下簡單展示介面在JavaGo 語言中的範例。在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;
    }
}

上面範例展示了JavaGo語言中介面的定義方式以及介面的實現方式,雖然具體實現方式各不相同,但它們都遵循了相似的概念,介面用於定義規範和契約,實現類則提供方法的具體實現來滿足介面的要求。

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 介面以及兩個不同的折扣計算器實現:TypeADiscountCalculatorTypeBDiscountCalculatorOrderProcessorWithInterface 結構體依賴於 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. 總結

在本文,我們介紹了什麼是介面,介面是一種契約,一種協定,用於模組間的互動。

在此基礎上,通過一個例子來介紹介面的優點,瞭解到介面可以提高程式碼的可延伸性,可維護性,以及降低系統之間的耦合度。

但是介面也不是任何場景都可以隨意使用的,我們會介紹介面使用的常見場景,包括隔絕系統的變化部分,以及一些類庫設計時對外提供設定項的場景。

最後我們還介紹了介面濫用可能帶來的問題,以及一些比較明顯的特徵,幫助我們更早識別出系統設計的壞味道。

基於此,完成了對介面的完整介紹,希望對你有所幫助。