設計模式之抽象工廠模式

抽象工廠模式(Abstract Factory),是23種設計模式之一。DP中是這麼定義抽象工廠模式的:

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

在學習抽象工廠模式之前,最好熟悉簡單工廠模式以及工廠方法模式,這樣對理解抽象工廠模式會有一定幫助,而且抽象工廠也是基於工廠方法模式的。

至於工廠是用來幹什麼的,我這裏也不過多介紹了,因爲之前在工廠方法模式裏已經說過了,如果對工廠的概念不是太清楚的話,可以參考我之前的文章:

http://blog.51cto.com/zero01/2067822

在這裏我們暫時先不談論抽象工廠是什麼,因爲如果直接上來就去描述、解釋什麼是抽象工廠,以及如何使用抽象工廠模式來設計代碼,這樣是無法很好的明白抽象工廠模式的概念以及它所帶來的好處或壞處的,只會讓人下意識的只去記住實現代碼,而不是設計模式的思想。講解其他模式也是一樣,如果一上來就是代碼+理論一頓灌,只會讓人看得億臉懵逼或似懂非懂。這就好比給你一塊披薩告訴你很好吃,以及這塊披薩上用了哪些好食材,你只管吃就可以了,那麼如果你沒有吃過難吃的披薩,可能就會以爲披薩就應該是這個味道的。

所以我們先從有些糟糕的代碼入手,並且分析這些代碼哪些地方有問題,然後再演進成使用設計模式去重構代碼,這樣就能有一個明顯的對比,畢竟有對比才有傷害嘛2333。

下面我們來寫一些簡單的代碼,這些代碼用於對MySQL數據庫的表格數據進行訪問:

1.User類,封裝User表的數據,假設只有uid和uname兩個字段:

package org.zero01.test;

public class User {

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getUname() {
        return uname;
    }

    public void setUname(String uname) {
        this.uname = uname;
    }

    private int uid;
    private String uname;

}

2.MysqlUser類,用於對mysql數據庫進行訪問,這裏只是簡單的進行模擬,並沒有實際的訪問數據庫的代碼:

package org.zero01.test;

public class MysqlUser {

    public void insert(User user){
        System.out.println("對 MySQL 裏的 User 表插入了一條數據");
    }

    public User getUser(int uid){
        System.out.println("通過 uid 在 MySQL 裏的 User 表得到了一條數據");
        return null;
    }
}

3.客戶端代碼如下:

package org.zero01.test;

public class Client {

    public static void main(String[] args){

        User user=new User();

        MysqlUser mysqlUser=new MysqlUser();

        mysqlUser.insert(user);
        mysqlUser.getUser(1);
    }
}

從以上的客戶端代碼可以很明顯到看到一個問題,就是MysqlUser mysqlUser=new MysqlUser();這一句代碼使得mysqlUser 這個對象被寫死在了MysqlUser 上。如果需求變更,數據庫方面不用MySQL而改用Oracle了呢,那麼與之有關聯的代碼都得需要進行更改。


使用工廠方法模式進行重構

這是因爲代碼上依賴了具體的實現類,導致與 MysqlUser 耦合,如果熟悉多態或工廠模式的話,可能就已經想到可以用工廠模式來改造它了,通過工廠方法模式可以封裝 new MysqlUser(); 所造成的變化,因爲工廠方法模式可以定義一個用於創建對象的接口,讓子類決定實例化哪一個類。

使用工廠方法重構以上的代碼,代碼結構圖如下:
設計模式之抽象工廠模式

IUser接口,用於客戶端訪問,解除與具體數據庫訪問的耦合:

package org.zero01.product;

import org.zero01.test.User;

public interface IUser {

    public void insert(User user);
    public IUser getUser(int uid);

}

MysqlUser類,用於訪問MySQL數據庫的User表:

package org.zero01.product;

import org.zero01.test.User;

public class MysqlUser implements IUser{

    public void insert(User user) {
        System.out.println("對 MySQL 裏的 User 表插入了一條數據");
    }

