設計模式系列 | 工廠方法模式
## 概述
**工廠方法模式**是一種創建型設計模式,其在父類中提供一個創建對象的方法,允許子類決定實例化對象的類型。

## 問題
假設你正在開發一款物流管理應用。最初版本只能處理卡車運輸,因此大部分代碼都在位于名為`卡車`的類中。一段時間后,這款應用變得極受歡迎。你每天都能收到十幾次來自海運公司的請求,希望應用能夠支持海上物流功能。

> 如果代碼其余部分與現有類已經存在耦合關系,那么向程序中添加新類其實并沒有那么容易。
這可是個好消息。但是代碼問題該如何處理呢?目前,大部分代碼都與`卡車`類相關。在程序中添加`輪船`類需要修改全部代碼。更糟糕的是,如果你以后需要在程序中支持另外一種運輸方式,很可能需要再次對這些代碼進行大幅修改。
最后,你將不得不編寫繁復的代碼,根據不同的運輸對象類,在應用中進行不同的處理。
## 解決方案
工廠方法模式建議使用特殊的*工廠*方法代替對于對象構造函數的直接調用(即使用`new`運算符)。不用擔心,對象仍將通過`new`運算符創建,只是該運算符改在工廠方法中調用罷了。工廠方法返回的對象通常被稱作“產品”。

> 子類可以修改工廠方法返回的對象類型。
乍看之下,這種更改可能毫無意義:我們只是改變了程序中調用構造函數的位置而已。但是,仔細想一下,現在你可以在子類中重寫工廠方法,從而改變其創建產品的類型。
但有一點需要注意:僅當這些產品具有共同的基類或者接口時,子類才能返回不同類型的產品,同時基類中的工廠方法還應將其返回類型聲明為這一共有接口。

舉例來說,?`卡車`Truck和`輪船`Ship類都必須實現`運輸`Transport接口,該接口聲明了一個名為`deliver`交付的方法。每個類都將以不同的方式實現該方法:卡車走陸路交付貨物,輪船走海路交付貨物。?`陸路運輸`Road-Logistics類中的工廠方法返回卡車對象,而`海路運輸`Sea-Logistics類則返回輪船對象。

> 只要產品類實現一個共同的接口,你就可以將其對象傳遞給客戶代碼,而無需提供額外數據。
調用工廠方法的代碼(通常被稱為*客戶端*代碼)無需了解不同子類返回實際對象之間的差別。客戶端將所有產品視為抽象的`運輸`。客戶端知道所有運輸對象都提供`交付`方法,但是并不關心其具體實現方式。
## 工廠方法模式結構

1. **產品**(Product)將會對接口進行聲明。對于所有由創建者及其子類構建的對象,這些接口都是通用的。
2. **具體產品**(Concrete Products)是產品接口的不同實現。
3. **創建者**(Creator)類聲明返回產品對象的工廠方法。該方法的返回對象類型必須與產品接口相匹配。你可以將工廠方法聲明為抽象方法,強制要求每個子類以不同方式實現該方法。或者,你也可以在基礎工廠方法中返回默認產品類型。注意,盡管它的名字是創建者,但它最主要的職責并**不是**創建產品。一般來說,創建者類包含一些與產品相關的核心業務邏輯。工廠方法將這些邏輯處理從具體產品類中分離出來。打個比方,大型軟件開發公司擁有程序員培訓部門。但是,這些公司的主要工作還是編寫代碼,而非生產程序員。
4. **具體創建者**(Concrete Creators)將會重寫基礎工廠方法,使其返回不同類型的產品。注意,并不一定每次調用工廠方法都會**創建**新的實例。工廠方法也可以返回緩存、對象池或其他來源的已有對象。
## 偽代碼
以下示例演示了如何使用**工廠方法**開發跨平臺 UI(用戶界面)組件,并同時避免客戶代碼與具體 UI 類之間的耦合。

基礎對話框類使用不同的 UI 組件渲染窗口。在不同的操作系統下,這些組件外觀或許略有不同,但其功能保持一致。Windows 系統中的按鈕在 Linux 系統中仍然是按鈕。
如果使用工廠方法,就不需要為每種操作系統重寫對話框邏輯。如果我們聲明了一個在基本對話框類中生成按鈕的工廠方法,那么我們就可以創建一個對話框子類,并使其通過工廠方法返回 Windows 樣式按鈕。子類將繼承對話框基礎類的大部分代碼,同時在屏幕上根據 Windows 樣式渲染按鈕。
如需該模式正常工作,基礎對話框類必須使用抽象按鈕(例如基類或接口),以便將其擴展為具體按鈕。這樣一來,無論對話框中使用何種類型的按鈕,其代碼都可以正常工作。
你可以使用此方法開發其他 UI 組件。不過,每向對話框中添加一個新的工廠方法,你就離[抽象工廠](https://refactoringguru.cn/design-patterns/abstract-factory)模式更近一步。我們將在稍后談到這個模式。
```
// 創建者類聲明的工廠方法必須返回一個產品類的對象。創建者的子類通常會提供
// 該方法的實現。
class Dialog is
// 創建者還可提供一些工廠方法的默認實現。
abstract method createButton():Button
// 請注意,創建者的主要職責并非是創建產品。其中通常會包含一些核心業務
// 邏輯,這些邏輯依賴于由工廠方法返回的產品對象。子類可通過重寫工廠方
// 法并使其返回不同類型的產品來間接修改業務邏輯。
method render() is
// 調用工廠方法創建一個產品對象。
Button okButton = createButton()
// 現在使用產品。
okButton.onClick(closeDialog)
okButton.render()
// 具體創建者將重寫工廠方法以改變其所返回的產品類型。
class WindowsDialog extends Dialog is
method createButton():Button is
return new WindowsButton()
class WebDialog extends Dialog is
method createButton():Button is
return new HTMLButton()
// 產品接口中將聲明所有具體產品都必須實現的操作。
interface Button is
method render()
method onClick(f)
// 具體產品需提供產品接口的各種實現。
class WindowsButton implements Button is
method render(a, b) is
// 根據 Windows 樣式渲染按鈕。
method onClick(f) is
// 綁定本地操作系統點擊事件。
class HTMLButton implements Button is
method render(a, b) is
// 返回一個按鈕的 HTML 表述。
method onClick(f) is
// 綁定網絡瀏覽器的點擊事件。
class Application is
field dialog: Dialog
// 程序根據當前配置或環境設定選擇創建者的類型。
method initialize() is
config = readApplicationConfigFile()
if (config.OS == "Windows") then
dialog = new WindowsDialog()
else if (config.OS == "Web") then
dialog = new WebDialog()
else
throw new Exception("錯誤!未知的操作系統。")
// 當前客戶端代碼會與具體創建者的實例進行交互,但是必須通過其基本接口
// 進行。只要客戶端通過基本接口與創建者進行交互,你就可將任何創建者子
// 類傳遞給客戶端。
method main() is
this.initialize()
dialog.render()
```
## 工廠方法模式適合應用場景
#### 當你在編寫代碼的過程中,如果無法預知對象確切類別及其依賴關系時,可使用工廠方法。
工廠方法將創建產品的代碼與實際使用產品的代碼分離,從而能在不影響其他代碼的情況下擴展產品創建部分代碼。
例如,如果需要向應用中添加一種新產品,你只需要開發新的創建者子類,然后重寫其工廠方法即可。
#### 如果你希望用戶能擴展你軟件庫或框架的內部組件,可使用工廠方法。
繼承可能是擴展軟件庫或框架默認行為的最簡單方法。但是當你使用子類替代標準組件時,框架如何辨識出該子類?
解決方案是將各框架中構造組件的代碼集中到單個工廠方法中,并在繼承該組件之外允許任何人對該方法進行重寫。
讓我們看看具體是如何實現的。假設你使用開源 UI 框架編寫自己的應用。你希望在應用中使用圓形按鈕,但是原框架僅支持矩形按鈕。你可以使用`圓形按鈕`Round-Button子類來繼承標準的`按鈕`Button類。但是,你需要告訴`UI框架`UIFramework類使用新的子類按鈕代替默認按鈕。
為了實現這個功能,你可以根據基礎框架類開發子類`圓形按鈕 UI`UIWith-Round-Buttons,并且重寫其`create-Button`創建按鈕方法。基類中的該方法返回`按鈕`對象,而你開發的子類返回`圓形按鈕`對象。現在,你就可以使用`圓形按鈕 UI`類代替`UI框架`類。就是這么簡單!
#### 如果你希望復用現有對象來節省系統資源,而不是每次都重新創建對象,可使用工廠方法。
在處理大型資源密集型對象(比如數據庫連接、文件系統和網絡資源)時,你會經常碰到這種資源需求。
讓我們思考復用現有對象的方法:
1. 首先,你需要創建存儲空間來存放所有已經創建的對象。
2. 當他人請求一個對象時,程序將在對象池中搜索可用對象。
3. …然后將其返回給客戶端代碼。
4. 如果沒有可用對象,程序則創建一個新對象(并將其添加到對象池中)。
這些代碼可不少!而且它們必須位于同一處,這樣才能確保重復代碼不會污染程序。
可能最顯而易見,也是最方便的方式,就是將這些代碼放置在我們試圖重用的對象類的構造函數中。但是從定義上來講,構造函數始終返回的是**新對象**,其無法返回現有實例。
因此,你需要有一個既能夠創建新對象,又可以重用現有對象的普通方法。這聽上去和工廠方法非常相像。
## 實現方式
1. 讓所有產品都遵循同一接口。該接口必須聲明對所有產品都有意義的方法。
2. 在創建類中添加一個空的工廠方法。該方法的返回類型必須遵循通用的產品接口。
3. 在創建者代碼中找到對于產品構造函數的所有引用。將它們依次替換為對于工廠方法的調用,同時將創建產品的代碼移入工廠方法。
你可能需要在工廠方法中添加臨時參數來控制返回的產品類型。
工廠方法的代碼看上去可能非常糟糕。其中可能會有復雜的`switch`分支運算符,用于選擇各種需要實例化的產品類。但是不要擔心,我們很快就會修復這個問題。
4. 現在,為工廠方法中的每種產品編寫一個創建者子類,然后在子類中重寫工廠方法,并將基本方法中的相關創建代碼移動到工廠方法中。
5. 如果應用中的產品類型太多,那么為每個產品創建子類并無太大必要,這時你也可以在子類中復用基類中的控制參數。
例如,設想你有以下一些層次結構的類。基類`郵件`及其子類`航空郵件`和`陸路郵件`;?`運輸`及其子類`飛機`,`卡車`和`火車`。?`航空郵件`僅使用`飛機`對象,而`陸路郵件`則會同時使用`卡車`和`火車`對象。你可以編寫一個新的子類(例如`火車郵件`)來處理這兩種情況,但是還有其他可選的方案。客戶端代碼可以給`陸路郵件`類傳遞一個參數,用于控制其希望獲得的產品。
6. 如果代碼經過上述移動后,基礎工廠方法中已經沒有任何代碼,你可以將其轉變為抽象類。如果基礎工廠方法中還有其他語句,你可以將其設置為該方法的默認行為。
## 代碼示例
在本例中,**工廠方法**模式為創建社交網絡連接器提供接口,可用于進行登錄網絡、發帖和潛在的其他行為,而實現所有這些功能都無需客戶端代碼與特定社交網絡的特定類相耦合。
### PHP
**index.php:**
```
<?php
namespace RefactoringGuru\FactoryMethod\RealWorld;
/**
* The Creator declares a factory method that can be used as a substitution for
* the direct constructor calls of products, for instance:
*
* - Before: $p = new FacebookConnector();
* - After: $p = $this->getSocialNetwork;
*
* This allows changing the type of the product being created by
* SocialNetworkPoster's subclasses.
*/
abstract class SocialNetworkPoster
{
/**
* The actual factory method. Note that it returns the abstract connector.
* This lets subclasses return any concrete connectors without breaking the
* superclass' contract.
*/
abstract public function getSocialNetwork(): SocialNetworkConnector;
/**
* When the factory method is used inside the Creator's business logic, the
* subclasses may alter the logic indirectly by returning different types of
* the connector from the factory method.
*/
public function post($content): void
{
// Call the factory method to create a Product object...
$network = $this->getSocialNetwork();
// ...then use it as you will.
$network->logIn();
$network->createPost($content);
$network->logout();
}
}
/**
* This Concrete Creator supports Facebook. Remember that this class also
* inherits the 'post' method from the parent class. Concrete Creators are the
* classes that the Client actually uses.
*/
class FacebookPoster extends SocialNetworkPoster
{
private $login, $password;
public function __construct(string $login, string $password)
{
$this->login = $login;
$this->password = $password;
}
public function getSocialNetwork(): SocialNetworkConnector
{
return new FacebookConnector($this->login, $this->password);
}
}
/**
* This Concrete Creator supports LinkedIn.
*/
class LinkedInPoster extends SocialNetworkPoster
{
private $email, $password;
public function __construct(string $email, string $password)
{
$this->email = $email;
$this->password = $password;
}
public function getSocialNetwork(): SocialNetworkConnector
{
return new LinkedInConnector($this->email, $this->password);
}
}
/**
* The Product interface declares behaviors of various types of products.
*/
interface SocialNetworkConnector
{
public function logIn(): void;
public function logOut(): void;
public function createPost($content): void;
}
/**
* This Concrete Product implements the Facebook API.
*/
class FacebookConnector implements SocialNetworkConnector
{
private $login, $password;
public function __construct(string $login, string $password)
{
$this->login = $login;
$this->password = $password;
}
public function logIn(): void
{
echo "Send HTTP API request to log in user $this->login with " .
"password $this->password\n";
}
public function logOut(): void
{
echo "Send HTTP API request to log out user $this->login\n";
}
public function createPost($content): void
{
echo "Send HTTP API requests to create a post in Facebook timeline.\n";
}
}
/**
* This Concrete Product implements the LinkedIn API.
*/
class LinkedInConnector implements SocialNetworkConnector
{
private $email, $password;
public function __construct(string $email, string $password)
{
$this->email = $email;
$this->password = $password;
}
public function logIn(): void
{
echo "Send HTTP API request to log in user $this->email with " .
"password $this->password\n";
}
public function logOut(): void
{
echo "Send HTTP API request to log out user $this->email\n";
}
public function createPost($content): void
{
echo "Send HTTP API requests to create a post in LinkedIn timeline.\n";
}
}
/**
* The client code can work with any subclass of SocialNetworkPoster since it
* doesn't depend on concrete classes.
*/
function clientCode(SocialNetworkPoster $creator)
{
// ...
$creator->post("Hello world!");
$creator->post("I had a large hamburger this morning!");
// ...
}
/**
* During the initialization phase, the app can decide which social network it
* wants to work with, create an object of the proper subclass, and pass it to
* the client code.
*/
echo "Testing ConcreteCreator1:\n";
clientCode(new FacebookPoster("john_smith", "******"));
echo "\n\n";
echo "Testing ConcreteCreator2:\n";
clientCode(new LinkedInPoster("john_smith@example.com", "******"));
```
執行結果
```
Testing ConcreteCreator1:
Send HTTP API request to log in user john_smith with password ******
Send HTTP API requests to create a post in Facebook timeline.
Send HTTP API request to log out user john_smith
Send HTTP API request to log in user john_smith with password ******
Send HTTP API requests to create a post in Facebook timeline.
Send HTTP API request to log out user john_smith
Testing ConcreteCreator2:
Send HTTP API request to log in user john_smith@example.com with password ******
Send HTTP API requests to create a post in LinkedIn timeline.
Send HTTP API request to log out user john_smith@example.com
Send HTTP API request to log in user john_smith@example.com with password ******
Send HTTP API requests to create a post in LinkedIn timeline.
Send HTTP API request to log out user john_smith@example.com
```
- 設計模式系列
- 工廠方法模式
- 序言
- Windows程序注冊為服務的工具WinSW
- 基礎
- 安裝
- 開發規范
- 目錄結構
- 配置
- 快速入門
- 架構
- 請求流程
- 架構總覽
- URL訪問
- 容器和依賴注入
- 中間件
- 事件
- 代碼層結構
- 四個層次
- 路由
- 控制器
- 請求
- 響應
- 數據庫
- MySQL實時同步數據到ES解決方案
- 阿里云DTS數據MySQL同步至Elasticsearch實戰
- PHP中的MySQL連接池
- PHP異步非阻塞MySQL客戶端連接池
- 模型
- 視圖
- 注解
- @SpringBootApplication(exclude={DataSourceAutoConfiguration.calss})
- @EnableFeignClients(basePackages = "com.wotu.feign")
- @EnableAspectJAutoProxy
- @EnableDiscoveryClient
- 錯誤和日志
- 異常處理
- 日志處理
- 調試
- 驗證
- 驗證器
- 驗證規則
- 擴展庫
- 附錄
- Spring框架知識體系詳解
- Maven
- Maven和Composer
- 構建Maven項目
- 實操課程
- 01.初識SpringBoot
- 第1章 Java Web發展史與學習Java的方法
- 第2章 環境與常見問題踩坑
- 第3章 springboot的路由與控制器
- 02.Java編程思想深度理論知識
- 第1章 Java編程思想總體
- 第2章 英雄聯盟的小案例理解Java中最為抽象的概念
- 第3章 徹底理解IOC、DI與DIP
- 03.Spring與SpringBoot理論篇
- 第1章 Spring與SpringBoot導學
- 第2章 Spring IOC的核心機制:實例化與注入
- 第3章 SpringBoot基本配置原理
- 04.SprinBoot的條件注解與配置
- 第1章 conditonal 條件注解
- 第2章 SpringBoot自動裝配解析
- 05.Java異常深度剖析
- 第1章 Java異常分類剖析與自定義異常
- 第2章 自動配置Url前綴
- 06.參數校驗機制與LomBok工具集的使用
- 第1章 LomBok工具集的使用
- 第2章 參數校驗機制以及自定義校驗
- 07.項目分層設計與JPA技術
- 第1章 項目分層原則與層與層的松耦合原則
- 第2章 數據庫設計、實體關系與查詢方案探討
- 第3章 JPA的關聯關系與規則查詢
- 08.ORM的概念與思維
- 第1章 ORM的概念與思維
- 第2章 Banner等相關業務
- 第3章 再談數據庫設計技巧與VO層對象的技巧
- 09.JPA的多種查詢規則
- 第1章 DozerBeanMapper的使用
- 第2章 詳解SKU的規格設計
- 第3章 通用泛型Converter
- 10.令牌與權限
- 第1章 通用泛型類與java泛型的思考
- 常見問題
- 微服務
- demo
- PHP中Self、Static和parent的區別
- Swoole-Cli
- 為什么要使用現代化PHP框架?
- 公眾號
- 一鍵部署微信公眾號Markdown編輯器(支持適配和主題設計)
- Autodesigner 2.0發布
- Luya 一個現代化PHP開發框架
- PHPZip - 創建、讀取和管理 ZIP 文件的簡單庫
- 吊打Golang的PHP界天花板webman壓測對比
- 簡潔而強大的 YAML 解析庫
- 推薦一個革命性的PHP測試框架:Kahlan
- ServBay下一代Web開發環境
- 基于Websocket和Canvas實現多人協作實時共享白板
- Apipost預執行腳本如何調用外部PHP語言
- 認證和授權的安全令牌 Bearer Token
- Laradock PHP 的 Docker 完整本地開發環境
- 高效接口防抖策略,確保數據安全,避免重復提交的終極解決方案!
- TIOBE 6月榜單:PHP穩步前行,編程語言生態的微妙變化
- Aho-Corasick字符串匹配算法的實現
- Redis鍵空間通知 Keyspace Notification 事件訂閱
- ServBay如何啟用并運行Webman項目
- 使用mpdf實現導出pdf文件功能
- Medoo 輕量級PHP數據庫框架
- 在PHP中編寫和運行單元測試
- 9 PHP運行時基準性能測試
- QR碼生成器在PHP中的源代碼
- 使用Gogs極易搭建的自助Git服務
- Gitea
- webman如何記錄SQL到日志?
- Sentry PHP: 實時監測并處理PHP應用程序中的錯誤
- Swoole v6 Alpha 版本已發布
- Proxypin
- Rust實現的Redis內存數據庫發布
- PHP 8.4.0 Alpha 1 測試版本發布
- 121
- Golang + Vue 開發的開源輕量 Linux 服務器運維管理面板
- 內網穿透 FRP VS Tailscale
- 新一代開源代碼托管平臺Gitea
- 微服務系列
- Nacos云原生配置中心介紹與使用
- 輕量級的開源高性能事件庫libevent
- 國密算法
- 國密算法(商用密碼)
- GmSSL 支持國密SM2/SM3/SM4/SM9/SSL 密碼工具箱
- GmSSL PHP 使用
- 數據庫
- SQLite數據庫的Web管理工具
- 阿里巴巴MySQL數據庫強制規范
- PHP
- PHP安全測試秘密武器 PHPGGC
- 使用declare(strict_types=1)來獲得更健壯的PHP代碼
- PHP中的魔術常量
- OSS 直傳阿里騰訊示例
- PHP源碼編譯安裝APCu擴展實現數據緩存
- BI性能DuckDB數據管理系統
- 為什么別人可以是架構師!而我卻不是?
- 密碼還在用 MD5 加鹽?不如試試 password_hash
- Elasticsearch 在電商領域的應用與實踐
- Cron 定時任務入門
- 如何動態設置定時任務!而不是寫死在Linux Crontab
- Elasticsearch的四種查詢方式,你知道多少?
- Meilisearch vs Elasticsearch
- OpenSearch vs Elasticsearch
- Emlog 輕量級開源博客及建站系統
- 現代化PHP原生協程引擎 PRipple
- 使用Zephir編寫C擴展將PHP源代碼編譯加密
- 如何將PHP源代碼編譯加密,同時保證代碼能正常的運行
- 為什么選擇Zephir給PHP編寫動態擴展庫?
- 使用 PHP + XlsWriter實現百萬級數據導入導出
- Rust編寫PHP擴展
- 阿里云盤開放平臺對接進行文件同步
- 如何構建自己的PHP靜態可執行文件
- IM后端架構
- RESTful設計方法和規范
- PHP編譯器BPC 7.3 發布,成功編譯ThinkPHP8
- 高性能的配置管理擴展 Yaconf
- PHP實現雪花算法庫 Snowflake
- PHP官方現代化核心加密庫Sodium
- pie
- 現代化、精簡、非阻塞PHP標準庫PSL
- PHP泛型和集合
- 手把手教你正確使用 Composer包管理
- JWT雙令牌認證實現無感Token自動續期
- 最先進PHP大模型深度學習庫TransformersPHP
- PHP如何啟用 FFI 擴展
- PHP超集語言PXP
- 低延遲雙向實時事件通信 Socket.IO
- PHP OOP中的繼承和多態
- 強大的現代PHP高級調試工具Kint
- PHP基金會
- 基于webman+vue3高質量中后臺框架SaiAdmin
- 開源免費的定時任務管理系統:Gocron
- 簡單強大OCR工具EasyOCR在PHP中使用
- PHP代碼抽象語法樹工具PHP AST Viewer
- MySQL數據庫管理工具PHPMyAdmin
- Rust編寫的一款高性能多人代碼編輯器Zed
- 超高性能PHP框架Workerman v5.0.0-beta.8 發布
- 高并發系列
- 入門介紹及安裝
- Lua腳本開發 Hello World
- 執行流程與階段詳解
- Nginx Lua API 接口開發
- Lua模塊開發
- OpenResty 高性能的正式原因
- 記一次查找 lua-resty-mysql 庫 insert_id 的 bug
- 包管理工具OPM和LuaRocks使用
- 異步非阻塞HTTP客戶端庫 lua-resty-http
- Nginx 內置綁定變量
- Redis協程網絡庫 lua-resty-redis
- 動態HTML渲染庫 lua-testy-template
- 單獨的
- StackBlitz在線開發環境
- AI
- 基礎概念
- 12312
- 基礎鏡像的坑
- 利用phpy實現 PHP 編寫 Vision Transformer (ViT) 模型
- 語義化版本 2.0.0