Abstract Factory

December 22, 2021

你有個應用程式,會從組態檔中讀取資訊,後續根據組態檔來做相關處理,組態檔一開始是使用 .properties,例如:

cc.openhome.username=justin
cc.openhome.password=123456

為了讀取 .properties,你寫了以下的程式:

package cc.openhome;

import java.io.*;
import java.util.Properties;

class App {
	App(String configFile) throws IOException {
        var props = new Properties();
        props.load(new FileInputStream(configFile));
        
        var username = props.getProperty("cc.openhome.username");
        var password = props.getProperty("cc.openhome.password");
        
        for(Object key : props.keySet()) {
        	...做些處理
        }
        ...其他處理
	}
}

public class Main {
    public static void main(String[] args) throws IOException {
        var app = new App("config.properties");
        ...其他處理
    }
}

由於 .properties 不利於階層式的組態,你考慮增加其他格式的組態檔,例如 JSON,如果直接在 App 增加相關的程式碼,下次要增加 XML 格式組態檔,又要再修改 App,這顯然不是好事!

可替換的次型態

從〈Factory Method〉的經驗中,應該知道目前 App 的問題,在於相依具體的實作,那麼先就以上程式片段看到的部份來重構:

import java.io.*;
import java.util.Properties;
import java.util.Set;

interface Config {
	String get(String key);
	Set<Object> keySet();
}

class PropConfig implements Config {
	Properties props = new Properties();
	PropConfig(String configFile) throws FileNotFoundException, IOException {
		props.load(new FileInputStream(configFile));
	}

	@Override
	public String get(String key) {
		return props.getProperty(key);
	}

	@Override
	public Set<Object> keySet() {
		return props.keySet();
	}
}

class App {
	App(Config config) {
        var username = config.get("cc.openhome.username");
        var password = config.get("cc.openhome.password");
        for(Object key : config.keySet()) {
        	...做些處理
        }
        ...其他處理
	}
}

public class Main {
    public static void main(String[] args) throws IOException {
    	var config = new PropConfig("config.properties");
        var app = new App(config);
        ...其他處理
    }
}

現在 App 相依在抽象的 Config 取得組態資料,如果日後需要增加 JSON 格式組態檔,可以定義 JSONConfig 實作 Config,接著以 JSONConfig 實例作為 App 建構之用:

var config = new JSONConfig("config.json");
var app = new App(config);

App 的角度來看,Config 的實作是可以替換的,這符合里氏替換(Liskov substitution)原則,也就是若 t 是 T 的實例,S 是 T 的次型態(subtype),s 是 S 的實例,q(t) 若是可證的(provable),那 q(s) 也應該要成立(true)。

次型態不見得是父子類別關係,雖然這邊使用 Java 的介面作為示範,然而次型態也不一定是某介面的實現類別,次型態是指具有相同規範的型態,例如,若使用動態定型語言來實作以上範例的話,T 型態是隱含的,只要求具有 getkeySet 方法,可取得規範的組態資訊能力,只要物件具有相對應的 getkeySet 方法,無論它的型態 S 實際上是什麼,都可以稱 S 是 T 的次型態。

開放擴充/關閉修改

顯然地,可以為新需求撰寫新程式碼,然而既有的 App 不用因應新需求的增加而修改,就設計原則而言,符合開放/關閉(open–closed)。

就設計上的模式而言,這是被稱為 Abstract Factory 的實現,Config 是抽象的,keySet 建立的 Set<Object> 是抽象的,若必要的話,一開始 get 的傳回型態也可以設計為抽象的,從 App 的角度來看,就像是相依在一個抽象的工廠 Config,透過它取得各種產品。

由於實際的產品,是由 Config 的實作類別來定義,當你抽換掉 Config 的實作,整個 App 使用的產品實例,自然也就抽換了。

也就是說,如果你需要是一整組彼此相關的產品,那就為它們建立一座工廠來生成這些產品,如果你會有換工廠的需求,那就定義出你需要的工廠與產品規格,接下來只要找尋適當的工廠就可以了。

例如,你的網頁需要更換佈景主題,或者你的圖形介面需要抽換視感(look and feel)元件,那麼抽象工廠或許就是個思考的方向。

在更複雜的情況下,工廠的規格中可能要包含物件的相依性設定、生命週期管理等(這就像你在實體世界中,可能會要求工廠必須提供保固之類的),如果你接觸過 Spring 框架,應該會聯想到 Spring 核心,確實是的,Spring 核心的 BeanFactory,以及它的 XmlBeanFactory 等實作,就是 Abstract Factory 的實現。