    public IUser getUser(int uid) {
        System.out.println("通過 uid 在 MySQL 裏的 User 表得到了一條數據");
        return null;
    }
}

OracleUser類,用於訪問Oracle數據庫的User表:

package org.zero01.product;

import org.zero01.test.User;

public class OracleUser implements IUser{

    public void insert(User user) {
        System.out.println("對 Oracle 裏的 User 表插入了一條數據");
    }

    public IUser getUser(int uid) {
        System.out.println("通過 uid 在 Oracle 裏的 User 表得到了一條數據");
        return null;
    }
}

IFactory接口,定義一個抽象的工廠接口,該工廠用於生產訪問User表的對象:

package org.zero01.factory;

import org.zero01.product.IUser;

public interface IFactory {

    public IUser createUser();

}

MysqlFactory類,實現IFactory接口,用於生產 MysqlUser 的實例對象:

package org.zero01.factory;

import org.zero01.product.IUser;
import org.zero01.product.MysqlUser;

public class MysqlFactory implements IFactory{

    public IUser createUser() {
        return new MysqlUser();
    }
}

OracleFactory 類,實現IFactory接口,用於生產 OracleUser 的實例對象:

package org.zero01.factory;

import org.zero01.product.IUser;
import org.zero01.product.OracleUser;

public class OracleFactory implements IFactory{

    public IUser createUser() {
        return new OracleUser();
    }
}

客戶端代碼如下:

package org.zero01.client;

import org.zero01.factory.IFactory;
import org.zero01.factory.MysqlFactory;
import org.zero01.product.IUser;
import org.zero01.test.User;

public class Client {

    public static void main(String[] args){

        User user=new User();

        IFactory factory=new MysqlFactory();
        IUser userOperation=factory.createUser();

        userOperation.getUser(1);
        userOperation.insert(user);

    }
}

以上我們使用工廠方法模式重構的之前的代碼,現在如果需求改變,要更換數據庫,只需要把 MysqlFactory(); 改爲 OracleFactory(); 就可以了,此時由於多態的特性,使得 IUser 接口的對象 userOperation 根本不知道是在訪問哪個數據庫,卻可以在運行時很好的完成工作,這就是所謂的業務邏輯與數據訪問的解耦。


使用抽象工廠模式重構

但是,問題還沒有解決完,因爲數據庫裏不可能只有一個表吧,很有可能會有其他表,比如與用戶表相關的登錄記錄表(Login表),此時該如何解決?

Login 類,封裝 Login 表的數據,假設只有 id 和 date 兩個字段:

package org.zero01.obj;

import java.util.Date;

public class Login {

    private int id;
    private Date date;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

其實即便是數據庫中會有多個表,那也是屬於數據訪問這一類的,屬於這一系列的,所以我們只需要增加一些相關的類即可。

代碼結構圖如下:
設計模式之抽象工廠模式

ILogin接口,用於客戶端訪問,解除與具體數據庫訪問的耦合:

package org.zero01.product;

import org.zero01.obj.Login;

public interface ILogin {

    public void insert(Login login);
    public Login getLogin(int id);

}

MysqlLogin類,用於訪問MySQL數據庫的Login表:

package org.zero01.product;

import org.zero01.obj.Login;

public class MysqlLogin implements ILogin{

    public void insert(Login login) {
        System.out.println("對 MySQL 裏的 Login 表插入了一條數據");
    }

    public Login getLogin(int id) {
        System.out.println("通過 uid 在 MySQL 裏的 Login 表得到了一條數據");
        return null;
    }
}

OracleLogin 類,用於訪問MySQL數據庫的Login表:

package org.zero01.product;

import org.zero01.obj.Login;

public class OracleLogin implements ILogin{

    public void insert(Login login) {
        System.out.println("對 Oracle 裏的 Login 表插入了一條數據");
    }

    public Login getLogin(int id) {
        System.out.println("通過 uid 在 Oracle 裏的 Login 表得到了一條數據");
        return null;
    }
}

IFactory,定義一個抽象的工廠接口,該工廠用於生產訪問User表以及Login表的對象:

package org.zero01.factory;

import org.zero01.product.ILogin;
import org.zero01.product.IUser;

public interface IFactory {

    public IUser createUser();
    public ILogin createLogin();
}

MysqlFactory類,實現IFactory接口,用於生產 MysqlUser 以及 MysqlLogin 的實例對象:

package org.zero01.factory;

import org.zero01.product.ILogin;
import org.zero01.product.IUser;
import org.zero01.product.MysqlLogin;
import org.zero01.product.MysqlUser;

public class MysqlFactory implements IFactory{

    public IUser createUser() {
        return new MysqlUser();
    }

    public ILogin createLogin() {
        return new MysqlLogin();
    }
}

OracleFactory 類,實現IFactory接口,用於生產 OracleUser 以及 OracleLogin 的實例對象:

package org.zero01.factory;

import org.zero01.product.ILogin;
import org.zero01.product.IUser;
import org.zero01.product.OracleLogin;
import org.zero01.product.OracleUser;

public class OracleFactory implements IFactory{

    public IUser createUser() {
        return new OracleUser();
    }

    public ILogin createLogin() {
        return new OracleLogin();
    }
}

客戶端代碼如下:

package org.zero01.client;

import org.zero01.factory.IFactory;
import org.zero01.factory.MysqlFactory;
import org.zero01.factory.OracleFactory;
import org.zero01.obj.Login;
import org.zero01.product.ILogin;
import org.zero01.product.IUser;
import org.zero01.obj.User;

public class Client {

