009.設計模式與範式:創建型--工廠模式

四.工廠模式(上):爲什麼說沒事不要隨便用工廠模式創建對象?

  1. 一般情況下,工廠模式分爲三種更加細分的類型:簡單工廠、工廠方法和抽象工廠
1. 簡單工廠
  1. 需求: 我們根據配置文件的後綴(json、xml、yaml、properties),選擇不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),將存儲在文件中的配置解析成內存對象 RuleConfig

  2. 傳統實現方法


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: " + ruleConfigFilePath);
    }

    String configText = "";
    //從ruleConfigFilePath文件中讀取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    //...解析文件名獲取擴展名,比如rule.json,返回json
    return "json";
  }
}
  1. 傳統實現方法進階:將功能獨立的代碼塊封裝成函數。按照這個設計思路,我們可以將代碼中涉及 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: " + ruleConfigFilePath);
    }

    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;
  }
}
  1. 簡單工廠實現方法一:(將不同創建邏輯放在一個工廠類,if - else 邏輯放在這個工廠類)爲了讓類的職責更加單一、代碼更加清晰,我們還可以進一步將 createParser() 函數剝離到一個獨立的類中,讓這個類只負責對象的創建。而這個類就是我們現在要講的簡單工廠模式類

public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
    if (parser == null) {
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    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;
  }
}
  1. 簡單工廠實現方法二:(複用對象,將事先創建好的對象緩存起來)我們每次調用 RuleConfigParserFactory 的 createParser() 的時候,都要創建一個新的 parser。實際上,如果 parser 可以複用,爲了節省內存和對象創建的時間,我們可以將 parser 事先創建好緩存起來。當調用 createParser() 函數的時候,我們從緩存中取出 parser 對象直接使用。這有點類似單例模式和簡單工廠模式的結
    實現:

public class RuleConfigParserFactory {
  private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();

  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;
  }
}
  1. 儘管簡單工廠模式的代碼實現中,有多處 if 分支判斷邏輯,違背開閉原則,但權衡擴展性和可讀性,這樣的代碼實現在大多數情況下(比如,不需要頻繁地添加 parser,也沒有太多的 parser)是沒有問題的
2. 工廠方法
  1. 工廠方法實現

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 IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new PropertiesRuleConfigParser();
  }
}
  1. 分析:實際上,這就是工廠方法模式的典型代碼實現。這樣當我們新增一種 parser 的時候,只需要新增一個實現了 IRuleConfigParserFactory 接口的 Factory 類即可。所以,工廠方法模式比起簡單工廠模式更加符合開閉原則。

  2. 工廠類再創建一個簡單工廠,也就是工廠的工廠,用來創建工廠類對象
    如下所示:


public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);

    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
    if (parserFactory == null) {
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }
    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 = new HashMap<>();

  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.toLowerCase());
    return parserFactory;
  }
}

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

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

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

4. 抽象工廠
  1. 讓一個工廠負責創建多個不同類型的對象(IRuleConfigParser、ISystemConfigParser 等),而不是隻創建一種 parser 對象。這樣就可以有效地減少工廠類的個數
  2. 實現如下:

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代碼
5.總結
  1. 簡單工廠和工廠方法比較常用,抽象工廠的應用場景比較特殊,所以很少用到
  2. 使用工廠模式場景:當創建邏輯比較複雜,是一個“大工程”的時候,我們就考慮使用工廠模式,封裝對象的創建過程,將對象的創建和使用相分離
  1. 第一種情況:類似規則配置解析的例子,代碼中存在 if-else 分支判斷,動態地根據不同的類型創建不同的對象。針對這種情況,我們就考慮使用工廠模式,將這一大坨 if-else 創建對象的代碼抽離出來,放到工廠類中
  1. 還有一種情況,儘管我們不需要根據不同的類型創建不同的對象,但是,單個對象本身的創建過程比較複雜,比如前面提到的要組合其他類對象,做各種初始化操作。在這種情況下,我們也可以考慮使用工廠模式,將對象的創建過程封裝到工廠類中。
  1. 對於第一種情況,當每個對象的創建邏輯都比較簡單的時候,我推薦使用簡單工廠模式,將多個對象的創建邏輯放到一個工廠類中。當每個對象的創建邏輯都比較複雜的時候,爲了避免設計一個過於龐大的簡單工廠類,我推薦使用工廠方法模式,將創建邏輯拆分得更細,每個對象的創建邏輯獨立到各自的工廠類中。同理,對於第二種情況,因爲單個對象本身的創建邏輯就比較複雜,所以,我建議使用工廠方法模式。

  2. 工廠模式作用

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

