• <menu id="gyiem"><menu id="gyiem"></menu></menu>
  • <menu id="gyiem"><code id="gyiem"></code></menu>

    Java設計模式(一) 簡單工廠模式不簡單

    原創文章,轉載請務必將下面這段話置于文章開頭處(保留超鏈接)。
    本文轉發自技術世界原文鏈接 http://www.luozeyang.com/design_pattern/simple_factory

    簡單工廠模式使用案例

    有一種抽象產品——汽車(Car),同時有多種具體的子類產品,如BenzCar,BMWCar,LandRoverCar。類圖如下
    Product class diagram

    作為司機,如果要開其中一種車,比如BenzCar,最直接的做法是直接創建BenzCar的實例,并執行其drive方法,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.jasongj.client;

    import com.jasongj.product.BenzCar;

    public class Driver1 {

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

    }

    此時如果要改為開Land Rover,則需要修改代碼,創建Land Rover的實例并執行其drive方法。這也就意味著任何時候需要換一輛車開的時候,都必須修改客戶端代碼。

    一種稍微好點的方法是,通過讀取配置文件,獲取需要開的車,然后創建相應的實例并由父類Car的引用指向它,利用多態執行不同車的drive方法。如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    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();
    }

    }

    對于Car的使用方而言,只需要通過參數即可指定所需要Car的各類并得到其實例,同時無論使用哪種Car,都不需要修改后續對Car的操作。至此,簡單工廠模式的原型已經形成。如果把上述的邏輯判斷封裝到一個專門的類的靜態方法中,則實現了簡單工廠模式。工廠代碼如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    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;
    }

    }

    調用方代碼如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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)當中,Driver3不再需要關心Car的實例化,實現了對象的創建和使用的隔離。

    當然,簡單工廠模式并不要求一定要讀配置文件來決定實例化哪個類,可以把參數作為工廠靜態方法的參數傳入。

    簡單工廠模式進階

    使用反射實現擴展性

    從Driver2和CarFactory1的實現中可以看到,當有新的車加入時,需要更新Driver2和CarFactory1的代碼也實現對新車的支持。這就違反了開閉原則(Open-Close Principle)。可以利用反射(Reflection)解決該問題。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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即可通過反射將其實例化。實現了對擴展的開放,同時保證了對修改的關閉。熟悉Spring的讀者應該會想到Spring IoC的實現。

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

    上例中使用反射做到了對擴展開放,對修改關閉。但有些時候,使用類的全名不太方便,使用別名會更合適。例如Spring中每個Bean都會有個ID,引用Bean時也會通過ID去引用。像Apache Nifi這樣的數據流工具,在流程上使用了職責鏈模式,而對于單個Processor的創建則使用了工廠,對于用戶自定義的Processor并不需要通過代碼去注冊,而是使用注解(為了更方便理解下面這段代碼,請先閱讀筆者另外一篇文章《Java系列(一)Annotation(注解)》)。

    下面就繼續在上文案例的基礎上使用注解升級簡單工廠模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    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;
    }

    }

    從上面代碼中可以看到,該工廠會掃描所有被Vehicle注解的Car(每種Car都在注解中聲明了自己的type,可作為該種Car的別名)然后建立起Car別名與具體Car的Class原映射。此時工廠的靜態方法即可根據目標別名實例化對應的Car。

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

    簡單工廠模式詳解

    簡單工廠模式定義

    簡單工廠模式(Simple Factory Pattern)又叫靜態工廠方法模式(Static FactoryMethod Pattern)。專門定義一個類(如上文中的CarFactory1、CarFactory2、CarFactory3)來負責創建其它類的實例,由它來決定實例化哪個具體類,從而避免了在客戶端代碼中顯式指定,實現了解耦。該類由于可以創建同一抽象類(或接口)下的不同子類對象,就像一個工廠一樣,因此被稱為工廠類。

    簡單工廠模式類圖

    簡單工廠模式類圖如下所示
    Simple factory pettern class diagram

    簡單工廠模式角色劃分

    • 工廠角色(如上文中的CarFactory1/2/3):這是簡單工廠模式的核心,由它負責創建所有的類的內部邏輯。當然工廠類必須能夠被外界調用,創建所需要的產品對象。一般而言,工廠類提供一個靜態方法,外部程序通過該方法創建所需對象。
    • 抽象產品角色(如上文中的Car):簡單工廠模式所創建的是所有對象的父類。注意,這里的父類可以是接口也可以是抽象類,它負責描述所創建實例共有的公共接口。
    • 具體產品角色(如上文中的BMWCar,BenzCar,LandRoverCar):簡單工廠所創建的具體實例對象,這些具體的產品往往都擁有共同的父類。

    簡單工廠模式優點

    • 工廠類是整個簡單工廠模式的關鍵所在。它包含必要的判斷邏輯,能夠根據外界給定的信息(配置,或者參數),決定究竟應該創建哪個具體類的對象。用戶在使用時可以直接根據工廠類去創建所需的實例,而無需了解這些對象是如何創建以及如何組織的。有利于整個軟件體系結構的優化。
    • 通過引入配置文件和反射,可以在不修改任何客戶端代碼的情況下更換和增加新的具體產品類,在一定程度上提高了系統的靈活性(如CarFactory2)。
    • 客戶端無須知道所創建的具體產品類的類名,只需要知道具體產品類所對應的參數即可,對于一些復雜的類名,通過簡單工廠模式可以減少使用者的記憶量(如CarFactory3)。

    簡單工廠模式缺點

    • 由于工廠類集中了所有實例的創建邏輯,這就直接導致一旦這個工廠出了問題,所有的客戶端都會受到牽連。
    • 由于簡單工廠模式的產品是基于一個共同的抽象類或者接口,這樣一來,產品的種類增加的時候,即有不同的產品接口或者抽象類的時候,工廠類就需要判斷何時創建何種接口的產品,這就和創建何種種類的產品相互混淆在了一起,違背了單一職責原則,導致系統喪失靈活性和可維護性。
    • 正如上文提到的,一般情況下(如CarFactory1),簡單工廠模式違背了“開放-關閉原則”,因為當我們新增加一個產品的時候必須修改工廠類,相應的工廠類就需要重新編譯一遍。但這一點可以利用反射(CarFactory3在本質上也是利用反射)在一定程度上解決(如CarFactory2)。
    • 使用反射可以使簡單工廠在一定條件下滿足“開放-關閉原則”,但這僅限于產品類的構造及初始化相同的場景。對于各產品實例化或者初始化不同的場景,很難利用反射滿足“開放-關閉”原則。
    • 簡單工廠模式由于使用了靜態工廠方法,造成工廠角色無法形成基于繼承的等級結構。這一點筆者持保留態度,因為繼承不是目的,如果沒有這樣的需求,這一點完全不算缺點,例如JDBC的DriverManager。

    簡單工廠模式與OOP原則

    已遵循的原則

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

    未遵循的原則

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

    簡單工廠模式在JDK中的典型應用

    簡單工廠模式在JDK中最典型的應用要數JDBC了。可以把關系型數據庫認為是一種抽象產品,各廠商提供的具體關系型數據庫(MySQL,PostgreSQL,Oracle)則是具體產品。DriverManager是工廠類。應用程序通過JDBC接口使用關系型數據庫時,并不需要關心具體使用的是哪種數據庫,而直接使用DriverManager的靜態方法去得到該數據庫的Connection。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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設計模式系列

    郭俊 Jason wechat
    歡迎關注作者微信公眾號【大數據架構】
    您的贊賞將支持作者繼續原創分享
    速赢彩app 迪庆 | 六盘水 | 馆陶 | 定西 | 三河 | 台州 | 惠东 | 阿勒泰 | 商丘 | 北海 | 大丰 | 醴陵 | 杞县 | 文山 | 柳州 | 松原 | 新余 | 鄂州 | 松原 | 湖州 | 恩施 | 本溪 | 梧州 | 秦皇岛 | 德阳 | 淮北 | 文昌 | 崇左 | 白城 | 大理 | 十堰 | 鄂尔多斯 | 万宁 | 灵宝 | 甘肃兰州 | 寿光 | 宜春 | 德阳 | 沧州 | 泸州 | 石嘴山 | 淮北 | 盘锦 | 通辽 | 桓台 | 甘南 | 张家口 | 固原 | 平潭 | 梅州 | 昌都 | 枣阳 | 日土 | 保定 | 汝州 | 池州 | 慈溪 | 上饶 | 襄阳 | 丹阳 | 黔西南 | 慈溪 | 定西 | 阿里 | 巢湖 | 海安 | 孝感 | 大同 | 巴中 | 清徐 | 清远 | 海南 | 承德 | 四川成都 | 北海 | 台山 | 雄安新区 | 海西 | 唐山 | 淮北 | 临汾 | 泉州 | 安岳 | 和田 | 株洲 | 福建福州 | 盐城 | 台北 | 喀什 | 黔东南 | 龙岩 | 淮南 | 百色 | 湖北武汉 | 威海 | 延安 | 衡阳 | 阜新 | 临汾 | 绵阳 | 海西 | 钦州 | 阳江 | 阜阳 | 漯河 | 潜江 | 万宁 | 简阳 | 阜阳 | 吕梁 | 商丘 | 永州 | 巴彦淖尔市 |