設計模式-工廠方法模式(FactoryMethod)-Java

設計模式-工廠方法模式-Java


目錄




內容

1、示例案例

  簡單工廠模式雖然簡單,但存在一個很嚴重的問題。當系統中需要引入新產品時,由於靜態工廠方法通過所傳入參數的不同來創建不同的產品,者必定要修改工廠類的源代碼,將違背“開閉原則”,如何實現增加新產品而不影響已有代碼?工廠方法模式應運而生,本文將介紹第二種工廠模式-工廠方法模式。

1.1、日誌記錄器的設計

  Sunny軟件公司欲開發一個系統運行日誌記錄器(Logger),該記錄器可以通過多種途徑保存系統的運行日誌,如通過文件記錄或數據庫記錄,用戶可以通過修改配置文件靈活地更換日誌記錄方式。在設計各類日誌記錄器時,Sunny公司的開發人員發現需要對日誌記錄器進行一些初始化工作,初始化參數的設置過程較爲複雜,而且某些參數的設置由嚴格的先後次序,否則可能會發生記錄失敗。如何封裝記錄器的初始化過程並保證多種記錄器切換的靈活性是Sunny公共開發人員面臨的要給難題。

  公司的開發人員通過對該需求進行分析,發現該日誌記錄器由兩個設計要點:

  1. 需要封裝日誌記錄器的初始化過程,這些初始化工作較爲複雜,例如需要初始化其他相關的類,還有可能需要讀取配置文件(例如連接數據庫或者創建文件),導致代碼較長,如果將它們都寫在構造函數中,會導致構造函數龐大,不利於代碼的修改和維護。
  2. 用戶可能需要更換日誌記錄方式,在客戶端代碼中需要提供一種靈活的方式來選擇日誌記錄器,儘量在不修改源代碼的基礎上更換或者增加日誌記錄方式。

 &emsp公司開發人員最初使用簡單工廠模式對日誌記錄器進行了設計,初始結構如圖1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yO6ahhim-1591265368987)(./images/logger_simple.png)]
日記記錄器,Logger是抽象日誌記錄器接口,其子類爲具體日誌記錄器。其中,工廠類LoggerFactory代碼片段如下所示:

	// 日誌記錄器工廠
	class LoggerFactory {
		// 靜態工廠方法
		public static Logger createLogger(String args) {
			if(args.equalsIgnoreCase("db")) {
				// 連接數據庫,代碼省略
				// 創建數據庫日誌記錄器,代碼省略
				Logger logger = new DatabaseLogger();
				// 初始化數據庫日誌記錄器,代碼省略
				return logger;
			}else if(args.equalsIgnoreCase("file")) {
				// 創建日誌文件
				// 創建日誌文件記錄器對象
				Logger logger = new FileLogger();
				// 初始化文件日誌記錄器,代碼省略
				return logger;
			}else {
				return null;
			}
		}
	}

  爲了突出設計重點,我們對上述代碼進行了簡化,省略了具體日誌記錄器類的初始化代碼。在LoggerFactory類中提供了靜態工廠方法creatLogger(),用於根據所傳入參數創建各種不同類型的日記記錄器。通過使用簡單工廠模式,我們將日記記錄器對象的創建和使用分離,客戶端只需要使用由工廠類創建的日誌記錄器對象即可,無須關心對象的創建過程,但是我們發現,雖然簡單工廠模式實現了對象的創建和使用分離,但是仍然存在如下兩個問題:
1.工廠類過於龐大,包含了大量的if…else…代碼,導致維護和測試難度增大;
2.系統擴展不靈活,如果增加新類型的日誌記錄器,必須修改靜態公共方法的業務邏輯,違反了“開閉原則”。

  如何解決這兩個問題,提供一種簡單工廠模式的改進方案?這就是本文所介紹的工廠方法嗎模式的動機之一。

