引子:能不能不換 DB?#

小菜原本用 SQL Server 寫好了一個電子商務網站,公司接了類似專案的客戶想省錢只能用 Access。小菜以為直接全文取代就好,結果錯誤百出

  • insert into 在 Access 必須用 into,SQL Server 可以省略
  • GetDate() 在 Access 不存在,要改成 Now()
  • SQL Server 有 Substring,Access 沒有,要改用 Mid
  • Access 對部分關鍵字(如 password)有保留,作為欄位名要用 [ ] 包起來
  • ADO.NET 的 SqlConnectionSqlCommand…… 都得換成 OleDb 系列

兩個不同版本意味著兩倍工作量

以後若要改 Oracle,差異更大、修改更多。

第一版:寫死資料庫存取#

class SqlserverUser
{
    public void Insert(User user) { /* SQL Server */ }
    public User GetUser(int id)   { /* SQL Server */ return null; }
}

// 客戶端
SqlserverUser su = new SqlserverUser();
su.Insert(user);
su.GetUser(1);

SqlserverUser su = new SqlserverUser()su 釘死在 SQL Server 上。

第二版:工廠方法模式#

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

class SqlserverUser : IUser { ... }
class AccessUser    : IUser { ... }

interface IFactory
{
    IUser CreateUser();
}

class SqlServerFactory : IFactory { public IUser CreateUser() => new SqlserverUser(); }
class AccessFactory    : IFactory { public IUser CreateUser() => new AccessUser(); }

客戶端:

IFactory factory = new SqlServerFactory();
IUser iu = factory.CreateUser();
iu.Insert(user);
iu.GetUser(1);

換資料庫只需把 new SqlServerFactory() 換成 new AccessFactory(),業務邏輯與資料存取解耦。

但問題還沒完——資料庫不會只有 User 一張表。

抽象工廠模式#

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

結構#

  • AbstractFactory:抽象工廠介面,包含所有產品創建的抽象方法
  • ConcreteFactory:具體工廠,創建具有特定實現的產品物件
  • AbstractProduct:抽象產品(如 IUserIDepartment
  • ConcreteProduct:具體產品(如 SqlserverUserAccessUser
classDiagram
    class IFactory {
        <<interface>>
        +CreateUser() IUser
        +CreateDepartment() IDepartment
    }
    class SqlServerFactory
    class AccessFactory
    class IUser {
        <<interface>>
    }
    class IDepartment {
        <<interface>>
    }
    class SqlserverUser
    class AccessUser
    class SqlserverDepartment
    class AccessDepartment
    IFactory <|.. SqlServerFactory
    IFactory <|.. AccessFactory
    IUser <|.. SqlserverUser
    IUser <|.. AccessUser
    IDepartment <|.. SqlserverDepartment
    IDepartment <|.. AccessDepartment
    SqlServerFactory ..> SqlserverUser
    SqlServerFactory ..> SqlserverDepartment
    AccessFactory ..> AccessUser
    AccessFactory ..> AccessDepartment
interface IDepartment
{
    void Insert(Department department);
    Department GetDepartment(int id);
}

class SqlserverDepartment : IDepartment { ... }
class AccessDepartment    : IDepartment { ... }

interface IFactory
{
    IUser CreateUser();
    IDepartment CreateDepartment();
}

class SqlServerFactory : IFactory
{
    public IUser CreateUser() => new SqlserverUser();
    public IDepartment CreateDepartment() => new SqlserverDepartment();
}

class AccessFactory : IFactory
{
    public IUser CreateUser() => new AccessUser();
    public IDepartment CreateDepartment() => new AccessDepartment();
}

抽象工廠的優缺點#

優點:

  • 易於交換產品系列:具體工廠類在應用中只需在初始化時出現一次,改變整個應用只需更換具體工廠
  • 客戶端透過抽象介面操縱實例,產品的具體類名不會出現在客戶端程式碼中

缺點:

  • 新增產品(如 Project 表)需要修改 IFactorySqlServerFactoryAccessFactory改動三個類別
  • 客戶端可能有 100 個地方寫 IFactory factory = new SqlServerFactory(),換資料庫要改 100 處

用簡單工廠改進#

把三個工廠類改為一個 DataAccess 簡單工廠類:

class DataAccess
{
    private static readonly string db = "Sqlserver";
    // private static readonly string db = "Access";

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

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

客戶端只用 DataAccess.CreateUser()DataAccess.CreateDepartment(),沒有任何 SQL Server 或 Access 字樣。

但若要新增 Oracle,每個方法的 switch 都要加 case。

反射 + 抽象工廠#

反射(Reflection) 可以把實例化從編譯時轉為運行時,徹底擺脫 switch

// 常規寫法(編譯時固定)
IUser result = new SqlserverUser();

// 反射寫法(運行時動態)
using System.Reflection;
IUser result = (IUser)Assembly.Load("抽象工廠模式")
    .CreateInstance("抽象工廠模式.SqlserverUser");

字串可換成變數,於是:

class DataAccess
{
    private static readonly string AssemblyName = "抽象工廠模式";
    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);
    }
}

新增 Oracle 時,只要把 db 字串改成 "Oracle" 並新增對應類別即可——不需要改 DataAccess 內部任何邏輯

反射 + 配置文件#

更進一步,把 db 寫進 App.config,連程式碼都不用重新編譯:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="DB" value="Sqlserver"/>
  </appSettings>
</configuration>
using System.Configuration;

private static readonly string db = ConfigurationManager.AppSettings["DB"];

從這個角度看,所有用簡單工廠的地方,都可以考慮用反射技術去除 switchif 帶來的分支判斷耦合

switchif 在程式裡是好東西,但在應對變化上顯得老態龍鍾——反射技術可以解決它們難以擴展與維護的毛病。