引子:能不能不換 DB?#
小菜原本用 SQL Server 寫好了一個電子商務網站,公司接了類似專案的客戶想省錢只能用 Access。小菜以為直接全文取代就好,結果錯誤百出:
insert into在 Access 必須用into,SQL Server 可以省略GetDate()在 Access 不存在,要改成Now()- SQL Server 有
Substring,Access 沒有,要改用Mid - Access 對部分關鍵字(如
password)有保留,作為欄位名要用[ ]包起來 - ADO.NET 的
SqlConnection、SqlCommand…… 都得換成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:抽象產品(如
IUser、IDepartment) - ConcreteProduct:具體產品(如
SqlserverUser、AccessUser)
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 ..> AccessDepartmentinterface 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表)需要修改IFactory、SqlServerFactory、AccessFactory,改動三個類別- 客戶端可能有 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"];從這個角度看,所有用簡單工廠的地方,都可以考慮用反射技術去除
switch或if帶來的分支判斷耦合。
switch與if在程式裡是好東西,但在應對變化上顯得老態龍鍾——反射技術可以解決它們難以擴展與維護的毛病。