2、工廠方法模式概述

  在簡單工廠模式中只提供一個工廠類,該工廠類處於對產品類進行實例化的中心位置,它需要知道每一個產品對象的創建細節,並決定何時實例化哪一個產品類。簡單工廠模式最大缺點是當由新產品要加入到系統中時,必須修改工廠類,需要在其中加入必要的業務邏輯,這違背了“開閉原則”。此外,在簡單工廠模式中,所有的產品都由同一個工廠創建,工廠類職責較重,業務邏輯較爲複雜,具體產品與工廠類之間的耦合度高,嚴重影響了系統的靈活性和擴展性,而工廠方法模式則可以很好地解決這一問題。

  在工廠方法模式中,我們不再提供一個統一的工廠類來創建索引的產品對象,而是針對不同的產品提供不同的工廠,系統提供一個與產品等級結構對於的工廠等級結構。工廠方法模式定義如下:

2.1、工廠方法模式定義

定義一個用於創建對象的接口,讓子類決定哪一個類實例化。工廠方法模式(Factory Method Pattern)讓一個類的實例化延遲到其子類。工廠方法模式又簡稱工廠模式,又可稱作虛擬構造器模式或多態工廠模式。工廠方法模式是一種類創建型模式。

2.2、工廠方法模式要點

  工廠方法模式提供一個抽象工廠接口來聲明抽象工廠方法,而由其子類來具體實現工廠方法,差個年級具體的產品對象。工廠方法模式結構如圖2所示:

圖2工廠方法模式結構圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ustzfJlo-1591265368991)(./images/model_factoryMethod.png)]

2.3、工廠方法模式結構圖中角色

  • Product(抽象產品):它是定義產品的接口,是工廠方法模式所場景對象的超模型,也就是產品對象的公共父類(接口)。
  • ConcreteProduct(具體產品):它實現了抽象產品接口,某種類型的具體產品由專門的具體工廠創建,具體工廠和具體產品之間一一對應。
  • Factory(抽象工廠):在抽象工廠中,聲明瞭工廠方法(Factory Method),用於返回一個產品。抽象工廠是工廠方法模式的核心,索引創建對象的工廠類都必須實現該接口。
  • ConcerteFactory(具體工廠):它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端調用,返回一個具體產品類的實例。

2.4、工廠方法模式使用

&與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠可以是接口,也可以是抽象類或者具體類,其典型代碼嗎如下所示:

	interface Factory {
		public Product factoryMethod();
	}

  在抽象工廠中聲明瞭工廠方法但是並未實現工廠方法,具體產品對象的創建由其子類負責,客戶端針對抽象工廠編程,可在運行時在指定具體工廠類,具體工廠類實現了工廠方法,不同的具體工廠可以創建不同的具體產品,其典型代碼如下所示:

	class ConcreteFactory implements Factory {
		public Product factoryMethod() {
			return new ConcreteProduct();
		}
	}

  在實際使用時,具體工廠類在實現工廠方法時除了創建具體產品對象之外,還可以負責產品對象的初始化工作以及一些資源和環境配置工作,例如數據庫連接、創建文件等。

  在客戶端代碼中,只需關心工廠類即可,不同的具體工廠可以創建不同的產品,典型的客戶端類代碼片段如下所示:

	...
	Factory factory;
	factory = new ConcreteFactory();
	Product product;
	product = factory.factoryMethod();
	...

  可以通過配置文件來存儲具體工廠類ConcreteFactory的類名,更新新的具體工廠時無須修改源代碼,系統擴展更爲方便。

2.5、思考

  工廠方法模式中的工廠方法能否爲靜態方法?爲什麼?

3、工廠方法模式完整解決方案

  Sunny公司開發人員決定使用工廠方法模式來設計日誌記錄器,其基本結構如圖3-1所示:

圖3-1 日誌記錄器結構圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mE4beA6X-1591265368993)(./images/logger_factory.png)]
在圖3-1中,Logger接口充當抽象產品,其子類FileLogger和DatabaseLogger充當具體產品,LoggerFactory接口充當抽象工廠,其子類FileLoggerFactory和DatabaseLoggerFactory充當具體工廠。完整代碼如下所示:

// 日誌記錄器接口:抽象產品
	
	package factoryMethod.first;

	// 日誌記錄器接口
	public interface Logger {
		void writeLog();
	}

// 數據庫日誌記錄器:具體產品
	package factoryMethod.first;

	public class DatabaseLogger implements Logger{

		@Override
		public void writeLog() {
			System.out.println("數據庫日誌記錄");	
		}

	}

	
// 文件日誌記錄器:具體產品
	
	package factoryMethod.first;

	public class FileLogger implements Logger{

		@Override
		public void writeLog() {
			System.out.println("文件日誌記錄");
		}

	}

// 日誌記錄器工廠接口:抽象工廠
	package factoryMethod.first;

	public class FileLoggerFactory implements LoggerFactory{

		@Override
		public Logger creatLogger() {
			// 連接文件,代碼省略
			// 創建文件日誌記錄器
			Logger logger = new FileLogger();
			// 文件日誌記錄器初始化,代碼省略
			return logger;
		}

	}

// 數據庫日誌記錄器工廠:具體工廠

	package factoryMethod.first;

	public class DatabaseLoggerFactory implements LoggerFactory{

		@Override
		public Logger creatLogger() {
			// 連接數據庫,代碼省略
			// 創建數據庫日誌記錄器
			Logger logger = new DatabaseLogger();
			// 數據庫日誌記錄器初始化,代碼省略
			return logger;
		}

	}

// 文件日誌記錄器工廠:具體工廠

	package factoryMethod.first;

	public class FileLoggerFactory implements LoggerFactory{

		@Override
		public Logger creatLogger() {
			// 連接文件,代碼省略
			// 創建文件日誌記錄器
			Logger logger = new FileLogger();
			// 文件日誌記錄器初始化,代碼省略
			return logger;
		}

	}

// 客戶端
	package factoryMethod.first;

	public class Client {
		public static void main(String[] args) {
			LoggerFactory lf = new DatabaseLoggerFactory();
			Logger logger = lf.creatLogger();
			logger.writeLog();
		}
	}
// 測試結果
數據庫日誌記錄

4、方案的改進

  爲了讓系統具有更好的靈活性和可擴展性,Sunny公司開發人員決定對日誌記錄器客戶端代碼進行重構,使得可以在不修改任何客戶端代碼的基礎上更換或增加新的日誌記錄方式。

&在客戶端代碼中將不再使用new關鍵字來創建工程對象,而是將具體工廠類的類名存儲在配置文件中,通過讀取配置文件獲取文件類名字符串,在使用反射機制,根據類名字符串生產對象。在整個實現過程中需要用到兩個技術:Java反射和配置文件讀取。

4.1、反射與配置文件

  • 配置文件logger.properties內容

      className=factoryMethod.first.FileLoggerFactory
    
  • 創建工具類生成具體的日誌記錄器工廠

      // 生成具體日誌記錄器工廠對象方法類
      
      	package factoryMethod.first;
    
      import java.util.Properties;
    
      public class Utils {
      	public static LoggerFactory getLoggerFactory() {
      		try {
      		LoggerFactory factory = null;
      		Properties prop = new Properties();
      		prop.load(Utils.class.getClassLoader().getResourceAsStream("logger.properties"));
      		String className = prop.getProperty("className");
      		factory = (LoggerFactory)(Class.forName(className).newInstance());
      		return factory;
      		}catch (Exception e) {
      			e.printStackTrace();
      			return null;
      		}
      	}
      }
    

4.2、修改客戶端代碼

// 客戶端代碼
	package factoryMethod.first;

	public class Client {
		public static void main(String[] args) {
			LoggerFactory lf = Utils.getLoggerFactory();
			Logger logger = lf.creatLogger();
			logger.writeLog();
		}
	}

// 測試結果:
文件日誌記錄

