實踐GoF的設計模式:工廠方法模式
摘要:工廠方法模式(Factory Method Pattern)將物件建立的邏輯封裝起來,為使用者提供一個簡單易用的物件建立介面,常用於不指定物件具體型別的情況下建立物件的場景。
本文分享自華為雲社群《【Go實現】實踐GoF的23種設計模式:工廠方法模式》,作者: 元閏子。
簡述
工廠方法模式(Factory Method Pattern)跟上一篇討論的建造者模式類似,都是將物件建立的邏輯封裝起來,為使用者提供一個簡單易用的物件建立介面。兩者在應用場景上稍有區別,建造者模式常用於需要傳遞多個引數來進行例項化的場景;工廠方法模式常用於不指定物件具體型別的情況下建立物件的場景。
UML 結構
程式碼實現
示例
在簡單的分散式應用系統(示例程式碼工程)中,我們設計了 Sidecar 邊車模組, Sidecar 的作用是為了給原生的 Socket 增加額外的功能,比如流控、日誌等。
Sidecar 模組的設計運用了裝飾者模式,修飾的是 Socket 。所以客戶端其實是把 Sidecar 當成是 Socket 來使用了,比如:
// demo/network/http/http_client.go
package http
// 建立一個新的HTTP客戶端,以Socket介面作為入參
func NewClient(socket network.Socket, ip string) (*Client, error) {
... // 一些初始化邏輯
return client, nil
}
// 使用NewClient時,我們可以傳入Sidecar來給Http客戶端附加額外的流控功能
client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), "192.168.0.1")
在服務訊息中介中,每次收到上游服務的 HTTP 請求,都會呼叫 http.NewClient 來建立一個 HTTP 客戶端,並通過它將請求轉發給下游服務:
type ServiceMediator struct {
...
server *http.Server
}
// Forward 轉發請求,請求URL為 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task
func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
...
// 發現下游服務的目的IP地址
dest, err := s.discovery(svcType)
// 建立HTTP客戶端,硬編碼sidecar.NewFlowCtrlSidecar(network.DefaultSocket())
client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), s.localIp)
// 通過HTTP客戶端轉發請求
resp, err := client.Send(dest, forwardReq)
...
}
在上述實現中,我們在呼叫 http.NewClient 時把 sidecar.NewFlowCtrlSidecar(network.DefaultSocket()) 硬編碼進去了,那麼如果以後要擴充套件 Sidecar ,就得修改這段程式碼邏輯,這違反了開閉原則 OCP。
有經驗的同學可能會想到,可以通過讓 ServiceMediator 依賴 Socket 介面,在 Forward 方法呼叫 http.NewClient 時把 Socket 介面作為入參;然後在 ServiceMediator 初始化時,將具體型別的 Sidecar 注入到 ServiceMediator 中:
type ServiceMediator struct {
...
server *http.Server
// 依賴Socket抽象介面
socket network.Socket
}
// Forward 轉發請求,請求URL為 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task
func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
...
// 發現下游服務的目的IP地址
dest, err := s.discovery(svcType)
// 建立HTTP客戶端,將s.socket抽象介面作為入參
client, err := http.NewClient(s.socket, s.localIp)
// 通過HTTP客戶端轉發請求
resp, err := client.Send(dest, forwardReq)
...
}
// 在ServiceMediator初始化時,將具體型別的Sidecar注入到ServiceMediator中
mediator := &ServiceMediator{
socket: sidecar.NewFlowCtrlSidecar(network.DefaultSocket())
}
上述的修改,從原來依賴具體,改成了依賴抽象,符合了開閉原則。
但是, Forward 方法存在併發呼叫的場景,因此它希望每次被呼叫時都建立一個新的 Socket/Sidecar 來完成網路通訊,否則就需要加鎖來保證併發安全。而上述的修改會導致在 ServiceMediator 的生命週期內都使用同一個 Socket/Sidecar,顯然不符合要求。
因此,我們需要一個方法,既能夠滿足開閉原則,而且在每次呼叫Forward 方法時也能夠建立新的 Socket/Sidecar 例項。工廠方法模式恰好就能滿足這兩點要求,下面我們通過它來完成程式碼的優化。
實現
// demo/sidecar/sidecar_factory.go
// 關鍵點1: 定義一個Sidecar工廠抽象介面
type Factory interface {
// 關鍵點2: 工廠方法返回Socket抽象介面
Create() network.Socket
}
// 關鍵點3: 按照需要實現具體的工廠
// demo/sidecar/raw_socket_sidecar_factory.go
// RawSocketFactory 只具備原生socket功能的sidecar,實現了Factory介面
type RawSocketFactory struct {
}
func (r RawSocketFactory) Create() network.Socket {
return network.DefaultSocket()
}
// demo/sidecar/all_in_one_sidecar_factory.go
// AllInOneFactory 具備所有功能的sidecar工廠,實現了Factory介面
type AllInOneFactory struct {
producer mq.Producible
}
func (a AllInOneFactory) Create() network.Socket {
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.producer)
}
上述程式碼中,我們定義了一個工廠抽象介面 Factory ,並有了 2 個具體的實現 RawSocketFactory 和 AllInOneFactory。最後, ServiceMediator 依賴 Factory ,並在 Forward 方法中通過 Factory 來建立新的 Socket/Sidecar :
// demo/service/mediator/service_mediator.go
type ServiceMediator struct {
...
server *http.Server
// 關鍵點4: 客戶端依賴Factory抽象介面
sidecarFactory sidecar.Factory
}
// Forward 轉發請求,請求URL為 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task
func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
...
// 發現下游服務的目的IP地址
dest, err := s.discovery(svcType)
// 建立HTTP客戶端,呼叫sidecarFactory.Create()生成Socket作為入參
client, err := http.NewClient(s.sidecarFactory.Create(), s.localIp)
// 通過HTTP客戶端轉發請求
resp, err := client.Send(dest, forwardReq)
...
}
// 關鍵點5: 在ServiceMediator初始化時,將具體型別的sidecar.Factory注入到ServiceMediator中
mediator := &ServiceMediator{
sidecarFactory: &AllInOneFactory{}
// sidecarFactory: &RawSocketFactory{}
}
下面總結實現工廠方法模式的幾個關鍵點:
- 定義一個工廠方法抽象介面,比如前文中的 sidecar.Factory。
- 工廠方法中,返回需要建立的物件/介面,比如 network.Socket。其中,工廠方法通常命名為 Create。
- 按照具體需要,定義工廠方法抽象介面的具體實現物件,比如 RawSocketFactory 和 AllInOneFactory。
- 客戶端使用時,依賴工廠方法抽象介面。
- 在客戶端初始化階段,完成具體工廠物件的依賴注入。
擴充套件
Go 風格的實現
前文的工廠方法模式實現,是非常典型的面向物件風格,下面我們給出一個更具 Go 風格的實現。
// demo/sidecar/sidecar_factory_func.go
// 關鍵點1: 定義Sidecar工廠方法型別
type FactoryFunc func() network.Socket
// 關鍵點2: 按需定義具體的工廠方法實現,注意這裡定義的是工廠方法的工廠方法,返回的是FactoryFunc工廠方法型別
func RawSocketFactoryFunc() FactoryFunc {
return func() network.Socket {
return network.DefaultSocket()
}
}
func AllInOneFactoryFunc(producer mq.Producible) FactoryFunc {
return func() network.Socket {
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer)
}
}
type ServiceMediator struct {
...
server *http.Server
// 關鍵點3: 客戶端依賴FactoryFunc工廠方法型別
sidecarFactoryFunc FactoryFunc
}
func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
...
dest, err := s.discovery(svcType)
// 關鍵點4: 建立HTTP客戶端,呼叫sidecarFactoryFunc()生成Socket作為入參
client, err := http.NewClient(s.sidecarFactoryFunc(), s.localIp)
resp, err := client.Send(dest, forwardReq)
...
}
// 關鍵點5: 在ServiceMediator初始化時,將具體型別的FactoryFunc注入到ServiceMediator中
mediator := &ServiceMediator{
sidecarFactoryFunc: RawSocketFactoryFunc()
// sidecarFactory: AllInOneFactoryFunc(producer)
}
上述的實現,利用了 Go 語言中函式作為一等公民的特點,少定義了幾個 interface 和 struct,程式碼更加的簡潔。
幾個實現的關鍵點與面向物件風格的實現類似。值得注意的是 關鍵點2 ,我們相當於定義了一個工廠方法的工廠方法,這麼做是為了利用函式閉包的特點來傳遞引數。如果直接定義工廠方法,那麼 AllInOneFactoryFunc 的實現是下面這樣的,無法實現多型:
// 並非FactoryFunc型別,無法實現多型
func AllInOneFactoryFunc(producer mq.Producible) network.Socket {
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer)
}
簡單工廠
工廠方法模式的另一個變種是簡單工廠,它並不通過多型,而是通過簡單的 switch-case/if-else 條件判斷來決定建立哪種產品:
// demo/sidecar/sidecar_simple_factory.go
// 關鍵點1: 定義sidecar型別
type Type uint8
// 關鍵點2: 按照需要定義sidecar具體型別
const (
Raw Type = iota
AllInOne
)
// 關鍵點3: 定義簡單工廠物件
type SimpleFactory struct {
producer mq.Producible
}
// 關鍵點4: 定義工廠方法,入參為sidecar型別,根據switch-case或者if-else來建立產品
func (s SimpleFactory) Create(sidecarType Type) network.Socket {
switch sidecarType {
case Raw:
return network.DefaultSocket()
case AllInOne:
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), s.producer)
default:
return nil
}
}
// 關鍵點5: 建立產品時傳入具體的sidecar型別,比如sidecar.AllInOne
simpleFactory := &sidecar.SimpleFactory{producer: producer}
sidecar := simpleFactory.Create(sidecar.AllInOne)
靜態工廠方法
靜態工廠方法是 Java/C++ 的說法,主要用於替代建構函式來完成物件的例項化,能夠讓程式碼的可讀性更好,而且起到了與客戶端解耦的作用。比如 Java 的靜態工廠方法實現如下:
public class Packet {
private final Endpoint src;
private final Endpoint dest;
private final Object payload;
private Packet(Endpoint src, Endpoint dest, Object payload) {
this.src = src;
this.dest = dest;
this.payload = payload;
}
// 靜態工廠方法
public static Packet of(Endpoint src, Endpoint dest, Object payload) {
return new Packet(src, dest, payload);
}
...
}
// 用法
packet = Packet.of(src, dest, payload)
Go 中並沒有靜態一說,直接通過普通函式來完成物件的構造即可,比如:
// demo/network/packet.go
type Packet struct {
src Endpoint
dest Endpoint
payload interface{}
}
// 工廠方法
func NewPacket(src, dest Endpoint, payload interface{}) *Packet {
return &Packet{
src: src,
dest: dest,
payload: payload,
}
}
// 用法
packet := NewPacket(src, dest, payload)
典型應用場景
- 物件例項化邏輯較為複雜時,可選擇使用工廠方法模式/簡單工廠/靜態工廠方法來進行封裝,為客戶端提供一個易用的介面。
- 如果例項化的物件/介面涉及多種實現,可以使用工廠方法模式實現多型。
- 普通物件的建立,推薦使用靜態工廠方法,比直接的例項化(比如 &Packet{src: src, dest: dest, payload: payload})具備更好的可讀性和低耦合。
優缺點
優點
- 程式碼的可讀性更好。
- 與客戶端程式解耦,當例項化邏輯變更時,只需改動工廠方法即可,避免了霰彈式修改。
缺點
- 引入工廠方法模式會新增一些物件/介面的定義,濫用會導致程式碼更加複雜。
與其他模式的關聯
很多同學容易將工廠方法模式和抽象工廠模式混淆,抽象工廠模式主要運用在例項化“產品族”的場景,可以看成是工廠方法模式的一種演進。
參考
[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] Design Patterns, Chapter 3. Creational Patterns, GoF
[3] Factory patterns in Go (Golang), Soham Kamani
[4] 工廠方法, 維基百科
簡單的分散式應用系統(示例程式碼工程):https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation
- 帶你掌握 C 中三種類成員初始化方式
- 實踐GoF的設計模式:工廠方法模式
- DCM:一個能夠改善所有應用資料互動場景的中介軟體新秀
- 手繪圖解java類載入原理
- 關於加密通道規範,你真正用的是TLS,而非SSL
- 程式碼重構,真的只有複雜化一條路嗎?
- 解讀分散式排程平臺Airflow在華為雲MRS中的實踐
- 透過例項demo帶你認識gRPC
- 帶你聚焦GaussDB(DWS)儲存時遊標使用
- 傳統到敏捷的轉型中,誰更適合做Scrum Master?
- 輕鬆解決研發知識管理難題
- Java中觀察者模式與委託,還在傻傻分不清
- 如何使用Python實現影象融合及加法運算?
- 什麼是強化學習?
- 探索開源工作流引擎Azkaban在MRS中的實踐
- GaussDB(DWS) NOT IN優化技術解密:排他分析場景400倍效能提升
- Java中觀察者模式與委託,還在傻傻分不清
- Java中的執行緒到底有哪些安全策略
- 一圖詳解java-class類檔案原理
- Java中的執行緒到底有哪些安全策略