Java設(shè)計(jì)模式(一) 簡單工廠模式不簡單

原創(chuàng)文章吠裆,同步發(fā)自作者人個(gè)博客,轉(zhuǎn)載請(qǐng)務(wù)必將下面這段話置于文章開頭處。
  本文轉(zhuǎn)發(fā)自Jason's Blog原文鏈接 http://www.jasongj.com/design_pattern/simple_factory

簡單工廠模式使用案例

有一種抽象產(chǎn)品——汽車(Car)朋鞍,同時(shí)有多種具體的子類產(chǎn)品,如BenzCar妥箕,BMWCar滥酥,LandRoverCar。類圖如下


Product class diagram

作為司機(jī)矾踱,如果要開其中一種車恨狈,比如BenzCar疏哗,最直接的做法是直接創(chuàng)建BenzCar的實(shí)例呛讲,并執(zhí)行其drive方法,如下

package com.jasongj.client;

import com.jasongj.product.BenzCar;

public class Driver1 {

  public static void main(String[] args) {
    BenzCar car = new BenzCar();
    car.drive();
  }

}

此時(shí)如果要改為開Land Rover返奉,則需要修改代碼贝搁,創(chuàng)建Land Rover的實(shí)例并執(zhí)行其drive方法。這也就意味著任何時(shí)候需要換一輛車開的時(shí)候芽偏,都必須修改客戶端代碼雷逆。

一種稍微好點(diǎn)的方法是,通過讀取配置文件污尉,獲取需要開的車膀哲,然后創(chuàng)建相應(yīng)的實(shí)例并由父類Car的引用指向它,利用多態(tài)執(zhí)行不同車的drive方法被碗。如下

package com.jasongj.client;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jasongj.product.BMWCar;
import com.jasongj.product.BenzCar;
import com.jasongj.product.Car;
import com.jasongj.product.LandRoverCar;

public class Driver2 {
  
  private static final Logger LOG = LoggerFactory.getLogger(Driver2.class);

  public static void main(String[] args) throws ConfigurationException {
    XMLConfiguration config = new XMLConfiguration("car.xml");
    String name = config.getString("driver2.name");
    Car car;

    switch (name) {
    case "Land Rover":
      car = new LandRoverCar();
      break;
    case "BMW":
      car = new BMWCar();
      break;
    case "Benz":
      car = new BenzCar();
      break;
    default:
      car = null;
      break;
    }
    LOG.info("Created car name is {}", name);
    car.drive();
  }

}

對(duì)于Car的使用方而言某宪,只需要通過參數(shù)即可指定所需要Car的各類并得到其實(shí)例,同時(shí)無論使用哪種Car锐朴,都不需要修改后續(xù)對(duì)Car的操作兴喂。至此,簡單工廠模式的原型已經(jīng)形成焚志。如果把上述的邏輯判斷封裝到一個(gè)專門的類的靜態(tài)方法中衣迷,則實(shí)現(xiàn)了簡單工廠模式。工廠代碼如下

package com.jasongj.factory;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jasongj.product.BMWCar;
import com.jasongj.product.BenzCar;
import com.jasongj.product.Car;
import com.jasongj.product.LandRoverCar;

public class CarFactory1 {
  
  private static final Logger LOG = LoggerFactory.getLogger(CarFactory1.class);

  public static Car newCar() {
    Car car = null;
    String name = null;
    try {
      XMLConfiguration config = new XMLConfiguration("car.xml");
      name = config.getString("factory1.name");
    } catch (ConfigurationException ex) {
      LOG.error("parse xml configuration file failed", ex);
    }

    switch (name) {
    case "Land Rover":
      car = new LandRoverCar();
      break;
    case "BMW":
      car = new BMWCar();
      break;
    case "Benz":
      car = new BenzCar();
      break;
    default:
      car = null;
      break;
    }
    LOG.info("Created car name is {}", name);
    return car;
  }

}

調(diào)用方代碼如下

package com.jasongj.client;

import com.jasongj.factory.CarFactory1;
import com.jasongj.product.Car;

public class Driver3 {

  public static void main(String[] args) {
    Car car = CarFactory1.newCar();
    car.drive();
  }

}

與Driver2相比酱酬,所有的判斷邏輯都封裝在工廠(CarFactory1)當(dāng)中壶谒,Driver3不再需要關(guān)心Car的實(shí)例化,實(shí)現(xiàn)了對(duì)象的創(chuàng)建和使用的隔離膳沽。

當(dāng)然汗菜,簡單工廠模式并不要求一定要讀配置文件來決定實(shí)例化哪個(gè)類泼差,可以把參數(shù)作為工廠靜態(tài)方法的參數(shù)傳入。

簡單工廠模式進(jìn)階

使用反射實(shí)現(xiàn)擴(kuò)展性

從Driver2和CarFactory1的實(shí)現(xiàn)中可以看到呵俏,當(dāng)有新的車加入時(shí)堆缘,需要更新Driver2和CarFactory1的代碼也實(shí)現(xiàn)對(duì)新車的支持。這就違反了開閉原則(Open-Close Principle)普碎『鸱剩可以利用反射(Reflection)解決該問題。

package com.jasongj.factory;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jasongj.product.Car;

public class CarFactory2 {
  
  private static final Logger LOG = LoggerFactory.getLogger(CarFactory2.class);

  public static Car newCar() {
    Car car = null;
    String name = null;
    try {
      XMLConfiguration config = new XMLConfiguration("car.xml");
      name = config.getString("factory2.class");
    } catch (ConfigurationException ex) {
      LOG.error("Parsing xml configuration file failed", ex);
    }
    
    try {
      car = (Car)Class.forName(name).newInstance();
      LOG.info("Created car class name is {}", name);
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
      LOG.error("Instantiate car {} failed", name);
    }
    return car;
  }

}

從上面代碼中可以看到麻车,之后如果需要引入新的Car缀皱,只需要在配置文件中指定該Car的完整類名(包括package名),CarFactory2即可通過反射將其實(shí)例化动猬。實(shí)現(xiàn)了對(duì)擴(kuò)展的開放啤斗,同時(shí)保證了對(duì)修改的關(guān)閉。熟悉Spring的讀者應(yīng)該會(huì)想到Spring IoC的實(shí)現(xiàn)赁咙。

注解讓簡單工廠模式不簡單

上例中使用反射做到了對(duì)擴(kuò)展開放钮莲,對(duì)修改關(guān)閉。但有些時(shí)候彼水,使用類的全名不太方便崔拥,使用別名會(huì)更合適。例如Spring中每個(gè)Bean都會(huì)有個(gè)ID凤覆,引用Bean時(shí)也會(huì)通過ID去引用链瓦。像Apache Nifi這樣的數(shù)據(jù)流工具,在流程上使用了職責(zé)鏈模式盯桦,而對(duì)于單個(gè)Processor的創(chuàng)建則使用了工廠慈俯,對(duì)于用戶自定義的Processor并不需要通過代碼去注冊(cè),而是使用注解(為了更方便理解下面這段代碼拥峦,請(qǐng)先閱讀筆者另外一篇文章《Java系列(一)Annotation(注解)》)贴膘。

下面就繼續(xù)在上文案例的基礎(chǔ)上使用注解升級(jí)簡單工廠模式。

package com.jasongj.factory;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jasongj.annotation.Vehicle;
import com.jasongj.product.Car;

public class CarFactory3 {

  private static final Logger LOG = LoggerFactory.getLogger(CarFactory3.class);

  private static Map<String, Class> allCars;