4.3、新增日誌記錄方式

  • 步驟:
    1. 新的日誌記錄方式需要實現(繼承)日誌記錄器Logger
    2. 對應增加新的具體日誌記錄器工廠,繼承抽象日誌記錄工廠LoggerFactory,並實現其他的供方法createLogger(),設置好初始化參數和環境變量,返回體貼的日誌記錄器對象;
    3. 修改配置文件logger.properties,將新增的日誌記錄器工廠類名字符串替換原有工廠類類名記錄字符串;

4.4、重載的工廠方法

  Sunny公司開發人員通過進一步分析,發現可以通過多種方式來初始化日誌記錄器,例如可以爲各種日誌記錄器提供默認實現;還可以爲數據庫日誌記錄器提供數據庫連接字符串,爲文件日誌記錄器提供文件路徑;也可以提供一組重載的工廠方法,以不同的方式對產品對象進行創建。當然,對於同一個具體工廠而已,無論使用哪個工廠方法,創建的產品類型均喲啊相同。如圖4.4-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IC88nxwC-1591265368994)(./images/overload_factoryMethod.png)]
  引入重載方法後,

  • 抽象工廠LoggerFactory的代碼4.4-1修改如下:

      package factoryMethod.first;
    
      // 日誌記錄器工廠接口
      public interface LoggerFactory {
      	Logger createLogger();
      	Logger createLogger(String args);
      	Logger createLogger(Object obj);
      }
    
  • 具體工廠類DatabaseLoggerFactory代碼4.4-2:

      package factoryMethod.first;
    
      public class DatabaseLoggerFactory implements LoggerFactory{
    
      	@Override
      	public Logger createLogger() {
      		// 使用默認方式連接數據庫,代碼省略
      		// 創建數據庫日誌記錄器
      		Logger logger = new DatabaseLogger();
      		// 數據庫日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
      	@Override
      	public Logger createLogger(String args) {
      		// 使用參數args作爲連接字符串連接數據庫,代碼省略
      		// 創建數據庫日誌記錄器
      		Logger logger = new DatabaseLogger();
      		// 數據庫日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
      	@Override
      	public Logger createLogger(Object obj) {
      		// 使用封裝在參數obj中連接字符串連接數據庫,代碼省略
      		// 創建數據庫日誌記錄器
      		Logger logger = new DatabaseLogger();
      		// 數據庫日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
      }
    

其他具體工廠類省略。

4.5、工廠方法的隱藏

  有時候,爲了進一步簡化客戶端的使用,還可以對客戶端隱藏工廠方法,此時,在工廠類中將直接調用產品類的業務方法,客戶端無須調用工廠方法差個年級產品,直接通過工廠即可使用所創建的對象中的業務方法。

  如果對客戶端隱藏工廠方法,日誌記錄器的結構圖修改爲圖4.5-1:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pEYoItGI-1591265368995)(./images/factoryMethod_yincang.png)]

  • 抽象工廠類LoggerFactory代碼修改4.5-1:

      package factoryMethod.second;
    
      // 抽象日誌記錄器工廠類
      public abstract class LoggerFactory {
      	public abstract Logger createLogger();
      	public abstract Logger createLogger(String args);
      	public abstract Logger createLogger(Object obj);
      	public void writeLog() {
      		Logger logger = this.createLogger();
      		logger.writeLog();
      	}
      }
    
  • 數據庫日誌記錄工廠DatabaseLoggerFactory代碼4.5-2:

      package factoryMethod.second;
    
      public class DatabaseLoggerFactory extends LoggerFactory{
    
    
      	public Logger createLogger() {
      		// 使用默認方式連接數據庫,代碼省略
      		// 創建數據庫日誌記錄器
      		Logger logger = new DatabaseLogger();
      		// 數據庫日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
    
      	public Logger createLogger(String args) {
      		// 使用參數args作爲連接字符串連接數據庫,代碼省略
      		// 創建數據庫日誌記錄器
      		Logger logger = new DatabaseLogger();
      		// 數據庫日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
    
      	public Logger createLogger(Object obj) {
      		// 使用封裝在參數obj中連接字符串連接數據庫,代碼省略
      		// 創建數據庫日誌記錄器
      		Logger logger = new DatabaseLogger();
      		// 數據庫日誌記錄器初始化,代碼省略
      		return logger;
      	}
      }
    
  • 文件日誌記錄器工廠FileLoggerFactoryd代碼4.5-3:

      package factoryMethod.second;
    
      public class FileLoggerFactory extends LoggerFactory{
    
      	public Logger createLogger() {
      		// 使用默認方式連接文件,代碼省略
      		// 創建文件日誌記錄器
      		Logger logger = new FileLogger();
      		// 文件日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
      	public Logger createLogger(String args) {
      		// 使用字符串參數args連接文件,代碼省略
      		// 創建文件日誌記錄器
      		Logger logger = new FileLogger();
      		// 文件日誌記錄器初始化,代碼省略
      		return logger;
      	}
    
      	@Override
      	public Logger createLogger(Object obj) {
      		// 使用封裝在obj中的參數連接文件,代碼省略
      		// 創建文件日誌記錄器
      		Logger logger = new FileLogger();
      		// 文件日誌記錄器初始化,代碼省略
      		return logger;
      	}
      }
    
  • 客戶端Client代碼4.5-4:

      package factoryMethod.second;
    
      public class Client {
      	public static void main(String[] args) {
      		LoggerFactory factory = Utils.getLoggerFactory();
      		factory.writeLog();
      	}
      }
    
  • 配置文件logger2.properties內容:

      className=factoryMethod.second.FileLoggerFactory
    
  • 測試結果:

      文件日誌記錄
    

5、總結

  工廠方法模式是簡單工廠方法模式的延伸,它繼承了簡單工廠模式的優點,同時還祕彌補了簡單工廠方法模式的不足。工廠方法模式是使用頻率最高的設計模式之一,是很多開源框架和API類庫的和核心模式。

5.1、優缺點

  • 主要優點:

    1. 工廠方法用來創建客戶所需的產品,同時還向客戶隱藏了那種具體產品類將被實例化這一細節,用戶只需要關心所需產品對應的工廠,無須關心創建細節,甚至無須指定具體產品類的類名。
    2. 基於工廠角色和產品角色的多態性設計是工廠方法模式的關機。它能夠讓工廠可以自主決定創建何種產品對象,而如何創建這個對象的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱爲多態工廠模式,就正是因爲所有的具體工廠類都具有同一抽象父類。
    3. 在系統中加入新產品是,無須修改抽象工廠和抽象產品提供的接口,無需修改客戶端,無無須修改其他的具體工廠和具體產品,而只需添加一個具體工廠和具體產品就可以了,這一,系統的可擴展性也就變得非常好,完全符合“開閉原則”
  • 主要缺點:

    1. 在添加新產品是,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中的類的個數將成對增加,在一定程度上增加了系統的複雜都,又更多的類需要編譯和運行,這一會給系統帶來一些額外的開銷。
    2. 由於考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增增加了系統的抽象性和理解難度,且在實現時需要配置文件加載、反射等技術,增加了系統的實現難度。

5.2、適用場景

  1. 在客戶端不知道它所需要的對象的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品對象由具體工廠類差個年級,可將具體工廠類的類名存儲在配置文件或者數據庫中。
  2. 抽象工廠類通過其子類來指定差個年級那個對象。在工廠方法模式中,對應抽象工廠類只需要提供一個創建產品的接口,而由其子類來確定具體要創建的對象,利用面向對象的多態性和李氏代換原則在程序運行時,子類對象將覆蓋父類對象,從而使得系統更容易擴展。

後記

  參考文獻:Java設計模式(劉偉).pdf。持續更新,歡迎交流,本人QQ:806797785

前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端JAVA源代碼地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章