設計模式之美 - 44 | 工廠模式(上):我爲什麼說沒事不要隨便用工廠模式創建對象?

這系列相關博客,參考 設計模式之美

上幾節課我們講了單例模式,今天我們再來講另外一個比較常用的創建型模式:工廠模式(Factory Design Pattern)。

一般情況下,工廠模式分爲三種更加細分的類型:簡單工廠、工廠方法和抽象工廠。不過,在 GoF 的《設計模式》一書中,它將簡單工廠模式看作是工廠方法模式的一種特例,所以工廠模式只被分成了工廠方法和抽象工廠兩類。實際上,前面一種分類方法更加常見,所以,在今天的講解中,我們沿用第一種分類方法。

在這三種細分的工廠模式中,簡單工廠、工廠方法原理比較簡單,在實際的項目中也比較常用。而抽象工廠的原理稍微複雜點,在實際的項目中相對也不常用。所以,我們今天講解的重點是前兩種工廠模式。對於抽象工廠,你稍微瞭解一下即可。

除此之外,我們講解的重點也不是原理和實現,因爲這些都很簡單,重點還是帶你搞清楚應用場景:什麼時候該用工廠模式?相對於直接 new 來創建對象,用工廠模式來創建究竟有什麼好處呢?

話不多說,讓我們正式開始今天的學習吧!

簡單工廠(Simple Factory)

首先,我們來看,什麼是簡單工廠模式。我們通過一個例子來解釋一下。

在下面這段代碼中,我們根據配置文件的後綴(json、xml、yaml、properties),選擇不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),將存儲在文件中的配置解析成內存對象 RuleConfig。

public class RuleConfigSource {
	public RuleConfig load(String ruleConfigFilePath) {
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		IRuleConfigParser parser = null;
		if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
			parser = new JsonRuleConfigParser();
		} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
			parser = new XmlRuleConfigParser();
		} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
			parser = new YamlRuleConfigParser();
		} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
			parser = new PropertiesRuleConfigParser();
		} else {
			throw new InvalidRuleConfigException(
						"Rule config file format is not supported: " + ruleConfigFilePa
		}
		String configText = "";
		//從ruleConfigFilePath文件中讀取配置文本到configText中
		RuleConfig ruleConfig = parser.parse(configText);
		return ruleConfig;
	}
	
	private String getFileExtension(String filePath) {
		//...解析文件名獲取擴展名,比如rule.json,返回json
		return "json";
	}
}

在“規範和重構”那一部分中,我們有講到,爲了讓代碼邏輯更加清晰,可讀性更好,我們要善於將功能獨立的代碼塊封裝成函數。按照這個設計思路,我們可以將代碼中涉及 parser 創建的部分邏輯剝離出來,抽象成 createParser() 函數。重構之後的代碼如下所示:

	public RuleConfig load(String ruleConfigFilePath) {
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		IRuleConfigParser parser = createParser(ruleConfigFileExtension);
		if (parser == null) {
			throw new InvalidRuleConfigException(
					"Rule config file format is not supported: " + ruleConfigFileP
		}
		String configText = "";
		//從ruleConfigFilePath文件中讀取配置文本到configText中
		RuleConfig ruleConfig = parser.parse(configText);
		return ruleConfig;
	}
	private String getFileExtension(String filePath) {
		//...解析文件名獲取擴展名,比如rule.json,返回json
		return "json";
	}
	
	private IRuleConfigParser createParser(String configFormat) {
		IRuleConfigParser parser = null;
		if ("json".equalsIgnoreCase(configFormat)) {
			parser = new JsonRuleConfigParser();
		} else if ("xml".equalsIgnoreCase(configFormat)) {
			parser = new XmlRuleConfigParser();
		} else if ("yaml".equalsIgnoreCase(configFormat)) {
			parser = new YamlRuleConfigParser();
		} else if ("properties".equalsIgnoreCase(configFormat)) {
			parser = new PropertiesRuleConfigParser();
		}
		return parser;
	}
}

