前言
本篇是設計模式學習筆記的其中一篇文章,如對其他模式有興趣,可從該地址查詢設計模式學習筆記彙總地址
1. 簡介
上一篇部落格介紹了簡單工廠模式,簡單工廠模式存在一個很嚴重的問題:
就是當系統需要引入新產品時,由於靜態工廠方法通過所傳入引數的不同來建立不同的產品,這必定要修改工廠類的原始碼,這將違背"開閉原則".
本篇將要介紹的工廠方法模式可以規避這個缺點.
2. 工廠方法模式
工廠方法模式又簡稱為工廠模式,又可稱作虛擬構造器模式或多型工廠模式.工廠方法模式是一種建立型模式.
2.1 簡單工廠方法模式的弊端
介紹工廠方法模式之前,我們來先看看簡單工廠方法模式的弊端.
簡單工廠模式最大的缺點是當有新產品要加入到系統中時,需要在其中加入必要的業務邏輯,這違背了"開閉原則".
此外,在簡單工廠模式中,所有的產品都由同一個工廠建立,工廠類職責較重,業務邏輯較為複雜,具體產品與工廠類之間的耦合度高,嚴重影響了系統的靈活性和擴充套件性,而工廠方法模式則可以很好地解決這一問題.
2.2 工廠方法模式定義
定義一個用於建立物件的介面,讓子類決定哪一個類例項化.
工廠方法模式讓一個類的例項化延遲到其子類.
2.3 結構圖
2.4 角色介紹
- Product(抽象產品)
- ConcreteProduct(具體產品)
- Factory(抽象工廠)
- ConcreteFactory(具體工廠)
2.4.1 Product(抽象產品)
它是定義產品的介面,是工廠方法模式所建立物件的超型別,也就是產品物件的公共父類
2.4.2 ConcreteProduct(具體產品)
它實現了抽象產品介面,某種型別的具體產品由專門的具體工廠建立,具體工廠和具體產品之間一一對應.
2.4.3 Factory(抽象工廠)
在抽象工廠類中,宣告瞭工廠方法(Factory Method),用於返回一個產品. 抽象工廠是工廠方法模式的核心,所有建立物件的工廠類都必須實現該介面.
2.4.4 ConcreteFactory(具體工廠)
它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端呼叫,返回一個具體產品類的例項.
2.5 程式碼
- 抽象工廠
- 具體工廠
- 客戶端
2.5.1 抽象工廠
與簡單工廠模式相比,工廠方法模式最要的區別是引入了抽象工廠角色,抽象工廠可以是介面,也可以是抽象類或具體類.
/**
* @author liuboren
* @Title: 工廠介面
* @Description: 工廠模式中的工廠介面
* @date 2019/7/15 14:20
*/
public interface Factory {
Product factoryMethod();
}
2.5.2 具體工廠
在抽象工廠中宣告瞭工廠方法但並未實現工廠方法,具體產品物件的建立由其子類負責,客戶端針對抽象工廠程式設計,可以在執行時再指定具體工廠類,具體工廠類實現了工廠方法,不同的具體工廠可以建立不同的具體產品.
/**
* @author liuboren
* @Title: 具體工廠
* @Description: 根據需要建立具體的產品類
* @date 2019/7/15 14:21
*/
public class ConcreteFactory implements Factory {
@Override
public Product factoryMethod() {
return new ConcreteProduct();
}
}
在實際使用時,具體工廠類在實現工廠方法時除了建立具體產品物件之外,還可以負責產品物件的初始化工作以及一些資源和環境配置工作,例如連線資料庫、建立檔案等
2.5.3 客戶端
在客戶端程式碼中,只關心工廠類即可,不同的具體工廠可以建立不同的產品.
/**
* @author liuboren
* @Title: 客戶端類
* @Description: 實際調工廠方法
* @date 2019/7/15 14:28
*/
public class Client {
public static void main(String[] args) {
Factory factory = new ConcreteFactory();
Product product = factory.factoryMethod();
}
}
3. 實戰
以虛構業務的形式來看看簡單工廠模式存在哪些問題以及工廠方法模式是如何解決簡單工廠模式存在的問題的.
開發一個系統執行日誌記錄器.
該記錄器可以通過多種途徑儲存系統的執行日誌,例如通過檔案記錄或資料庫記錄,使用者可以通過更改配置檔案自行更換日誌記錄方式.
需要對日記記錄器進行一些初始化工作,初始化引數的設定過程較為複雜,而且某些引數的設定有嚴格的先後次序,否則可能記錄失敗.
3.1 設計要點
- 需要封裝日誌記錄器的初始化過程
- 使用者可能需要更換日誌記錄方式
3.1.1 需要封裝日誌記錄器的初始化過程
這些初始化工作較為複雜,例如需要初始化其他相關的類,還有可能需要讀取配置檔案(例如連線資料庫或建立檔案),導致程式碼較長,如果將它們都寫在建構函式中,會導致建構函式龐大,不利於程式碼的修改和維護.
3.1.2 使用者可能需要更換日誌記錄方式
在客戶端程式碼中需要提供一種靈活的方式來選擇日誌記錄器,儘量在不修改原始碼的基礎上更換或者增加日誌記錄方式.
3.2 使用簡單工廠模式解決問題
3.2.1 結構圖
3.2.2 工廠類
/**
* @author liuboren
* @Title: 日誌工廠
* @Description: 使用簡單工廠方法模式解決需求
* @date 2019/7/15 15:02
*/
public class LoggerFactory {
//靜態工廠方法
public static Logger createLogger(String args){
Logger logger;
if("db".equalsIgnoreCase(args)){
logger = new DatabaseLogger();
}else if("file".equalsIgnoreCase(args)){
logger = new FileLogger();
}else{
logger = null;
}
return logger;
}
}
上述程式碼簡化了初始化過程,使用簡單工廠方法雖然實現了物件的建立和使用分離,但是有其它問題存在.
3.2.3 使用簡單工廠模式存在的問題
- 工廠類過於龐大,包含了大量的if...else...程式碼,導致維護和測試難度增大.
- 系統擴充套件不靈活,如果增加新型別的日誌記錄器,必須修改靜態工廠方法的業務邏輯,違反了"開閉原則"
3.3 使用工廠方法模式
3.3.1 結構圖
3.3.2 程式碼
日誌工廠類:
/**
* @author liuboren
* @Title: 抽象工廠類
* @Description:
* @date 2019/7/15 15:21
*/
public abstract class AbstractLoggerFactory {
public abstract Logger createLogger();
}
檔案日誌工廠類:
/**
* @author liuboren
* @Title: 檔案日誌工廠
* @Description: 建立檔案工廠類
* @date 2019/7/15 15:23
*/
public class FileLoggerFactory extends AbstractLoggerFactory {
@Override
public Logger createLogger() {
return new FileLogger();
}
}
資料庫日誌工廠類:
/**
* @author liuboren
* @Title: 資料庫日誌工廠方法
* @Description: 建立資料庫日誌類
* @date 2019/7/15 15:26
*/
public class DatabaseLoggerFactory extends AbstractLoggerFactory {
@Override
public Logger createLogger() {
//省略連線資料庫程式碼、初始化資料庫程式碼
return new DatabaseLogger();
}
}
日誌類:
/**
* @author liuboren
* @Title: 日之類
* @Description:
* @date 2019/7/15 15:20
*/
public interface Logger {
void wirteLog();
}
檔案日誌類:
/**
* @author liuboren
* @Title: 檔案日誌類
* @Description:
* @date 2019/7/15 15:06
*/
public class FileLogger implements Logger {
@Override
public void wirteLog() {
System.out.println("檔案日誌記錄日誌..");
}
}
資料庫日誌類:
/**
* @author liuboren
* @Title: 資料庫日誌類
* @Description:
* @date 2019/7/15 15:05
*/
public class DatabaseLogger implements Logger {
@Override
public void wirteLog() {
System.out.println("資料庫日誌記錄日誌..");
}
}
客戶端:
/**
* @author liuboren
* @Title: 客戶端
* @Description:
* @date 2019/7/15 15:30
*/
public class Client {
public static void main(String[] args) {
AbstractLoggerFactory loggerFactory = new FileLoggerFactory();
Logger logger = loggerFactory.createLogger();
logger.wirteLog();
}
}
3.3.3 工廠方法模式優化
使用反射 + xml檔案使工廠方法生成的Logger型別變為可配置的.
xml檔案:
<?xml version="1.0" encoding="utf-8" ?>
<config>
<!--傳入完全限定名才能獲取到類-->
<className>creational.factory.factory.optimize.FileLogger</className>
</config>
xml讀取工具類:
/**
* @author liuboren
* @Title: XML工具類
* @Description: 讀取XMl檔案配置
* @date 2019/7/15 15:50
*/
public class XMLUtil {
public static Object getBean(){
try {
//建立DOM文件物件
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("Factory_config.xml"));
//獲取包含類名的文字節點
NodeList nl = doc.getElementsByTagName("className");
Node classNode = nl.item(0).getFirstChild();
String cName = classNode.getNodeValue();
//通過類名生成例項物件並將其返回
Class c = Class.forName(cName);
Object obj = c.newInstance();
return obj;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
客戶端:
/**
* @author liuboren
* @Title: 客戶端
* @Description:
* @date 2019/7/15 15:30
*/
public class Client {
public static void main(String[] args) {
AbstractLoggerFactory loggerFactory = new DatabaseLoggerFactory();
Logger logger = loggerFactory.createLogger();
logger.wirteLog();
//測試通過反射方式生成物件
Logger reflectLogger = (Logger) XMLUtil.getBean();
reflectLogger.wirteLog();
}
}
3.3.4 使用反射優化後的工廠類使用方式
- 新的日誌記錄器需要繼承抽象日誌記錄器Logger
- 增加新的LoggerFactory對應增加一個新的具體日誌記錄器工廠,繼承抽象日誌記錄器工廠LoggerFactory,並實現其中的工廠方法createLogger(),設定好初始化引數和環境變數,返回具體日誌記錄器物件
- 修改配置檔案LoggerFactory_config.xml,將新增的具體日誌記錄器工廠類的類名字串替換原有工廠類類名字串.
- 編譯新增的具體日誌記錄器和具體日誌記錄器工廠類,執行客戶端測試類即可使用心得日誌記錄方式,而原有類庫無須做任何修改,完全符合"開閉原則",通過上述重構可以使得系統更加靈活,由於很多設計模式都關注系統的可擴充套件性和靈活性,因此都定義了抽象層,在抽象層宣告業務方法,而將業務方法的實現放在實現層中.
3.4 過載工廠方法
需求升級,需要使用杜仲方式來初始化日誌記錄器.
- 使用預設實現
- 為資料庫日記提供資料庫連線字串,為檔案日誌記錄器提供檔案路徑
- 將引數封裝到Object型別的物件中,通過Object物件將配置引數傳入工廠類.
結構圖:
3.5 隱藏工廠方法
有時候,為了進一步簡化客戶端的使用,還可以對客戶端隱藏工廠方法,此時,在工廠類中將直接呼叫產品類的業務方法,客戶端無須呼叫工廠方法建立產品
3.5.1 結構圖
3.5.2 程式碼
xml:
<?xml version="1.0" encoding="utf-8" ?>
<config>
<!--傳入完全限定名才能獲取到類-->
<className>creational.factory.factory.hide.DatabaseLoggerFactory</className>
</config>
抽象工廠:
/**
* @author liuboren
* @Title: 抽象工廠類
* @Description:
* @date 2019/7/15 15:21
*/
public abstract class AbstractLoggerFactory {
public abstract Logger createLogger();
public void wirteLog(){
Logger logger = this.createLogger();
logger.wirteLog();
}
}
客戶端:
/**
* @author liuboren
* @Title: 客戶端
* @Description:
* @date 2019/7/15 15:30
*/
public class Client {
public static void main(String[] args) {
AbstractLoggerFactory abstractLoggerFactory= (AbstractLoggerFactory) XMLUtil.getBean();
abstractLoggerFactory.wirteLog();
}
}
4. 總結
4.1 優點
- 隱藏建立細節
- 將建立物件的細節封裝在具體工廠內部
- 易於擴充套件
4.1.1 隱藏建立細節
在工廠方法模式中,工廠方法用來建立客戶所需要的產品,同時還向客戶隱藏了那種具體產品類將被例項化這一細節,使用者只需要關心所需產品對應的工廠,無須關心建立細節,甚至無須知道具體產品類的類名
4.1.2 將建立物件的細節封裝在具體工廠內部
基於 工廠角色和產品角色的多型性設計是工廠方法模式的關鍵.它能夠讓工廠可以自主確定建立何種產品物件,而如何建立這個物件的細節則完全封裝在具體工廠內部.
工廠方法模式之所以又被成為多型工廠模式,就正是因為所有的具體工廠類都具有統一抽象父類.
4.1.3 易於擴充套件
在系統加入新產品時無須修改抽象工廠和抽象產品提供的介面,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要新增一個具體工廠和具體產品就可以了.
這樣系統的可擴充套件性也就變得非常好,完全符合"開閉原則"
4.2 缺點
- 增加了系統的複雜度&效能開銷
- 增加了系統的抽象性和難理解度
4.2.1 增加了系統的複雜度&效能開銷
在新增新廠品時,需要編寫新的具體產品類,還要提供與之相對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的複雜度,有更多的類需要編譯和執行,會給系統帶來一些額外的開銷
4.2.2 增加了系統的抽象性和難理解度
由於考慮到系統的可擴充套件性,需要引入抽象層,在客戶端程式碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度,且在實現時需要用到DOM、反射等技術,增加了系統的實現難度.
4.3 適用場景
- 客戶端不知道它所需要的物件的類
- 抽象工廠類通過其子類來指定建立哪個物件
4.3.1 客戶端不知道它所需要的物件的類
在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品物件由具體工廠類建立,可將具體工廠類的類名儲存在配置檔案或資料庫中.
4.3.2 抽象工廠類通過其子類來指定建立哪個物件
在工廠方法模式中,對於抽象工廠類只需要提供一個建立產品的介面,而由其子類來決定具體需要建立的物件,利用物件導向的多型性和里氏代換原則,在程式執行時,子類物件將覆蓋弗雷獨享,從而使得系統更容易擴充套件.