  static {
    Reflections reflections = new Reflections("com.jasongj.product");
    Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(Vehicle.class);
    allCars = new ConcurrentHashMap<String, Class>();
    for (Class<?> classObject : annotatedClasses) {
      Vehicle vehicle = (Vehicle) classObject.getAnnotation(Vehicle.class);
      allCars.put(vehicle.type(), classObject);
    }
    allCars = Collections.unmodifiableMap(allCars);
  }

  public static Car newCar() {
    Car car = null;
    String type = null;
    try {
      XMLConfiguration config = new XMLConfiguration("car.xml");
      type = config.getString("factory3.type");
      LOG.info("car type is {}", type);
    } catch (ConfigurationException ex) {
      LOG.error("Parsing xml configuration file failed", ex);
    }

    if (allCars.containsKey(type)) {
      LOG.info("created car type is {}", type);
      try {
        car = (Car) allCars.get(type).newInstance();
      } catch (InstantiationException | IllegalAccessException ex) {
        LOG.error("Instantiate car failed", ex);
      }
    } else {
      LOG.error("specified car type {} does not exist", type);
    }
    return car;
  }

}

從上面代碼中可以看到事镣,該工廠會(huì)掃描所有被Vehicle注解的Car(每種Car都在注解中聲明了自己的type步鉴,可作為該種Car的別名)然后建立起Car別名與具體Car的Class原映射。此時(shí)工廠的靜態(tài)方法即可根據(jù)目標(biāo)別名實(shí)例化對(duì)應(yīng)的Car璃哟。

本文所有代碼都可從作者GitHub下載.

簡單工廠模式詳解

簡單工廠模式定義

簡單工廠模式(Simple Factory Pattern)又叫靜態(tài)工廠方法模式(Static FactoryMethod Pattern)氛琢。專門定義一個(gè)類(如上文中的CarFactory1、CarFactory2随闪、CarFactory3)來負(fù)責(zé)創(chuàng)建其它類的實(shí)例阳似,由它來決定實(shí)例化哪個(gè)具體類,從而避免了在客戶端代碼中顯式指定铐伴,實(shí)現(xiàn)了解耦撮奏。該類由于可以創(chuàng)建同一抽象類(或接口)下的不同子類對(duì)象俏讹,就像一個(gè)工廠一樣,因此被稱為工廠類畜吊。

簡單工廠模式類圖

簡單工廠模式類圖如下所示


Simple factory pettern class diagram

簡單工廠模式角色劃分

  • 工廠角色(如上文中的CarFactory1/2/3):這是簡單工廠模式的核心泽疆,由它負(fù)責(zé)創(chuàng)建所有的類的內(nèi)部邏輯。當(dāng)然工廠類必須能夠被外界調(diào)用玲献,創(chuàng)建所需要的產(chǎn)品對(duì)象殉疼。一般而言,工廠類提供一個(gè)靜態(tài)方法捌年,外部程序通過該方法創(chuàng)建所需對(duì)象瓢娜。
  • 抽象產(chǎn)品角色(如上文中的Car):簡單工廠模式所創(chuàng)建的所有對(duì)象的父類。注意礼预,這里的父類可以是接口也可以是抽象類眠砾,它負(fù)責(zé)描述所創(chuàng)建實(shí)例共有的公共接口。
  • 具體產(chǎn)品角色(如上文中的BMWCar托酸,BenzCar褒颈,LandRoverCar):簡單工廠所創(chuàng)建的具體實(shí)例對(duì)象,這些具體的產(chǎn)品往往都擁有共同的父類获高。

簡單工廠模式優(yōu)點(diǎn)

  • 工廠類是整個(gè)簡單工廠模式的關(guān)鍵所在哈肖。它包含必要的判斷邏輯,能夠根據(jù)外界給定的信息(配置念秧,或者參數(shù)),決定究竟應(yīng)該創(chuàng)建哪個(gè)具體類的對(duì)象布疼。用戶在使用時(shí)可以直接根據(jù)工廠類去創(chuàng)建所需的實(shí)例摊趾,而無需了解這些對(duì)象是如何創(chuàng)建以及如何組織的。有利于整個(gè)軟件體系結(jié)構(gòu)的優(yōu)化游两。
  • 通過引入配置文件和反射砾层,可以在不修改任何客戶端代碼的情況下更換和增加新的具體產(chǎn)品類,在一定程度上提高了系統(tǒng)的靈活性(如CarFactory2)贱案。
  • 客戶端無須知道所創(chuàng)建的具體產(chǎn)品類的類名肛炮,只需要知道具體產(chǎn)品類所對(duì)應(yīng)的參數(shù)即可,對(duì)于一些復(fù)雜的類名宝踪,通過簡單工廠模式可以減少使用者的記憶量(如CarFactory3)侨糟。

簡單工廠模式缺點(diǎn)

  • 由于工廠類集中了所有實(shí)例的創(chuàng)建邏輯,這就直接導(dǎo)致一旦這個(gè)工廠出了問題瘩燥,所有的客戶端都會(huì)受到牽連秕重。
  • 由于簡單工廠模式的產(chǎn)品是基于一個(gè)共同的抽象類或者接口,這樣一來厉膀,產(chǎn)品的種類增加的時(shí)候溶耘,即有不同的產(chǎn)品接口或者抽象類的時(shí)候二拐,工廠類就需要判斷何時(shí)創(chuàng)建何種接口的產(chǎn)品,這就和創(chuàng)建何種種類的產(chǎn)品相互混淆在了一起凳兵,違背了單一職責(zé)原則百新,導(dǎo)致系統(tǒng)喪失靈活性和可維護(hù)性。
  • 正如上文提到的,一般情況下(如CarFactory1)挡篓,簡單工廠模式違背了“開放-關(guān)閉原則”偎蘸,因?yàn)楫?dāng)我們新增加一個(gè)產(chǎn)品的時(shí)候必須修改工廠類,相應(yīng)的工廠類就需要重新編譯一遍杰妓。但這一點(diǎn)可以利用反射(CarFactory3在本質(zhì)上也是利用反射)在一定程度上解決(如CarFactory2)。
  • 使用反射可以使簡單工廠在一定條件下滿足“開放-關(guān)閉原則”碘勉,但這僅限于產(chǎn)品類的構(gòu)造及初始化相同的場(chǎng)景巷挥。對(duì)于各產(chǎn)品實(shí)例化或者初始化不同的場(chǎng)景,很難利用反射滿足“開放-關(guān)閉”原則验靡。
  • 簡單工廠模式由于使用了靜態(tài)工廠方法倍宾,造成工廠角色無法形成基于繼承的等級(jí)結(jié)構(gòu)。這一點(diǎn)筆者持保留態(tài)度胜嗓,因?yàn)槔^承不是目的高职,如果沒有這樣的需求,這一點(diǎn)完全不算缺點(diǎn)辞州,例如JDBC的DriverManager怔锌。

簡單工廠模式與OOP原則

已遵循的原則

  • 依賴倒置原則
  • 迪米特法則
  • 里氏替換原則
  • 接口隔離原則

未遵循的原則

  • 開閉原則(如上文所述,利用配置文件+反射或者注解可以避免這一點(diǎn))
  • 單一職責(zé)原則(工廠類即要負(fù)責(zé)邏輯判斷又要負(fù)責(zé)實(shí)例創(chuàng)建)

簡單工廠模式在JDK中的典型應(yīng)用

簡單工廠模式在JDK中最典型的應(yīng)用要數(shù)JDBC了变过“T可以把關(guān)系型數(shù)據(jù)庫認(rèn)為是一種抽象產(chǎn)品,各廠商提供的具體關(guān)系型數(shù)據(jù)庫(MySQL媚狰,PostgreSQL岛杀,Oracle)則是具體產(chǎn)品。DriverManager是工廠類崭孤。應(yīng)用程序通過JDBC接口使用關(guān)系型數(shù)據(jù)庫時(shí)类嗤,并不需要關(guān)心具體使用的是哪種數(shù)據(jù)庫,而直接使用DriverManager的靜態(tài)方法去得到該數(shù)據(jù)庫的Connection辨宠。

package com.jasongj.client;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JDBC {
  
  private static final Logger LOG = LoggerFactory.getLogger(JDBC.class);

  public static void main(String[] args) {
    Connection conn = null;
    try {
      Class.forName("org.apache.hive.jdbc.HiveDriver");
      conn = DriverManager.getConnection("jdbc:hive2://127.0.0.1:10000/default");
      PreparedStatement ps = conn.prepareStatement("select count(*) from test.test");
      ps.execute();
    } catch (SQLException ex) {
      LOG.warn("Execute query failed", ex);
    } catch(ClassNotFoundException e) {
      LOG.warn("Load Hive driver failed", e);
    } finally {
      if(conn != null ){
        try {
          conn.close();
        } catch (SQLException e) {
          // NO-OPT
        }
      }
    }
  }
}

Java設(shè)計(jì)模式系列

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末彭羹,一起剝皮案震驚了整個(gè)濱河市黄伊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌派殷,老刑警劉巖还最,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墓阀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡拓轻,警方通過查閱死者的電腦和手機(jī)斯撮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扶叉,“玉大人勿锅,你說我怎么就攤上這事≡嫜酰” “怎么了溢十?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長达吞。 經(jīng)常有香客問我张弛,道長,這世上最難降的妖魔是什么酪劫? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任吞鸭,我火速辦了婚禮,結(jié)果婚禮上覆糟,老公的妹妹穿的比我還像新娘刻剥。我一直安慰自己,他們只是感情好滩字,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布造虏。 她就那樣靜靜地躺著,像睡著了一般踢械。 火紅的嫁衣襯著肌膚如雪酗电。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天内列,我揣著相機(jī)與錄音,去河邊找鬼背率。 笑死话瞧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寝姿。 我是一名探鬼主播交排,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼饵筑!你這毒婦竟也來了埃篓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤根资,失蹤者是張志新(化名)和其女友劉穎架专,沒想到半個(gè)月后同窘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡部脚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年想邦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片委刘。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丧没,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锡移,到底是詐尸還是另有隱情呕童,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布淆珊,位于F島的核電站夺饲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏套蒂。R本人自食惡果不足惜钞支,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望操刀。 院中可真熱鬧烁挟,春花似錦、人聲如沸骨坑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欢唾。三九已至且警,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間礁遣,已是汗流浹背斑芜。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祟霍,地道東北人杏头。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像沸呐,于是被迫代替她去往敵國和親醇王。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 結(jié)合簡單示例和UML圖畸裳,講解工廠模式簡單原理。 一淳地、引子 話說十年前怖糊,有一個(gè)爆發(fā)戶,他家有三輛汽車(Benz(奔馳...
    某人在閱讀 1,346評(píng)論 1 6
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,527評(píng)論 25 707
  • 文 / 路人鋒 晨光颇象,如一貼面巾敷臉?biāo){天伍伤,綠葉間剪成碎片石階,躲藏于冬樹林間歡悅遣钳,因人與百鳥互念 抱著孩子的我扰魂,與...
    路人鋒閱讀 741評(píng)論 26 40
  • {{countResult}} //使用computed計(jì)算屬性 {{countResult()}} //使用m...
    lalabao閱讀 273評(píng)論 0 0
  • 錦纏道 折盡芳菲,淡煙細(xì)雨水流蕴茴。挽素衣劝评,低眉煮酒。海棠胭脂眉似柳倦淀。淺笑嫣然蒋畜,香映春衫袖。 吹一夜暖風(fēng)撞叽,花開清幽姻成。意...
    淡墨卿衫閱讀 219評(píng)論 0 2