爲了讓類的職責更加單一、代碼更加清晰,我們還可以進一步將 createParser() 函數剝離到一個獨立的類中,讓這個類只負責對象的創建。而這個類就是我們現在要講的簡單工廠模式類。具體的代碼如下所示:

public class RuleConfigSource {
	public RuleConfig load(String ruleConfigFilePath) {
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConf
		if (parser == null) {
			throw new InvalidRuleConfigException(
					"Rule config file format is not supported: " + ruleConfigFileP
		}
		String configText = "";
		//從ruleConfigFilePath文件中讀取配置文本到configText中
		RuleConfig ruleConfig = parser.parse(configText);
		return ruleConfig;
	}
		
	private String getFileExtension(String filePath) {
		//...解析文件名獲取擴展名,比如rule.json,返回json
		return "json";
	}
}
public class RuleConfigParserFactory {
	public static IRuleConfigParser createParser(String configFormat) {
		IRuleConfigParser parser = null;
		if ("json".equalsIgnoreCase(configFormat)) {
			parser = new JsonRuleConfigParser();
		} else if ("xml".equalsIgnoreCase(configFormat)) {
			parser = new XmlRuleConfigParser();
		} else if ("yaml".equalsIgnoreCase(configFormat)) {
			parser = new YamlRuleConfigParser();
		} else if ("properties".equalsIgnoreCase(configFormat)) {
			parser = new PropertiesRuleConfigParser();
		}
		return parser;
	}
}

大部分工廠類都是以“Factory”這個單詞結尾的,但也不是必須的,比如 Java 中的DateFormat、Calender。除此之外,工廠類中創建對象的方法一般都是 create 開頭,比如代碼中的 createParser(),但有的也命名爲 getInstance()、createInstance()、newInstance(),有的甚至命名爲 valueOf()(比如 Java String 類的 valueOf() 函數)等等,這個我們根據具體的場景和習慣來命名就好。

在上面的代碼實現中,我們每次調用 RuleConfigParserFactory 的 createParser() 的時候,都要創建一個新的 parser。實際上,如果 parser 可以複用,爲了節省內存和對象創建的時間,我們可以將 parser 事先創建好緩存起來。當調用 createParser() 函數的時候,我們從緩存中取出 parser 對象直接使用。

這有點類似單例模式和簡單工廠模式的結合,具體的代碼實現如下所示。在接下來的講解中,我們把上一種實現方法叫作簡單工廠模式的第一種實現方法,把下面這種實現方法叫作簡單工廠模式的第二種實現方法。

public class RuleConfigParserFactory {
	private static final Map<String, RuleConfigParser> cachedParsers = new Has
	
	static {
		cachedParsers.put("json", new JsonRuleConfigParser());
		cachedParsers.put("xml", new XmlRuleConfigParser());
		cachedParsers.put("yaml", new YamlRuleConfigParser());
		cachedParsers.put("properties", new PropertiesRuleConfigParser());
	}
	
	public static IRuleConfigParser createParser(String configFormat) {
		if (configFormat == null || configFormat.isEmpty()) {
			return null;//返回null還是IllegalArgumentException全憑你自己說了算
		}
		IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase())
		return parser;
	}
}

對於上面兩種簡單工廠模式的實現方法,如果我們要添加新的 parser,那勢必要改動到RuleConfigParserFactory 的代碼,那這是不是違反開閉原則呢?實際上,如果不是需要頻繁地添加新的 parser,只是偶爾修改一下 RuleConfigParserFactory 代碼,稍微不符合開閉原則,也是完全可以接受的。

除此之外,在 RuleConfigParserFactory 的第一種代碼實現中,有一組 if 分支判斷邏輯,是不是應該用多態或其他設計模式來替代呢?實際上,如果 if 分支並不是很多,代碼中有 if 分支也是完全可以接受的。應用多態或設計模式來替代 if 分支判斷邏輯,也並不是沒有任何缺點的,它雖然提高了代碼的擴展性,更加符合開閉原則,但也增加了類的個數,犧牲了代碼的可讀性。關於這一點,我們在後面章節中會詳細講到。

