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
型態是隱含的,只要求具有 get
、keySet
方法,可取得規範的組態資訊能力,只要物件具有相對應的 get
、keySet
方法,無論它的型態 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 的實現。