介面使用的最佳時機

来源:https://www.cnblogs.com/chenjiazhan/archive/2023/09/09/17688890.html
-Advertisement-
Play Games

淺聊一下SpringMVC的核心組件以及通過源碼瞭解其執行流程 MVC作為WEB項目開發的核心環節,正如三個單詞的分解那樣,Controller(控制器)將View(視圖、用戶客戶端)與Model(javaBean:封裝數據)分開構成了MVC,今天我們淺聊一下SpringMVC的相關組件以及通過源碼... ...


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 來計算商品的折扣信息,通過這幾部分的交互配合,共同來完成商家價格的計算。

image.png

下麵我們通過一段代碼來展示上面的計算流程:

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. 總結

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

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

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

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SpringBoot項目的業務工具類(如:參數工具類ParamUtils,僅包含static方法,依賴DAO訪問DB載入數據),在SpringBoot啟動過程中會被其他業務Bean初始化依賴。由於參數工具類和業務Bean均被Spring框架托管,如何在其他Bean初始化之前,就優雅安全的初始化Par... ...
  • Python中sys 模塊中的一個方法是stdout ,它使用其參數直接顯示在控制台視窗上。 這些種類的輸出可以是不同的,像一個簡單的列印語句,一個表達式,或者一個輸入提示。print() 方法,它有相同的行為,首先轉換為sys.stdout() 方法,然後在控制台顯示結果。 sys.stdout ...
  • 用os.path.expanduser 模塊獲取主目錄 為了獲得用戶的主目錄,我們可以使用Python中的os.path.expanduser 。我們必須在os.path.expanduser() 函數裡面傳遞一個字元串字元~ ,它將返回當前登錄用戶的主目錄路徑。 它使用內置的密碼資料庫或pwd 模 ...
  • Relocation(重定位)是一種將程式中的一些地址修正為運行時可用的實際地址的機制。在程式編譯過程中,由於程式中使用了各種全局變數和函數,這些變數和函數的地址還沒有確定,因此它們的地址只能暫時使用一個相對地址。當程式被載入到記憶體中運行時,這些相對地址需要被修正為實際的絕對地址,這個過程就是重定位... ...
  • 方案 markdown+Typora + picGo + jsdelivr + github倉庫 + bloghelper Typora: 本地 Markdown 編輯器,用於本地編寫文檔 PicGo:一個用於快速上傳圖片並獲取圖片 URL 鏈接的工具,可以與 Typora 集成,實現黏貼圖片後自動 ...
  • 方案 markdown+Typora + picGo + jsdelivr + github倉庫 + bloghelper Typora: 本地 Markdown 編輯器,用於本地編寫文檔 PicGo:一個用於快速上傳圖片並獲取圖片 URL 鏈接的工具,可以與 Typora 集成,實現黏貼圖片後自動 ...
  • 本文介紹在Anaconda環境下,創建、使用與刪除Python虛擬環境的方法。 在Python的使用過程中,我們常常由於不同Python版本以及不同第三方庫版本的支持情況與相互之間的衝突情況,而需要創建不同的Python虛擬環境;在Anaconda的幫助下,這一步驟就變得十分方便。 首先,我們需要打 ...
  • LeetCode的hard題都很難嗎?不一定,297就非常簡單,隨本文一起,用最基礎的知識寫代碼,執行用時能擊敗98.46%,與此同時,記憶體消耗擊敗99.73% ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...