1. 程式人生 > >設計模式(十三)抽象工廠模式

設計模式(十三)抽象工廠模式

資料訪問的程式碼?(以“新增使用者”和“得到使用者”為例。)

最基本的資料訪問程式

使用者類, 假設只有ID和Name兩個欄位,其餘省略:

class User{
    private int _id;
    public int ID{
        get{return _id;}
        set{_id=value;}
    }
    
    private sting _name;
    public string Name{
        get{return _name;)
        set{_name=value;}
    }
}

SqlserverUser類——用於操作User表,假設只有“新增使用者”和“得到使用者”方法,其餘方法以及具體的SQL語句省略:

class SqlserverUser{
    public void Insert(User user){
        Console.WriteLine("在 SQL Server 中給User表中增加一條記錄");
    }

    public User GetUser(int id){
        Console.WriteLine("在 SQL Server 中根據ID得到User表一條記錄");
        return null;
    }
}

客戶端程式碼:

static void Main(string[] args){
    User user = new User();

    SqlserverUser su = new SqlserverUser();    // 與SQL Server耦合

    su.Insert(user);    // 插入使用者

    su.GetUser(1);    // 得到ID為1的使用者

    Console.Read();
}

這裡的問題就在於SqlserverUser su = new SqlserverUser()使得su這個物件被“框死”在SQL Server上了。想想我們之前學的,工廠方法模式是定義一個用於建立物件的介面,讓子類決定例項化哪一個類。

用了工廠方法模式的資料訪問程式

程式碼結構圖:

IUser介面:用於客戶端訪問,解除與具體資料庫訪問的耦合:

interface IUser{
    void Insert(User user);
    
    User getUser(int id);
}

SqlserverUser類,用於訪問SQL Server的User:

class SqlserverUser : IUser{
    public void Insert(User user){
        Console.WriteLine("在SQL Server中給User表增加一條記錄");
    }
    
    public User GetUser(int id){
        Console.WriteLine("在SQL Server中根據ID得到User表一條記錄");
        return null;
    }
}

AccessUser類,用於訪問Access的User:

class AccessUser : IUser{
    public void Insert(User user){
        Console.WriteLine("在Access中給User表增加一條記錄");
    }
    
    public User GetUser(int id){
        Console.WriteLine("在Access中根據ID得到User表一條記錄");
        return null;
    }
}

IFactory介面,定義一個建立訪問User表物件的抽象的工廠介面:

interface IFactory{
    IUser CreateUser();
}

SqlServerFactory類,實現IFactory介面,例項化SqlserverUser:

class SqlServerFactory:IFactory{
    public IUser CreateUser(){
        return new SqlserverUser();
    }
}

AccessFactory類,實現IFactory介面,例項化AccessUser:

class AccessFactory:IFactory{
    public IUser CreateUser(){
        return new AccessUser();
    }
}

客戶端程式碼:

static void Main(string[] args){
    User user = new User();
    
    // 若要更改成Access資料庫,只需要將本句改成IFactorry factory = new AccessFactory();
    IFactory factory = new SqlServerFactory();

    IUser iu = factory.CreateUser();

    iu.Insert(user);
    iu.GetUser(1);

    Console.Read();
}

現在如果要更改成Access資料庫,只需要將 new SqlServerFactory()改成new AccessFactory();,此時由於多型的關係,使得宣告IUser介面的物件iu事先根本不知道是在訪問哪個資料庫,卻可以在執行時很好地完成工作,這就是所謂的業務邏輯與資料訪問的解耦

資料庫裡不會只有一個User表,現在增加部門表(Department表),該如何辦呢?

class Department{
    private int id;
    public int ID{
        get{return _id;}
        set{_id=value;}
    }
    private string _deptName;
    public string DeptName{
        get{return _deptName;}
        set{_deptName=value;}
    }
}

用了抽象工廠模式的資料訪問程式

程式碼結構圖:

IDepartment介面,用於客戶端訪問,解除與具體資料庫訪問的耦合:

interface IDepartment{
    void Insert(Department department);
    Department GetDepartment(int id);
}

SqlserverDepartment類,用於訪問SQL Server的Department:

class SqlserverDepartment : IDepartment{
    public void Insert(Department department){
        Console.WriteLine("在SQL Server中給Department表增加一條記錄");
    }

    public Department GetDepartment(int id){
        Console.WriteLine("在SQL Server中根據ID得到Department表一條記錄");
        return null;
    }
}

AccessDepartment類,用於訪問Access的Department:

class AccessDepartment : IDepartment{
    public void Insert(Department department){
        Console.WriteLine("在Access中給Department表增加一條記錄");
    }

    public Department GetDepartment(int id){
        Console.WriteLine("在Access中根據ID得到Department表一條記錄");
        return null;
    }
}

IFactory介面,定義一個建立訪問Department表物件的抽象的工廠介面:

interface IFactory{
    IUser CreateUser();
    //增加的介面方法
    IDepartment CreateDepartment();
}

 SqlServerFactory類,實現IFactory介面,例項化SqlserverUser和SqlserverDepartment:

class SqlserverFactory : IFactory{
    public IUser CreateUser(){
        return new SqlserverUser();
    }

    // 增加了SqlserverDepartment工廠
    public IDepartment CreateDepartment(){
        return new SqlserverDepartment();
    }
}

 AccessFactory類,實現IFactory介面,例項化 AccessUser和 AccessDepartment:

class  AccessFactory : IFactory{
    public IUser CreateUser(){
        return new AccessUser();
    }

    // 增加了AccessDepartment工廠
    public IDepartment CreateDepartment(){
        return new  AccessDepartment();
    }
}

客戶端程式碼:

static void Main(string[] args){
    User user = new User();
    Department dept = new Department();

    // 只需確定例項化哪一個資料庫訪問物件給factory
    // IFactory factory = new SqlserverFactory();
    IFactory factory = new AccessFactory();

    // 則此時已與具體的資料庫訪問解除了依賴
    IUser iu=factory.CreateUser();

    iu.Insert(user);
    iu.GetUser(1);

    // 則此時已與具體的資料庫訪問解除了依賴
    IDepartment id =  factory.CreateDepartment();
    id.Insert(dept);
    id.GetDepartment(1);

    Console.Read();
}

 結果顯示如下:

這樣,只需更改IFactory factory = new AccessFactory()為IFactory factory = new SqlserverFactory(),就實現了資料庫訪問的切換。

抽象工廠模式

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

 

AbstractProductA和AbstractProductB是兩個抽象產品,之所以為抽象,是因為它們都有可能有兩種不同的實現,就剛才的例子來說就是User和Department,而ProductA1、ProductA2和ProductB1、ProductB2就是對兩個抽象產品的具體分類的實現,比如ProductA1可以理解為是SqlserverUser,而ProductB1是SqlserverDepartment。

IFActory是一個抽象工廠介面,它裡面應該包含所有的產品建立的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具體的工廠了。

通常是在執行時刻再建立一個ConcreteFactory類的例項,這個具體的工廠再建立具有特定實現的產品物件,也就是說,為建立不同的產品物件,客戶端應使用不同的具體工廠。

優點:

  1. 易於交換產品系列。由於具體工程類在一個應用中只需要在初始化的時候出現一次,這就使得改變一個應用的具體工廠變得非常容易,它只需要改變具體工廠即可使用不同的產品配置。
  2. 讓具體的建立例項過程與客戶端分離,客戶端是通過它們的抽象介面操縱例項,產品的具體類名也被具體工廠的實現分離,不會出現在客戶程式碼中。

缺點:

  1. 如果要增加功能,比如我們現在要增加專案表Project,那就至少要增加三個類IProject、SqlserverProject、AccessProject,還需要更改IFactory、SqlserverFactory和AccessFactory才可以完全實現。

  2. 客戶端程式類顯然不會是隻有一個,有很多地方都在使用IUser或IDepartment,而這樣的設計其實在每一個類的開始都需要宣告IFactory factory = new SqlserverFactory()。如果我有100個呼叫資料庫訪問的類,豈不是要更改100次 IFactory factory = new AccessFactory()這樣的程式碼。

須知,程式設計是門藝術,這樣大批量的改動,顯然是非常醜陋的做法。 

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