    public static void main(String[] args){

        User user=new User();
        Login login = new Login();

        // 只需要確定實例化哪一個數據庫訪問對象給factory
        // IFactory factory=new MysqlFactory();
        IFactory factory=new OracleFactory();

        // 已與具體的數據庫訪問解除了耦合
        IUser userOperation=factory.createUser();

        userOperation.getUser(1);
        userOperation.insert(user);

        // 已與具體的數據庫訪問解除了耦合
        ILogin loginOperation=factory.createLogin();

        loginOperation.insert(login);
        loginOperation.getLogin(1);

    }
}

運行結果:

通過 uid 在 Oracle 裏的 User 表得到了一條數據
對 Oracle 裏的 User 表插入了一條數據
對 Oracle 裏的 Login 表插入了一條數據
通過 uid 在 Oracle 裏的 Login 表得到了一條數據

從客戶端的代碼中,我們只需要更改 IFactory factory=new MysqlFactory();IFactory factory=new OracleFactory();,就實現了數據庫訪問的切換。而且實際上我們這次代碼的重構已經使用到了抽象工廠模式,抽象工廠可能表面上看起來貌似與工廠方法模式沒什麼區別,其實不然,所以我之前才說抽象工廠模式是基於工廠方法模式的。

只有一個User表的封裝類和User表的操作類時,我們只用到了工廠方法模式,而且也只需要使用到工廠方法模式。但是顯然現在我們的數據庫已經不止一個User表了,而 MySQL 和 Oracle 又是兩大不同的分類,所以解決這種涉及到多個產品系列的問題,就需要使用到專門解決這種問題的模式:抽象工廠模式。這時候再回過頭去看DP對抽象工廠模式的定義就不難理解了。

所以抽象工廠與工廠方法模式的區別在於:抽象工廠是可以生產多個產品的,例如 MysqlFactory 裏可以生產 MysqlUser 以及 MysqlLogin 兩個產品,而這兩個產品又是屬於一個系列的,因爲它們都是屬於MySQL數據庫的表。而工廠方法模式則只能生產一個產品,例如之前的 MysqlFactory 裏就只可以生產一個 MysqlUser 產品。

示意圖:
設計模式之抽象工廠模式

抽象工廠模式(Abstract Factory)結構圖:
設計模式之抽象工廠模式

AbstractProductA 和 AbstractProductB是兩個抽象的產品,之所以爲抽象,是因爲他們都有可能有兩種或多種不同的實現,就剛纔的例子來說就是 User 和 Login 表的不同數據庫的訪問對象,而ProductA1、ProductA2和ProductB1、ProductB2 就是對兩個抽象產品的具體分類的實現,例如 ProductA1可以對比爲 MysqlUser ,而 ProductB1 則可以對比爲 MysqlLogin。

IFactory 則是一個抽象的工廠接口,它裏面應該包含所有的產品創建的抽象方法。而ConcreteFactory 1 和 ConcreteFactory 2 就是具體的工廠了。就像MysqlFactory和OracleFactory一樣。

我們通常是在運行時再創建一個 ConcreteFactory 類的實例對象,這個具體的工廠再創建具有特定實現的產品對象,也就是說,爲創建不同的產品對象,客戶端應該使用不同的具體工廠。


抽象工廠模式的優缺點

優點:

抽象工廠模式最大的好處是易於交換產品系列,由於具體工廠類,例如 IFactory factory=new OracleFactory(); 在一個應用中只需要在初始化的時候出現一次,這就使得改變一個應用的具體工廠變得非常容易,它只需要改變具體工廠即可使用不同的產品配置。不管是任何人的設計都無法去完全防止需求的更改,或者項目的維護,那麼我們的理想便是讓改動變得最小、最容易,例如我現在要更改以上代碼的數據庫訪問時,只需要更改具體的工廠即可。

抽象工廠模式的另一個好處就是它讓具體的創建實例過程與客戶端分離,客戶端是通過它們的抽象接口操作實例,產品實現類的具體類名也被具體的工廠實現類分離,不會出現在客戶端代碼中。就像我們上面的例子,客戶端只認識IUser和ILogin,至於它是MySQl裏的表還是Oracle裏的表就不知道了。

缺點:

但是任何的設計模式都有自身的缺陷都不是完美的,都有不適用的時候,例如抽象工廠模式雖然可以很方便的幫我們切換兩個不同的數據庫訪問的代碼。但是如果我們的需求來自於增加功能,例如我們還需要加多一個會員數據表 MemberData,那麼我們就得先在以上代碼的基礎上,增加三個類:IMemberData,MysqlMemberData,OracleMemberData,還需要修改IFactory、MysqlFactory以及OracleFactory纔可以完全實現。增加類還好說,畢竟我們是對擴展開放的,但是卻要修改三個類,就有點糟糕了。

而且還有一個問題就是客戶端程序類在實際的開發中,肯定不止一個,很多地方都會需要使用 IUser 或 ILogin ,而這樣的設計,其實在每一個類的開始都需要寫上 IFactory factory=new OracleFactory(); 這樣的代碼,如果我有一百個訪問 User 或 Login 表的類,那不就得改一百個類?很多人都喜歡說編程是門藝術,但也的確如此,對於藝術我們應該去追求美感,所以這樣大批量的代碼更改,顯然是非常醜陋的做法。


用簡單工廠來改進抽象工廠

我們要有不向醜陋代碼低頭的精神,所以我們再來改進一下這些代碼。實際上,在這種情況下與其用那麼多的工廠類,不如直接用一個簡單工廠來實現,我們將IFactory、MySQLFactory以及OracleFactory三個工廠類都拋棄掉,取而代之的是一個簡單工廠類EasyFactory,代碼結構圖如下:
設計模式之抽象工廠模式

EasyFactory類,簡單工廠:

package org.zero01.easyfactory;

import org.zero01.product.*;

public class EasyFactory {

    // 數據庫名稱
    private static String db="MySQL";
    // private static String db="Oracle";

    public static IUser createUser(){

        IUser user=null;
        switch (db){
            case "MySQL":
                user=new MysqlUser();
                break;

            case "Oracle":
                user=new OracleUser();
                break;
        }
        return user;
    }

    public static ILogin createLogin(){

        ILogin login=null;
        switch (db){
            case "MySQL":
                login=new MysqlLogin();
                break;

            case "Oracle":
                login=new OracleLogin();
                break;
        }
        return login;
    }
}

客戶端代碼如下:

package org.zero01.client;

import org.zero01.easyfactory.EasyFactory;
import org.zero01.obj.Login;
import org.zero01.product.ILogin;
import org.zero01.product.IUser;
import org.zero01.obj.User;

public class Client {

    public static void main(String[] args){

        User user=new User();
        Login login = new Login();

        // 直接得到實際的數據庫訪問實例,而不存在任何依賴
        IUser userOperation= EasyFactory.createUser();

        userOperation.getUser(1);
        userOperation.insert(user);

        // 直接得到實際的數據庫訪問實例,而不存在任何依賴
        ILogin loginOperation=EasyFactory.createLogin();

        loginOperation.insert(login);
        loginOperation.getLogin(1);

    }
}

由於事先在簡單工廠類裏設置好了db的值,所以簡單工廠的方法都不需要由客戶端來輸入參數,這樣在客戶端就只需要使用 EasyFactory.createUser();EasyFactory.createLogin(); 方法來獲得具體的數據庫訪問類的實例,客戶端代碼上沒有出現任何一個 MySQL 或 Oracle 的字樣,達到了解耦的目的,客戶端已經不再受改動數據庫訪問的影響了。


用反射機制+簡單工廠模式繼續改進代碼

但是我們都知道簡單工廠也存在一個缺陷,例如我要增加一個 SQL Server 數據庫的訪問類,那麼本來抽象工廠模式只需要增加一個 SQLServerFactory 工廠類就可以了,而簡單工廠則需要在每個方法的switch中增加case條件了。

所以我們要考慮的是可以不可以不在代碼裏寫明條件分支語句,而是根據字符串db的值來去某個地方找需要實例化的那個類,這樣的話,我們就可以和switch語句say goodbye了。

而在Java中有一種技術可以做到這一點,那就是反射機制,有了反射機制我們只需要使用字符串就可以獲取某個類的實例,例如:

// 字符串裏的該類的全名
IUser result = (IUser) Class.forName("org.zero01.product.MysqlUser").newInstance();

這種反射的寫法和 IUser result = new MysqlUser(); 一樣可以拿到 MysqlUser 類的實例。而它們的區別在於,反射可以通過字符串來獲取 MysqlUser 類的實例,使用new關鍵字則不行,編譯後就無法改變了。我們都知道字符串是可以存儲在變量中的,可以通過變量來處理字符串,也就是說可以根據需求來進行動態更換。

以上我們使用簡單工廠模式設計的代碼中,是用一個字符串類型的db變量來存儲數據庫名稱的,所以變量的值到底是 MySQL 還是 Oracle ,完全可以由事先設置的那個db變量來決定,而我們又可以通過反射來去獲取實例,這樣就可以去除switch語句了。

下面我們就來使用反射機制改造一下之前的簡單工廠類:

package org.zero01.easyfactory;

import org.zero01.product.*;

public class EasyFactory {

    private static String packName = "org.zero01.product";

    // 數據庫名稱,可替換成Oracle
    private static String db = "Mysql";
    // private static String db="Oracle";

    public static IUser createUser() throws Exception {

        String className = packName + "." + db + "User";

        return (IUser)Class.forName(className).newInstance();
    }

    public static ILogin createLogin() throws Exception {

        String className = packName + "." + db + "Login";

        return (ILogin)Class.forName(className).newInstance();
    }
}

客戶端代碼如下,除了拋多一個異常,其他代碼都不需要改動:

package org.zero01.client;

import org.zero01.easyfactory.EasyFactory;
import org.zero01.obj.Login;
import org.zero01.product.ILogin;
import org.zero01.product.IUser;
import org.zero01.obj.User;

public class Client {