總結一下,儘管簡單工廠模式的代碼實現中,有多處 if 分支判斷邏輯,違背開閉原則,但權衡擴展性和可讀性,這樣的代碼實現在大多數情況下(比如,不需要頻繁地添加 parser,也沒有太多的 parser)是沒有問題的。

工廠方法(Factory Method)

如果我們非得要將 if 分支邏輯去掉,那該怎麼辦呢?比較經典處理方法就是利用多態。按照多態的實現思路,對上面的代碼進行重構。重構之後的代碼如下所示:

public interface IRuleConfigParserFactory {
	IRuleConfigParser createParser();
}

public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory
	@Override
	public IRuleConfigParser createParser() {
		return new JsonRuleConfigParser();
	}
}

public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory
	@Override
	public IRuleConfigParser createParser() {
		return new XmlRuleConfigParser();
	}
}

public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory
	@Override
	public IRuleConfigParser createParser() {
		return new YamlRuleConfigParser();
	}
}

public class PropertiesRuleConfigParserFactory implements IRuleConfigParserF
	@Override
	public IRuleConfigParser createParser() {
		return new PropertiesRuleConfigParser();
	}
}

實際上,這就是工廠方法模式的典型代碼實現。這樣當我們新增一種 parser 的時候,只需要新增一個實現了 IRuleConfigParserFactory 接口的 Factory 類即可。所以,工廠方法模式比起簡單工廠模式更加符合開閉原則。

從上面的工廠方法的實現來看,一切都很完美,但是實際上存在挺大的問題。問題存在於這些工廠類的使用上。接下來,我們看一下,如何用這些工廠類來實現RuleConfigSource 的 load() 函數。具體的代碼如下所示:

public class RuleConfigSource {
	public RuleConfig load(String ruleConfigFilePath) {
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		
		IRuleConfigParserFactory parserFactory = null;
		if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
			parserFactory = new JsonRuleConfigParserFactory();
		} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
			parserFactory = new XmlRuleConfigParserFactory();
		} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
			parserFactory = new YamlRuleConfigParserFactory();
		} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
			parserFactory = new PropertiesRuleConfigParserFactory();
		} else {
			throw new InvalidRuleConfigException("Rule config file format is not s
		}
		IRuleConfigParser parser = parserFactory.createParser();
		
		String configText = "";
		//從ruleConfigFilePath文件中讀取配置文本到configText中
		RuleConfig ruleConfig = parser.parse(configText);
		return ruleConfig;
	}
	
	private String getFileExtension(String filePath) {
		//...解析文件名獲取擴展名,比如rule.json,返回json
		return "json";
	}
}

從上面的代碼實現來看,工廠類對象的創建邏輯又耦合進了 load() 函數中,跟我們最初的代碼版本非常相似,引入工廠方法非但沒有解決問題,反倒讓設計變得更加複雜了。那怎麼來解決這個問題呢?

我們可以爲工廠類再創建一個簡單工廠,也就是工廠的工廠,用來創建工廠類對象。這段話聽起來有點繞,我把代碼實現出來了,你一看就能明白了。其中,RuleConfigParserFactoryMap 類是創建工廠對象的工廠類,getParserFactory() 返回的是緩存好的單例工廠對象。

public class RuleConfigSource {
	public RuleConfig load(String ruleConfigFilePath) {
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		
		IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getP
		if (parserFactory == null) {
			throw new InvalidRuleConfigException("Rule config file format is not s
		}
		IRuleConfigParser parser = parserFactory.createParser();
		
		String configText = "";
		//從ruleConfigFilePath文件中讀取配置文本到configText中
		RuleConfig ruleConfig = parser.parse(configText);
		return ruleConfig;
	}
	
	private String getFileExtension(String filePath) {
		//...解析文件名獲取擴展名,比如rule.json,返回json
		return "json";
	}
}

//因爲工廠類只包含方法,不包含成員變量,完全可以複用,
//不需要每次都創建新的工廠類對象,所以,簡單工廠模式的第二種實現思路更加合適。
public class RuleConfigParserFactoryMap { //工廠的工廠
	private static final Map<String, IRuleConfigParserFactory> cachedFactories
	
	static {
		cachedFactories.put("json", new JsonRuleConfigParserFactory());
		cachedFactories.put("xml", new XmlRuleConfigParserFactory());
		cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
		cachedFactories.put("properties", new PropertiesRuleConfigParserFactory(
	}
	
	public static IRuleConfigParserFactory getParserFactory(String type) {
		if (type == null || type.isEmpty()) {
			return null;
		}
		IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowe
		return parserFactory;
	}
}

當我們需要添加新的規則配置解析器的時候,我們只需要創建新的 parser 類和 parser factory 類,並且在 RuleConfigParserFactoryMap 類中,將新的 parser factory 對象添加到 cachedFactories 中即可。代碼的改動非常少,基本上符合開閉原則。

實際上,對於規則配置文件解析這個應用場景來說,工廠模式需要額外創建諸多 Factory類,也會增加代碼的複雜性,而且,每個 Factory 類只是做簡單的 new 操作,功能非常單薄(只有一行代碼),也沒必要設計成獨立的類,所以,在這個應用場景下,簡單工廠模式簡單好用,比工方法廠模式更加合適。

那什麼時候該用工廠方法模式,而非簡單工廠模式呢?

我們前面提到,之所以將某個代碼塊剝離出來,獨立爲函數或者類,原因是這個代碼塊的邏輯過於複雜,剝離之後能讓代碼更加清晰,更加可讀、可維護。但是,如果代碼塊本身並不複雜,就幾行代碼而已,我們完全沒必要將它拆分成單獨的函數或者類。

基於這個設計思想,當對象的創建邏輯比較複雜,不只是簡單的 new 一下就可以,而是要組合其他類對象,做各種初始化操作的時候,我們推薦使用工廠方法模式,將複雜的創建邏輯拆分到多個工廠類中,讓每個工廠類都不至於過於複雜。而使用簡單工廠模式,將所有的創建邏輯都放到一個工廠類中,會導致這個工廠類變得很複雜。

除此之外,在某些場景下,如果對象不可複用,那工廠類每次都要返回不同的對象。如果我們使用簡單工廠模式來實現,就只能選擇第一種包含 if 分支邏輯的實現方式。如果我們還想避免煩人的 if-else 分支邏輯,這個時候,我們就推薦使用工廠方法模式。

抽象工廠(Abstract Factory)

講完了簡單工廠、工廠方法,我們再來看抽象工廠模式。抽象工廠模式的應用場景比較\特殊,沒有前兩種常用,所以不是我們本節課學習的重點,你簡單瞭解一下就可以了。

在簡單工廠和工廠方法中,類只有一種分類方式。比如,在規則配置解析那個例子中,解析器類只會根據配置文件格式(Json、Xml、Yaml……)來分類。但是,如果類有兩種分類方式,比如,我們既可以按照配置文件格式來分類,也可以按照解析的對象(Rule 規則配置還是 System 系統配置)來分類,那就會對應下面這 8 個 parser 類。

針對規則配置的解析器:基於接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

針對系統配置的解析器:基於接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

針對這種特殊的場景,如果還是繼續用工廠方法來實現的話,我們要針對每個 parser 都編寫一個工廠類,也就是要編寫 8 個工廠類。如果我們未來還需要增加針對業務配置的解析器(比如 IBizConfigParser),那就要再對應地增加 4 個工廠類。而我們知道,過多的類也會讓系統難維護。這個問題該怎麼解決呢?

抽象工廠就是針對這種非常特殊的場景而誕生的。我們可以讓一個工廠負責創建多個不同類型的對象(IRuleConfigParser、ISystemConfigParser 等),而不是隻創建一種 parser 對象。這樣就可以有效地減少工廠類的個數。具體的代碼實現如下所示:

public interface IConfigParserFactory {
	IRuleConfigParser createRuleParser();
	ISystemConfigParser createSystemParser();
	//此處可以擴展新的parser類型,比如IBizConfigParser
}

public class JsonConfigParserFactory implements IConfigParserFactory {
	@Override
	public IRuleConfigParser createRuleParser() {
		return new JsonRuleConfigParser();
	}
	
	@Override
	public ISystemConfigParser createSystemParser() {
		return new JsonSystemConfigParser();
	}	
}

public class XmlConfigParserFactory implements IConfigParserFactory {
	@Override
	public IRuleConfigParser createRuleParser() {
		return new XmlRuleConfigParser();
	}
	
	@Override
	public ISystemConfigParser createSystemParser() {
		return new XmlSystemConfigParser();
	}
}

// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代碼

重點回顧

好了,今天的內容到此就講完了。我們來一塊總結回顧一下,你需要重點掌握的內容。

在今天講的三種工廠模式中,簡單工廠和工廠方法比較常用,抽象工廠的應用場景比較特殊,所以很少用到,不是我們學習的重點。所以,下面我重點對前兩種工廠模式的應用場景進行總結。

當創建邏輯比較複雜,是一個“大工程”的時候,我們就考慮使用工廠模式,封裝對象的創建過程,將對象的創建和使用相分離。何爲創建邏輯比較複雜呢?我總結了下面兩種情況。

  • 第一種情況:類似規則配置解析的例子,代碼中存在 if-else 分支判斷,動態地根據不同的類型創建不同的對象。針對這種情況,我們就考慮使用工廠模式,將這一大坨 ifelse 創建對象的代碼抽離出來,放到工廠類中。

  • 還有一種情況,儘管我們不需要根據不同的類型創建不同的對象,但是,單個對象本身的創建過程比較複雜,比如前面提到的要組合其他類對象,做各種初始化操作。在這種情況下,我們也可以考慮使用工廠模式,將對象的創建過程封裝到工廠類中。

對於第一種情況,當每個對象的創建邏輯都比較簡單的時候,我推薦使用簡單工廠模式,將多個對象的創建邏輯放到一個工廠類中。當每個對象的創建邏輯都比較複雜的時候,爲了避免設計一個過於龐大的簡單工廠類,我推薦使用工廠方法模式,將創建邏輯拆分得更細,每個對象的創建邏輯獨立到各自的工廠類中。同理,對於第二種情況,因爲單個對象本身的創建邏輯就比較複雜,所以,我建議使用工廠方法模式。

除了剛剛提到的這幾種情況之外,如果創建對象的邏輯並不複雜,那我們就直接通過 new 來創建對象就可以了,不需要使用工廠模式。

現在,我們上升一個思維層面來看工廠模式,它的作用無外乎下面這四個。這也是判斷要不要使用工廠模式的最本質的參考標準。

  • 封裝變化:創建邏輯有可能變化,封裝成工廠類之後,創建邏輯的變更對調用者透明。
  • 代碼複用:創建代碼抽離到獨立的工廠類之後可以複用。
  • 隔離複雜性:封裝複雜的創建邏輯,調用者無需瞭解如何創建對象。
  • 控制複雜度:將創建代碼抽離出來,讓原本的函數或類職責更單一,代碼更簡潔。

課堂討論

  1. 工廠模式是一種非常常用的設計模式,在很多開源項目、工具類中到處可見,比如Java 中的 Calendar、DateFormat 類。除此之外,你還知道哪些用工廠模式實現類?可以留言說一說它們爲什麼要設計成工廠模式類?

  2. 實際上,簡單工廠模式還叫作靜態工廠方法模式(Static Factory Method Pattern)。之所以叫靜態工廠方法模式,是因爲其中創建對象的方法是靜態的。那爲什麼要設置成靜態的呢?設置成靜態的,在使用的時候,是否會影響到代碼的可測試性呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章