適配器模式在 Go 語言中的應用
前段時間我負責對一個項目進行臨時性的技術方案改造,用到了適配器模式,今天就來跟大家簡單分享下適配器模式在 Go 語言中的應用。
適配器模式
適配器模式(Adapter Pattern)是 23 種經典設計模式中的一種,屬於行爲型模式,它允許不兼容的接口協同工作。該模式通過創建一個適配器類,封裝不兼容的接口,並對外提供一個兼容的接口。
《設計模式:可複用面向對象軟件的基礎》一書中對適配器的意圖定義如下:
將一個類的接口轉換成客戶希望的另外一個接口。Adapter 模式使得原本由於接口不兼容而不能一起工作的那些類可以一起工作。
這麼說比較抽象,我來舉一個現實生活中的例子,幫助你理解。
生活中的適配器模式
在現實生活中,我們平時使用的手機、電腦等充電器,實際上叫電源適配器(Power adapter)。
下圖中插座上有一個白色的電源適配器和一個黑色的普通插頭:
電源適配器
因爲手機和電腦無法直接接收 220V 交流電,一般只會接收如 9V3A 這種直流小電壓輸入才能進行充電。所以不能直接使用一個普通插頭來爲手機充電,而是需要使用電源適配器進行轉換。
這就是現實生活中的適配器模式。
Go 語言中的適配器模式
那麼在 Go 代碼中如何實現適配器呢?我們以一個簡單的支付系統作爲示例進行講解。
比如,我們有一個支付的功能,核心代碼如下:
// PaymentProcessor 是一個統一的支付接口
type PaymentProcessor interface {
Pay(amount float64)
}
// OldPaymentSystem 是舊的支付系統
type OldPaymentSystem struct{}
func (ops *OldPaymentSystem) Pay(amount float64) {
fmt.Printf("Processing payment of %.2f using old payment system\n", amount)
}
可以這樣使用:
// 聲明支付接口
var processor PaymentProcessor
// 使用舊支付系統
processor = &OldPaymentSystem{}
processor.Pay(100)
現在我們需要接入一個新支付系統,核心代碼如下:
// NewPaymentSystem 是新的支付系統
type NewPaymentSystem struct{}
func (nps *NewPaymentSystem) MakePayment(amount float64) {
fmt.Printf("Making payment of %.2f using new payment system\n", amount)
}
新的支付系統沒有 Pay
方法,所以沒有實現統一支付接口 PaymentProcessor
。
我們不能按照原來使用舊的支付系統的方式來使用新的支付系統:
processor = &NewPaymentSystem{}
processor.Pay(100)
這段代碼是行不通的,會編譯報錯。
因爲 NewPaymentSystem
並沒有實現 PaymentProcessor
接口,無法賦值給 processor
變量。
此時,我們可以創建一個適配器,來解決此問題:
// NewPaymentAdapter 是新支付系統的適配器
type NewPaymentAdapter struct {
// 內部持有新支付系統
NewSystem *NewPaymentSystem
}
func (npa *NewPaymentAdapter) Pay(amount float64) {
npa.NewSystem.MakePayment(amount)
}
定義 NewPaymentAdapter
作爲新支付系統 NewPaymentSystem
的適配器,其內部持有新的支付系統 NewPaymentSystem
,併爲適配器定義 Pay
方法。
有了適配器,我們就可以按照原來使用舊的支付系統的方式來使用新的支付系統了:
// 使用新支付系統
newPayment := &NewPaymentSystem{}
// 使用適配器模式
processor = &NewPaymentAdapter{NewSystem: newPayment}
processor.Pay(200)
因爲適配器 NewPaymentAdapter
實現了 PaymentProcessor
接口,所以經過它包裝的新支付系統 NewPaymentSystem
可以賦值給 processor
變量。
在調用 processor.Pay(200)
時,NewPaymentAdapter.Pay
方法內部會調用 npa.NewSystem.MakePayment(200)
方法,將請求轉發到新支付系統。
我們也就實現了使用統一的支付接口,來完成使用新支付系統進行支付。
這裏的 NewPaymentAdapter
就是適配器模式中的「適配器」。
NewPaymentAdapter
實現了將不兼容的接口(NewPaymentSystem
)轉換爲成客戶希望的另外一個接口(PaymentProcessor
),使得不兼容的二者可以協同工作。
這就是一個簡單的適配器模式在 Go 語言中的應用示例。
生產實踐
通過前文的描述,我們對適配器模式有了一定了解。
接下來我帶你看下我在生產實踐過程中是如何使用適配器模式的。
多雲管理平臺的應用
我曾經參與開發過一個多雲管理平臺,這是一個用 Go 編寫的管理多個第三方雲主機的平臺。可以在管理平臺上操作如阿里雲、騰訊雲、AWS 等平臺的雲主機,實現創建、續費、刪除等。
每家雲主機廠商都提供了非常方便的 Go SDK 來操作雲主機,遺憾的是,每家的 SDK 接口又都不一樣。
爲了統一操作,抹平不同雲主機廠商之間 SDK 接口的差異,我們定義瞭如下接口:
// Provider 定義雲廠商統一接口
type Provider interface {
// Type 返回 Provider 類型
Type() ProviderType
// RunInstance 創建雲主機
RunInstance(r *RunInstanceRequest) (*RunInstanceResponse, error)
// ...
}
NOTE: 這裏我僅列出接口的了
Type
和RunInstance
兩個方法,代碼意圖不變。
所有云主機相關操作,都必須遵循 Provider
接口。
爲了區分不同雲主機廠商類型,我們還需要爲每個雲主機廠商定義一個常量:
type ProviderType string
const (
ProviderTypeAliyun ProviderType = "aliyun"
ProviderTypeTencent ProviderType = "tencent"
// ...
)
接下來就要爲每一個雲主機廠商都各自封裝一個 XxxProvider
來適配 Provider
接口。
阿里雲 Provider
定義如下:
// AliCloudProvider 阿里雲 Provider
type AliCloudProvider struct {
typ ProviderType
// 包裝 alibaba-cloud-sdk-go
}
func NewAliCloudProvider(typ ProviderType) *AliCloudProvider {
return &AliCloudProvider{
typ: typ,
// ...
}
}
func (a AliCloudProvider) Type() ProviderType {
return a.typ
}
// RunInstance https://help.aliyun.com/zh/ecs/developer-reference/api-ecs-2014-05-26-runinstances
func (a AliCloudProvider) RunInstance(r *RunInstanceRequest) (*RunInstanceResponse, error) {
panic("implement me")
}
AliCloudProvider
是對阿里雲提供的 alibaba-cloud-sdk-go
的包裝,其實現了 Provider
接口,並將 Provider
所有方法轉換成對 alibaba-cloud-sdk-go
的調用。
騰訊雲 Provider
定義如下:
// TencentCloudProvider 騰訊雲 Provider
type TencentCloudProvider struct {
typ ProviderType
// 包裝 tencentcloud-sdk-go
}
func NewTencentCloudProvider(typ ProviderType) *TencentCloudProvider {
return &TencentCloudProvider{
typ: typ,
// ...
}
}
func (t TencentCloudProvider) Type() ProviderType {
return t.typ
}
// RunInstance https://cloud.tencent.com/document/api/213/15730
func (t TencentCloudProvider) RunInstance(r *RunInstanceRequest) (*RunInstanceResponse, error) {
panic("implement me")
}
同理,TencentCloudProvider
是對騰訊雲提供的 tencentcloud-sdk-go
的包裝,其實現了 Provider
接口,並將 Provider
所有方法轉換成對 tencentcloud-sdk-go
的調用。
定義 Provider
構造函數如下:
func NewProvider(typ ProviderType) Provider {
switch typ {
case ProviderTypeAliyun:
return NewAliCloudProvider(typ)
case ProviderTypeTencent:
return NewTencentCloudProvider(typ)
default:
panic("unknown provider")
}
return nil
}
在使用時,我們可以根據多雲管理平臺前端用戶選擇的不同雲主機廠商 ProviderType
,來構造不同的 Provider
對象,然後去操作雲主機,實現創建、刪除等。
使用示例如下:
var p Provider
// 阿里雲
p = NewProvider(ProviderTypeAliyun)
resp, err := p.RunInstance(&RunInstanceRequest{})
fmt.Println(resp, err)
// 騰訊雲
p = NewProvider(ProviderTypeTencent)
resp, err = p.RunInstance(&RunInstanceRequest{})
fmt.Println(resp, err)
以上,就是我從多雲管理平臺生產實踐中抽離出來的適配器模式使用示例。
這裏去掉了業務邏輯,僅保留了代碼整體思路。這個示例中適配器代碼的命名並沒有使用 Adapter
,但這其實也是一種適配器模式。
AliCloudProvider
和 TencentCloudProvider
都是適配器,它們分別包裝了阿里雲和騰訊雲的 SDK,然後適配雲廠商統一操作接口 Provider
。
在使用時,可以根據需要,指定 ProviderType
構造不同類型的 Provider
,然後操作對應廠商的雲主機。
模型訓練平臺的應用
再舉一個我在生產實踐中使用適配器模式的例子。
前段時間我負責對一個項目進行臨時性的技術方案改造,就用到了適配器模式。
這是一個支持大語言模型訓練和推理的綜合平臺,本小結以部署模型推理任務爲例進行講解。
部署推理任務步驟如下:
-
構造推理任務的 Kubernetes
Deployment
資源。 -
部署推理任務
Deployment
到 Kubernetes 集羣。
示例代碼大致如下:
import appsv1 "k8s.io/api/apps/v1"
func BuildDeployment() *appsv1.Deployment {
// ...
return nil
}
func DeployPredictService(deployment *appsv1.Deployment) error {
// ...
return nil
}
BuildDeployment
用於構造 Deployment
資源對象。
DeployPredictService
用於將這個 Deployment
資源對象部署到 Kubernetes 中,來啓動模型推理服務。
所以部署模型推理服務代碼流程如下:
deployment := BuildDeployment()
err := DeployPredictService(deployment)
fmt.Println(err)
現在要對這個平臺代碼進行臨時性改造,需要新建一個 feature
分支,使用微軟開源的 OpenPAI 平臺來部署模型推理服務,而不再直接使用 Kubernetes Deployment
資源進行部署。
NOTE: OpenPAI 是一個提供完整的人工智能模型訓練和資源管理能力開源平臺,它易於擴展,支持各種規模的 on-premise、on-cloud 和混合環境。
OpenPAI 平臺有自己的 CRD(Custom Resource Definition)來定義和管理部署資源,並且 OpenPAI 提供了 RESTful 接口,可以創建 CR(Custom Resource) 來生成 OpenPAI 的資源對象。
因爲是臨時性的改造,未來有很大不確定性,並且舊有代碼經過多人次維護,結構變來變去早已成爲屎山,藉着這一次的需求改造,索性引入適配器模式,來規範代碼結構。
首先,抽象出一個模型推理任務統一操作接口 Predictor
:
type Predictor interface {
Deploy(deployment *appsv1.Deployment) error
Scale(namespace, name string, replicas int) error
Delete(namespace, name string) error
// ...
}
Predictor
接口定義了模型推理服務的所有操作,包括部署、伸縮、刪除等。
然後爲 OpenPAI 提供一個適配器來適配 Predictor
接口:
type OpenPAIAdapter struct {
// ...
}
func NewOpenPAIAdapter() *OpenPAIAdapter {
return &OpenPAIAdapter{}
}
func (o *OpenPAIAdapter) Deploy(deployment *appsv1.Deployment) error {
// 將 K8s Deployment 資源轉換成 OpenPAI 的 RESTful 接口調用
panic("implement me")
}
func (o *OpenPAIAdapter) Scale(namespace, name string, replicas int) error {
panic("implement me")
}
func (o *OpenPAIAdapter) Delete(namespace, name string) error {
panic("implement me")
}
我們可以在 OpenPAIAdapter
的每個方法中,將對 Kubernetes Deployment
資源的每種操作,轉換成對應的 OpenPAI 的 RESTful 接口調用。
使用 OpenPAIAdapter
適配器部署模型推理服務代碼如下:
// 推理任務統一接口
var predictor Predictor
// 根據業務邏輯構造不同的適配器
// switch expr {
// case:
predictor = NewOpenPAIAdapter()
// }
// 部署推理服務
deployment := BuildDeployment()
err := predictor.Deploy(deployment)
fmt.Println(err)
通過引入適配器模式,來支撐 OpenPAI 資源的部署,能夠最小化改動代碼,且定義統一的規範。
這裏 Predictor
接口其實是圍繞着 Kubernetes Deployment
資源的操作來定義的。
之所以這樣定義,實際上是爲了對代碼只做最小化改動。因爲之前的代碼全部都是圍繞一個 Deployment
資源來寫的,且開發週期有限,抽象出 Predictor
接口已經足以引入適配器模式,以此來方便部署 OpenPAI 資源。
並且,將來哪一天想要改回來,再次使用 Kubernetes Deployment
資源部署模型推理服務,只需要將舊代碼也進行適配,寫一個 DeploymentAdapter
適配器即可。
以後可能還會對接其他平臺,都可以參照 OpenPAIAdapter
來設計新的適配器。
以上,同樣是我從生產實踐中抽離出來的適配器模式使用示例。
總結
本文主要介紹了什麼是適配器模式,以及適配器模式在 Go 語言中的應用。
既然這個設計模式叫「適配器」模式,那麼其作用必然是爲了適配,更多的時候是作爲一種事後補償機制。所以其實這個設計模式往往並不是首選。
我列舉的適配器模式應用場景,都是我在生產實踐中的探索。
在講解什麼是適配器模式的示例中,爲了兼容舊版本支付接口,我們將新版本支付接口做了修改,對舊版本支付接口進行適配,這其實就是一種所謂的事後補償機制。
在多雲管理平臺使用示例中,適配器模式的應用屬於統一多個外部系統編程接口。還有一種場景最爲常見,就是對接多個第三方支付系統,如支付寶、微信等。
在模型訓練平臺使用示例中,適配器模式的應用場景屬於替換依賴的外部系統,當我們把項目中依賴的一個外部系統替換爲另一個外部系統的時候,使用適配器模式,可以減少對代碼的改動。
本文介紹的這幾種應用場景算是適配器模式典型的使用場景了。
你認爲適配器模式還有哪些應用場景,可以一起交流學習。
本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。
希望此文能對你有所啓發。
聯繫我
-
公衆號:Go 編程世界
-
微信:jianghushinian
-
郵箱:jianghushinian007@outlook.com
-
博客:https://jianghushinian.cn
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/BxxMoPqYkhQMp1YxFHLP-w?poc_token=HLmgsWajMGOmurf6AJFD_N_T3-ytNbllf2Ota91K