去除IFactory、SqlserverFactory和AccessFactory三個工廠類,取而代之的是DataAccess類,用一個簡單工廠模式來實現:

 

class DataAccess{
    // 資料庫名稱,可替換成Access
    private static readonly string db ="Sqlserver";
    // private static readonly string db ="Access";

    public static IUser CreateUser(){
        IUser result = null;
        switch (db){
            case "Sqlserver":
                result = new SqlserverUser();
                break;
            case "Access":
                result = new AccessUser();
                break;
        }
        return result;
    }

     public static IDepartment CreateDepartment(){
        IDepartment result = null;
        switch (db){
            case "Sqlserver":
                result = new SqlserverDepartment();
                break;
            case "Access":
                result = new AccessDepartment();
                break;
        }
        return result;
    }
}

客戶端程式碼:

static void Main(string[] args){
    User user = new User();
    Department dept = new Department();;

    // 直接得到實際的資料庫訪問例項,而不存在任何依賴
    IUser iu = DataAccess.CreateUser();

    iu.Insert(user);
    iu.GetUser(1);

    // 直接得到實際的資料庫訪問例項,而不存在任何依賴
    IDepartment id = DataAccess.CreateDepartment();

    id.Insert(dept);
    id.GetDepartment(1);

    Console.Read();
}

可以看到客戶端沒有出現任何一個SQL Server 或Access的字樣,達到了解耦的目的。

可是現在,如果我需要增加Oracle資料庫訪問,本來抽象工廠只增加一個OracleFactory工廠類就可以了,現在就比較麻煩了,需要在DataAccess類中每個方法的switch中加case。

用反射+抽象工廠的資料訪問程式

能不能免去switch判斷的麻煩呢? 

常規的寫法:

IUser result = new SqlserverUser();

反射的寫法:

// 先引用System.Reflection的名稱空間
using System.Reflection;

IUser result = (IUser)Assembly.Load("抽象工廠模式").CreateInstance("抽象工廠模式.SqlserverUser");

 看出差別了嗎?原來的例項化是“寫死”在程式裡的,而現在用了反射就可以利用字串來例項化物件,而變數是可以更換的。這樣,變數的值到底是SQL Server,還是Access,完全可以由事先的那個db變數來決定。所以就去除了switch判斷的麻煩。

程式碼結構圖: 

 

DataAccess類,用反射技術,取代IFactory、SqlserverFactoryr和AccessFactory:

// 引入反射,必須要寫
using System.Reflection;

class DataAccess{
    // 程式集名稱
    private static readonly string AssemblyName ="抽象工廠模式";
    // 資料庫名稱,可替換成Access
    private static readonly string db ="Sqlserver";

    public static IUser CreateUser(){
        string className = AssemblyName + "." +db + "User";
        return (IUser)Assembly.Load(AssemblyName).CreateInstance(className);
    }

     public static IDepartment CreateDepartment(){
        string className = AssemblyName + "." +db + "Department";
        return (IDepartment)Assembly.Load(AssemblyName).CreateInstance(className);
    }
}

 

這樣的結果就是DataAccess.CreateUser()本來得到的是SqlserverUser的例項,而現在變成了OracleUser的例項了。

不過,還是有點遺憾。因為在更換資料庫訪問時,我們還是需要去改程式(改db這個字串的值)重編譯,如果可以不改程式,那才是真正地符合開放-封閉原則。

用反射+配置檔案實現資料訪問程式

可以利用配置檔案來解決更改DataAccess的問題。(可以讀檔案來給DB字串賦值,在配置檔案中寫明是Sqlserver還是Access,這樣就連DataAccess類也不用更改了。

新增一個App.config檔案,內容如下: 

到此為止,我們成功應用了反射+抽象工廠模式解決了資料庫訪問時的可維護、可擴充套件的問題。 

從這個角度上說,所有在用簡單工廠的地方,都可以考慮用反射技術來去除switch或if,解除分支判斷帶來的耦合

 本章完。

本文是連載文章,此為第十三章,學習提供一個建立一系列相關或相互依賴物件的介面,而無需指定它們具體的類的抽象工廠模式,並用反射+抽象工廠模式解決了資料庫訪問時的可維護、可擴充套件問題。

下一章: