設計模式 - 工廠模式

概述

我們都知道Java中共有 23 種設計模式,其中工廠模式分爲三種,即:簡單工廠模式(不在 23 種設計模式之列)、工廠方法模式和抽象工廠模式;我們平時說的工廠模式,其實大都指工廠方法模式,這種模式是我們平時編碼中用的頻率最高的一種,在Spring源碼中就有很多工廠模式的應用,比如 BeanFactory

下面依次按照簡單工廠模式、工廠方法模式、抽象工廠模式的順序,依次由淺入深說說這三種模式;文章分別從定義、場景、優缺點也示例進行講解。

簡單工廠模式

定義

簡單工廠模式(Simple Factory Pattern)是指由一個工廠對象決定創建出哪一種產品類的實例,簡單來說就是,
定義一個工廠類,根據傳入的參數不同返回不同的實例,被創建的實例具有共同的父類或接口。

場景

簡單工廠適用於工廠類負責創建的對象較少的場景,且客戶端只需要傳入工廠類的參數,對於如何創建對象的邏輯不需要關心。總結一下就是:

  1. 需要創建的對象較少;
  2. 客戶端不關心對象的創建過程;

優缺點

優點

實現了對責任的分割,提供了專門的工廠類用於創建對象

缺點

工廠類的職責相對過重,不易於擴展過於複雜的產品結構,不符合開閉原則(可解決)

示例

接下來我們構造一個場景來看看簡單工廠模式的應用:現在手機更新換代的比較快,手機廠商每年基本都會在不同時間或者在同一時間發佈生產不同型號和配置的手機。

假設某手機公司最近發佈了型號爲 A、B 的手機,其中生產任務交給代工廠去生產;我們都知道不管什麼類型的手機都屬於手機,所以我們先創建一個手機類Phone,並在其中聲明一個公共的手機型號方法type

/**
 * @author eamon.zhang
 * @date 2019-09-27 上午10:55
 */
public interface Phone {
    void type();
}

然後定義具體的手機類型:

型號 A:

/**
 * @author eamon.zhang
 * @date 2019-09-27 上午11:02
 */
public class PhoneA implements Phone {
    @Override
    public void type() {
        System.out.println("型號爲A的手機!");
    }
}

型號 B:

/**
 * @author eamon.zhang
 * @date 2019-09-27 上午11:03
 */
public class PhoneB implements Phone {
    @Override
    public void type() {
        System.out.println("型號爲B的手機!");
    }
}

創建手機代工廠 PhoneFactory 類:

/**
 * @author eamon.zhang
 * @date 2019-09-27 上午10:54
 */
public class PhoneFactory {
    public Phone product(String type) {
        switch (type) {
            case "A":
                return new PhoneA();
            case "B":
                return new PhoneB();
            default:
                return null;
        }
    }
}

測試:

/**
 * @author eamon.zhang
 * @date 2019-09-27 上午11:09
 */
public class PhoneFactoryTest {

    @Test
    public void product() {
        PhoneFactory phoneFactory = new PhoneFactory();
        phoneFactory.product("A").type();

        phoneFactory.product("B").type();
    }
}

輸出:

型號爲A的手機!
型號爲B的手機!

當然,爲了方便調用,PhoneFactory 中的product()也可以寫成靜態的。

類圖:

拓展

解決不符合開閉原則問題

上面的示例中,客戶端調用是簡單了,但如果我們業務繼續擴展,增加一個型號 C,那麼上面的工廠方法中的product() 方法就得再次修改邏輯。不符合開閉原則;因此我們客戶考慮對其進行進一步優化,利用反射技術修改product()方法:

 public Phone product(String className) {
    try {
        if (!(null == className || "".equals(className))) {
            return (Phone) Class.forName(className).newInstance();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

修改客戶端調用代碼:

public void product() {
    PhoneFactory phoneFactory = new PhoneFactory();
    phoneFactory.product("com.eamon.javadesignpatterns.factory.PhoneA").type();

    phoneFactory.product("com.eamon.javadesignpatterns.factory.PhoneB").type();
}

經過優化之後,今後再增加型號,就不用去修改工廠方法了;但是又有一個問題,方法參數是很長的字符串,可控性有待提升,而且還需要強制轉型,不方便閱讀和維護,所以進一步改造:

public Phone product(Class<? extends Phone> clazz) {
    try {
        if (null != clazz) {
            return clazz.newInstance();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

優化客戶端調用代碼:

@Test
public void product() {
    PhoneFactory phoneFactory = new PhoneFactory();
    phoneFactory.product(PhoneA.class).type();

    phoneFactory.product(PhoneB.class).type();
}

再來看一下類圖:


其他

簡單工廠模式在 JDK 源碼中也無處不足,比如常用的 Calendar類中Calendar.getInstance()方法,跟進源碼到createCalendar(TimeZone zone,Locale aLocale)就可以看出。

還有就是 常用的logback,我們可以看到 LoggerFactory 中有多個重載的方法 getLogger():

 public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

public final Logger getLogger(final Class<?> clazz) {
    return getLogger(clazz.getName());
}

工廠方法模式

定義

工廠方法模式(Fatory Method Pattern)是指定義一個創建對象的接口,但讓實現這個 接口的類來決定實例化哪個類,工廠方法讓類的實例化推遲到子類中進行。

在工廠方法模式中用戶只需要關心所需產品對應的工廠,無須關心創建細節,而且加入新的產品符 合開閉原則。

工廠方法模式主要解決產品擴展的問題,在簡單工廠中,隨着產品鏈的豐富,如果每個手機的創建邏輯有區別的話,工廠的職責會變得越來越多,有點像萬能工廠,並不便於維護。根據單一職責原則我們將職能繼續拆分,專人幹專事。

場景

工廠方法適用於以下場景:

  1. 創建對象需要大量重複的代碼。
  2. 客戶端(應用層)不依賴於產品類實例如何被創建、實現等細節。
  3. 一個類通過其子類來指定創建哪個對象。

優缺點

優點

  1. 具有良好的封裝性,代碼結構清晰,井底了模塊間的耦合。
  2. 拓展性非常優秀。(在增加產品類的情況下,只要修改具體的工廠類或擴展一個工廠類)
  3. 屏蔽了產品類。(產品類的實現如何變化,調用者不需要關心)

缺點:

1、類的個數容易過多,增加複雜度。
2、增加了系統的抽象性和理解難度。

示例

A 型號手機由PhoneA工廠創建,B 型號手機由PhoneB工廠創建,對工廠本身也做一個抽象。來看代碼,先創建 PhoneFactory 接口:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午1:45
 */
public interface PhoneFactory {
   Phone product();
}

分別創建子工廠 PhoneAFactory

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午1:50
 */
public class PhoneAFactory implements PhoneFactory {
    @Override
    public Phone product() {
        return new PhoneA();
    }
}

PhoneBFactory 類:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午1:50
 */
public class PhoneBFactory implements PhoneFactory {
    @Override
    public Phone product() {
        return new PhoneB();
    }
}

看測試代碼:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午1:54
 */
public class PhoneFactoryTest {

    @Test
    public void product() {
        PhoneFactory factory = new PhoneAFactory();
        factory.product().type();

        factory = new PhoneBFactory();
        factory.product().type();

    }
}

測試結果:

型號爲A的手機!
型號爲B的手機!

再看一下類圖:

拓展

再來看看 logback 中工廠方法模式的應用,看看類圖就 OK 了:


抽象工廠模式

定義

抽象工廠模式(Abastract Factory Pattern)是指提供一個創建一系列相關或相互依賴對象的接口,無需指定他們具體的類。

客戶端(應用層)不依賴於產品類實例如何被創建、實現等細節。強調的是一系列相關的產品對象(屬於同一產品族)一起使用創建對象需要大量重複的代碼。需要提供一個產品類的庫,所有的產品以同樣的接口出現,從而使客戶端不依賴於具體實現。

理解

爲了便於大家理解抽象工廠,我們先了解兩個概念產品等級結構和產品族,看下面的圖:

從上圖中看出有正方形,圓形和三角形三種圖形,相同顏色深淺的就代表同一個產品族,相同形狀的代表同一個產品等級結構。同樣可以從生活中來舉例,比如,美的電器生產多種家用電器。那麼上圖中,顏色最深的正方形就代表美的洗衣機、顏色最深的圓形代表美的空調、顏色最深的三角形代表美的熱水器,顏色最深的一排都屬於美的品牌,都是美的電器這個產品族。再看最右側的三角形,顏色最深的我們指定了代表美的熱水器,那麼第二排顏色稍微淺一點的三角形,代表海信的熱水器。同理,同一產品結構下還有格力熱水器,格力空調,格力洗衣機。

再看下面這張圖,最左側的箭頭代表具體的工廠,有美的工廠、海信工廠、格力工廠。每個品牌的工廠都生產洗衣機、熱水器、空調。

通過上面兩張圖的對比理解,相信大家對抽象工廠有了非常形象的理解。

場景

一個對象族(或是一組沒有任何關係的對象)都有相同的約束,則可以使用抽象工廠模式。簡單來說:

  1. 和工廠方法一樣客戶端不需要知道它所創建的對象的類。
  2. 需要一組對象共同完成某種功能時。並且可能存在多組對象完成不同功能的情況。
  3. 系統結構穩定,不會頻繁的增加對象。(因爲一旦增加就需要修改原有代碼,不符合開閉原則)

優缺點

優點

  • 封裝性,每個產品的實現類不是高層模塊要關心的,它要關心的是接口,不關心對象是如何創建的,只要知道工廠類是誰,就能創建出一個需要的對象,省時省力。
  • 產品族內的約束爲非公開狀態。

缺點

  • 規定了所有可能被創建的產品集合,產品族中擴展新的產品困難,需要修改抽象工廠的接口
  • 增加了系統的抽象性和理解難度

示例

比如現在有一個應用,假如是某視頻軟件,需要在三個不同的平臺(Windows、IOS、Android)上運行,該應用針對每套系統都設計了一套上傳控制器(UploadController)、播放控制(DisplayController),下面通過抽象工廠模式來設計該軟件。

視頻軟件裏邊的各個平臺的UploadControllerDisplayController應該是我們最終生產的具體產品。所以新建兩個抽象產品接口。

UploadController 接口:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午2:59
 */
public interface UploadController {
    void upload();
}

DisplayController 接口:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午2:59
 */
public interface DisplayController {
    void display();
}

定義抽象工廠VideoPlayerFactory類,它能夠創建UploadControllerDisplayController

/**
 * 抽象工廠是主入口,在Spring中應用的最廣泛的一種設計模式,易於擴展
 *
 * @author eamon.zhang
 * @date 2019-09-27 下午3:04
 */
public interface VideoPlayerFactory {
    DisplayController createDisplayController();

    UploadController createUploadController();
}

然後在各個平臺創建具體的 UploadControllerDisplayController

創建適用於WindowsUploadControllerDisplayController

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:09
 */
public class WindowsUploadController implements UploadController {
    @Override
    public void upload() {
        System.out.println("Windows 上傳控制器!");
    }
}

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:09
 */
public class WindowsDisplayController implements DisplayController {

    @Override
    public void display() {
        System.out.println("Windows 上的播放器!");
    }
}

創建適用於IOSUploadControllerDisplayController

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:10
 */
public class IosUploaderController implements UploadController {
    @Override
    public void upload() {
        System.out.println("IOS 上傳控制器!");
    }
}

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:09
 */
public class IosDisplayController implements DisplayController {

    @Override
    public void display() {
        System.out.println("IOS 上的播放器!");
    }
}

創建適用於AndroidUploadControllerDisplayController

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:10
 */
public class AndroidUploaderController implements UploadController {
    @Override
    public void upload() {
        System.out.println("Android 上傳控制器!");
    }
}

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:09
 */
public class AndroidDisplayController implements DisplayController {

    @Override
    public void display() {
        System.out.println("Android 上的播放器!");
    }
}

在各平臺具體的工廠類中完成上傳控制器和播放控制器的創建過程:

創建WindowsFactory類:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:15
 */
public class WindowsFactory implements VideoPlayerFactory {
    @Override
    public DisplayController createDisplayController() {
        return new WindowsDisplayController();
    }

    @Override
    public UploadController createUploadController() {
        return new WindowsUploadController();
    }
}

創建IosFactory類:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:17
 */
public class IosFactory implements VideoPlayerFactory {
    @Override
    public DisplayController createDisplayController() {
        return new IosDisplayController();
    }

    @Override
    public UploadController createUploadController() {
        return new IosUploaderController();
    }
}

創建AndroidFactory類:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:18
 */
public class AndroidFactory implements VideoPlayerFactory {
    @Override
    public DisplayController createDisplayController() {
        return new AndroidDisplayController();
    }

    @Override
    public UploadController createUploadController() {
        return new AndroidUploaderController();
    }
}

來看客戶端調用:

/**
 * @author eamon.zhang
 * @date 2019-09-27 下午3:20
 */
public class VideoPlayerFactoryTest {

    @Test
    public void VideoPlayer() {
        VideoPlayerFactory factory = new WindowsFactory();

        // IOS
//        factory = new IosFactory();
//        // Android
//        factory = new AndroidFactory();

        UploadController uploadController = factory.createUploadController();
        DisplayController displayController = factory.createDisplayController();

        uploadController.upload();
        displayController.display();

    }
}

以調用 Windows 爲例,結果:

Windows 上傳控制器!
Windows 上的播放器!

上面就是針對不同平臺只通過創建對應的工廠對象就完成了上傳控制器和播放控制器的創建。抽象工廠非常完美清晰地描述這樣一層複雜的關係。但是,不知道大家有沒有發現,如果我們再繼續擴展功能,將下載器也加入到產品中,那麼我們的代碼從抽象工廠,到具體工廠要全部調整,很顯然不符合開閉原則。因此就有了上面優缺點中所說的缺點。


總結

在實際應用中,我們千萬不能犯強迫症甚至有潔癖。在實際需求中產品等級結構升級是非常正常的一件事情。我們可以根據實際情況,只要不是頻繁升級,可以不遵循開閉原則。代碼每半年升級一次或者每年升級一次又有何不可呢?

源碼:github.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章