(1). 不用工廠模式,if-else 邏輯、創建邏輯和業務代碼耦合在一起
(2). 簡單工廠是將不同創建邏輯放到一個工廠類中,if-else 邏輯創建不同對象,適用於創建每種對象的邏輯不復雜
(3). 當每個對象的創建邏輯都比較複雜的時候,爲了避免設計一個過於龐大的簡單工廠類時,工廠方法是將不同創建邏輯放到不同工廠類中,先用一個工廠類的工廠來來得到某個工廠,再用這個工廠來創建,if-else 邏輯在工廠類的工廠中 ,適用於創建每種對象的邏輯比較複雜

五.工廠模式(下):如何設計實現一個Dependency Injection框架?

1. DI容器
  1. 依賴注入框架,或者叫依賴注入容器(Dependency Injection Container),簡稱 DI 容器
2. 工廠模式和 DI 容器有何區別?
  1. DI 容器相對於我們上節課講的工廠模式的例子來說,它處理的是更大的對象創建工程。上節課講的工廠模式中,一個工廠類只負責某個類對象或者某一組相關類對象(繼承自同一抽象類或者接口的子類)的創建,而 DI 容器負責的是整個應用中所有類對象的創建。
3. DI 容器的核心功能有哪些?
  1. DI 容器的核心功能一般有三個:配置解析、對象創建和對象生命週期管理。
1. 配置解析
  1. 工廠類要創建哪個類對象是事先確定好的,並且是寫死在工廠類代碼中的。
  2. DI 容器事先並不知道應用會創建哪些對象,讓應用告知 DI 容器要創建哪些對象。
  3. 解析配置

public class RateLimiter {
  private RedisCounter redisCounter;
  public RateLimiter(RedisCounter redisCounter) {
    this.redisCounter = redisCounter;
  }
  public void test() {
    System.out.println("Hello World!");
  }
  //...
}

public class RedisCounter {
  private String ipAddress;
  private int port;
  public RedisCounter(String ipAddress, int port) {
    this.ipAddress = ipAddress;
    this.port = port;
  }
  //...
}

配置文件beans.xml:
<beans>
   <bean id="rateLimiter" class="com.xzg.RateLimiter">
      <constructor-arg ref="redisCounter"/>
   </bean>
 
   <bean id="redisCounter" class="com.xzg.redisCounter">
     <constructor-arg type="String" value="127.0.0.1">
     <constructor-arg type="int" value=1234>
   </bean>
</beans>
2. 對象創建
  1. 如果我們給每個類都對應創建一個工廠類,那項目中類的個數會成倍增加,這會增加代碼的維護成本。要解決這個問題並不難。我們只需要將所有類對象的創建都放到一個工廠類中完成就可以了,比如 BeansFactory
  2. 反射機制:它能在程序運行的過程中,動態地加載類、創建對象,不需要事先在代碼中寫死要創建哪些對象。所以,不管是創建一個對象還是十個對象,BeansFactory 工廠類代碼都是一樣的
3. 生命週期管理
  1. 一種是每次都返回新創建的對象,另一種是每次都返回同一個事先創建好的對象,也就是所謂的單例對象。在 Spring 框架中,我們可以通過配置 scope 屬性,來區分這兩種不同類型的對象。scope=prototype 表示返回新創建的對象,scope=singleton 表示返回單例對象
4. 如何實現一個簡單的 DI 容器?
  1. 核心邏輯只需要包括這樣兩個部分:配置文件解析、根據配置文件通過“反射”語法來創建對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章