    public static void main(String[] args) throws Exception {

        User user=new User();
        Login login = new Login();

        IUser userOperation= EasyFactory.createUser();

        userOperation.getUser(1);
        userOperation.insert(user);

        ILogin loginOperation=EasyFactory.createLogin();

        loginOperation.insert(login);
        loginOperation.getLogin(1);

    }
}

運行結果:

通過 uid 在 MySQL 裏的 User 表得到了一條數據
對 MySQL 裏的 User 表插入了一條數據
對 MySQL 裏的 Login 表插入了一條數據
通過 uid 在 MySQL 裏的 Login 表得到了一條數據

現在如果需要增加 SQL Server數據庫的訪問功能,那麼增加相關的類是不可避免的,這點無論如何都無法解決,但是這叫擴展,開-閉原則告訴我們對於擴展要開放,但對於修改就要儘量關閉。就目前而言,如果要切換數據庫需要更改db變量的值即可,也就是說只需要改動一下代碼的註釋就可以了:

// private static String db = "Mysql";
private static String db="Oracle";

那麼如果我還需要增加會員數據表 MemberData 的話,只需要增加三個與 MemberData 相關的類,再修改一下 EasyFactory 類,在裏面增加一個創建實例的方法即可。

如果項目比較大的話,就可以直接使用工廠方法模式了,那樣只需要增加新的類即可,不需要對原有的代碼進行改動,靈活性比簡單工廠更強。所以在實際的項目中,我們應該根據情況來選擇使用哪種設計模式,不然使用哪種模式也好,都有可能會導致設計過度或不足。


用反射機制+配置文件+簡單工廠模式繼續改進代碼

雖然我們已經使用了反射機制改進了代碼,但是總感覺還是有點缺憾,因爲在更換數據庫訪問時,我們還是需要去打開代碼更改db變量的值,然後再重新進行編譯。所以如果能夠不打開代碼修改程序,就能達到更改變量的效果,那纔是完全符合開-閉原則。

這也不是沒辦法解決的,例如典型的配置文件就可以解決這種問題,我們可以在外部文件寫好這些信息,讓程序去讀文件中配置的信息來給變量賦值就可以了,以後修改也只需要修改配置文件,而不需要去打開代碼來修改,修改之後還得重新編譯那麼麻煩了。

在工程的根目錄下創建一個.json的配置文件,內容如下:

{
  "packName": "org.zero01.product",
  "DB": "Mysql"
}

由於用的是json來作爲配置文件的格式,所以我這裏使用瞭解析json的包:
設計模式之抽象工廠模式

EasyFactory 類代碼如下:

package org.zero01.easyfactory;

import org.json.JSONObject;
import org.zero01.product.ILogin;
import org.zero01.product.IUser;

import java.io.*;

public class EasyFactory {

    private static String packName;
    private static String db;

    // 讀取配置文件內容,初始化變量值
    static {

        try {

            FileReader fileReader = new FileReader("app.json");
            BufferedReader bufferedReader = new BufferedReader(fileReader);

            StringBuffer config = new StringBuffer();
            String s = null;

            while ((s = bufferedReader.readLine()) != null) {
                config.append(s);
            }

            bufferedReader.close();

            JSONObject jsonObject = new JSONObject(config.toString());

            packName = jsonObject.getString("packName");
            db = jsonObject.getString("DB");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static IUser createUser() throws Exception {

        String className = packName + "." + db + "User";

        return (IUser) Class.forName(className).newInstance();
    }

    public static ILogin createLogin() throws Exception {

        String className = packName + "." + db + "Login";

        return (ILogin) Class.forName(className).newInstance();
    }
}

客戶端代碼無需改動,運行結果如下:

通過 uid 在 Oracle 裏的 User 表得到了一條數據
對 Oracle 裏的 User 表插入了一條數據
對 Oracle 裏的 Login 表插入了一條數據
通過 uid 在 Oracle 裏的 Login 表得到了一條數據

小結:經過了一系列的改進代碼,這下對於目前的需求來說基本算得上是滿分了,我們最後應用了反射機制+配置文件+簡單工廠模式解決了數據庫訪問時的可維護、可擴展的問題。而且從這個角度上講,幾乎所有在用簡單工廠的地方,都可以考慮利用反射機制來去除 switch case 或 if else 等條件分支語句,進一步解除分支判斷帶來的耦合,所以在後面我纔沒有用工廠方法模式而是用簡單工廠方法來去改進之前的抽象工廠。

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