設計模式——抽象工廠模式

shanzm發表於2020-05-01

目錄

shanzm-2020年5月1日 23:20:41

1. 模式簡介

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

產品族(產品系列)同一個具體工廠建立的不同等級的產品稱為同一產品族,或是稱為同一產品系列。

注意:同一個產品族的產品是繼承於不同的產品抽象類

在抽象工廠模式中有產品族的概念,而在工廠方法模式中是沒有這個概念的,因為工廠方法模式中一個具體工廠只建立一種具體產品。

產品等級又稱為產品系列,指的是繼承與同一個抽象產品類的所有具體產品稱之為同一個產品等級

為了方便理解產品族和產品等級,舉一個小栗子:

04抽象工廠模式-產品族和產品系列

抽象工廠模式主要類:

  • AbstractProductA抽象產品A類(或是介面),派生出所有的具體產品類ConcreteProductA1、ConcreteProductA2 ……

  • AbstractProductB抽象產品B類(或是介面),派生出所有的具體產品類ConcreteProductB1、ConcreteProductB2 ……

  • AbstractFactory 抽象工廠介面,所有的具體工廠類都是實現該介面

  • ConcreteFactory1 具體工廠1,實現了IFactory介面,建立具體的產品物件ConcreteProductA1和ConcreteProductB1

  • ConcreteFactory2 具體工廠2,實現了IFactory介面,建立具體的產品物件ConcreteProductA2和ConcreteProductB2

注意: 兩個抽象產品類可以有關係,比如:共同繼承或實現一個抽象類或介面

抽象工廠模式的UML:

Abstract Factory Pattern UML

注:原圖片來自《設計模式實訓教程-第二版》

仔細檢視UML,可以發現:當系統中只存在一個產品等級時,抽象工廠模式將退化到工廠方法模式。

2. 示例1-使用工廠模式實現對不同資料庫的操作

2.1 背景說明

在實際開發中,有可能會出現更換不同的資料庫,或是一個專案就使用多個型別的資料庫。
所以為便於更換不同的資料庫,我們使用工廠模式,定義不同的具體工廠建立不同資料庫的操作類

示例來源《大話設計模式》,假設某個專案同時具體MSSQL資料庫和Oracle資料庫,兩個資料庫只是型別不同,其中的表以及表的欄位都是一樣的。

我們需要對兩個資料庫中的User表進行操作。

按照工廠模式的設計思路,依次實現以下介面和類:

抽象產品:IUserService-宣告查詢和新增User表資料的方法
具體產品:MSSQLUserServiceOracleUserService-分別針對MSSQ和Oracle資料庫的實現IUserService介面
抽象工廠:IDatabaseFactory-宣告建立IUserService物件的方法
具體工廠:MSSQLFactoryOracleFactory-實現IDatabaseFactory介面,分別建立MSSQLUserService物件和OracleUserService物件

2.2 程式碼實現

①建立User類

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

②建立產品總介面IUserService和具體產品MSSQLUserService、OracleUserService

//抽象產品
public interface IUserService
{
    void Insert(User user);
    User GetUser(int id);
}

//具體產品:模擬對MSSQL資料庫中的User表的查詢和新增操作
public class MSSQLUserService : IUserService
{
    public void Insert(User user)
    {
        Console.WriteLine($"MSSQL資料庫User表中中-新增新的使用者,Id:{user.Id },Name{user.Name}");
    }
    public User GetUser(int id)
    {
        Console.WriteLine($"MSSQL資料庫User表中-查詢到使用者,Id:{id}");
        return null;
    }
}

//具體產品:模擬對Oracle資料庫中的User表的查詢和新增操作
public class OracleUserService : IUserService
{
    public void Insert(User user)
    {
        Console.WriteLine($"Oracle資料庫User表中-新增新的使用者,Id:{user.Id },Name{user.Name}");
    }
    public User GetUser(int id)
    {
        Console.WriteLine($"Oracle資料庫User表中-查詢到使用者,Id:{id}");
        return null;
    }
}

③建立抽象工廠IDatabaseFactory和具體工廠MSSQLFactory、OracelFactory

//抽象工廠
public interface IDatabaseFactory
{
    IUserService CreateUserService();
}

//具體工廠:建立MSSQLUserService物件
public class MSSQLFactory : IDatabaseFactory
{
    public IUserService CreateUserService()
    {
        return new MSSQLUserService();
    }
}

//具體工廠:建立OracleUserService物件
public class OracleFactory : IDatabaseFactory
{
    public IUserService CreateUserService()
    {
        return new OracleUserService();
    }
}

④客戶端呼叫

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };

    IDatabaseFactory msSQlFactory = new MSSQLFactory();
    IDatabaseFactory oracleFactory = new OracleFactory();

    //若是對MSSQL資料庫中的User表操作
    IUserService msUserService = msSQlFactory.CreateUserService();
    msUserService.Insert(user);
    msUserService.GetUser(00001);//print:查詢到使用者,Id:00001

    //若是對Oracle資料庫中User表操作
    IUserService oracleUserService = oracleFactory.CreateUserService();
    oracleUserService.Insert(user);
    oracleUserService.GetUser(00001);//print:查詢到使用者,Id:00001
}

2.3 程式類圖

shanzm_抽象工廠模式_示例1


3. 示例2-多資料庫且多表操作

3.1 背景說明

在示例1中,有兩個不同的資料庫,每個資料庫中都有一張User表,我們實現了對每個資料庫的User表查詢和新增資料

我們使用了工廠方法模式,即有一個抽象產品介面(IUserService),有2個具體產品類(MSSQLUserServiceOracleUserService)實現該介面。

有一個抽象工廠介面(IDatabaseFactory),有兩個具體產品工廠類(MSSQLFactoryOracleFactory)實現該介面。

而現在,若是在兩個資料庫中還有一個部門表Department表,需要對Department表操作。

則需要按照以下修改和新增程式碼:

  • 新增一個抽象產品介面(IDepService),和實現該介面的有兩個具體產品類(MSSQLDepServiceOracleDepService)。

  • 在原有的抽象工廠介面和具體工廠類中新增建立MSSQLDepService物件和OracleDepService物件的方法。注意工廠方法是在原有的工廠中進行擴充套件。

3.2 程式碼實現

①在示例1的基礎上,新增一個Department類

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
}

②新增一個新的抽象產品介面(IDepService),和實現該介面的有兩個具體產品(MSSQLDepServiceOracleDepService

//抽象產品
public interface IDepartmentService
{
    void Insert(Department dep);
    Department GetDepartment(int id);
}

//具體產品:模擬對MSSQL資料庫中的Department表的查詢和新增操作
public class MSSQLDepService : IDepartmentService
{
    public Department GetDepartment(int id)
    {
        Console.WriteLine($"MSSQL資料庫的Department表中-查詢到部門,Id:{id}");
        return null;
    }
    public void Insert(Department dep)
    {
        Console.WriteLine($"MSSQL資料庫的Department表中-新增新的部門,Id:{dep.Id }Name:{dep.Name}");
    }
}

//具體產品:模擬對Oracle資料庫中的Department表的查詢和新增操作
class OracleDepService : IDepartmentService
{
    public Department GetDepartment(int id)
    {
        Console.WriteLine($"Oracle資料庫的Department表中-查詢到部門,Id:{id}");
        return null;
    }
    public void Insert(Department dep)
    {
        Console.WriteLine($"Oracle資料庫的Department表中-新增新的部門,Id:{dep.Id }Name:{dep.Name}");
    }
}

③在示例1的基礎上,在原有的抽象工廠介面和具體工廠類中新增建立MSSQLDepService物件和OracleDepService物件的方法

public interface IDatabaseFactory
{
    IUserService CreateUserService();
    IDepartmentService CreateDepService();//在介面中新增新的方法
}

public class MSSQLFactory : IDatabaseFactory
{
    public IDepartmentService CreateDepService()
    {
        return new MSSQLDepService();
    }
    public IUserService CreateUserService()
    {
        return new MSSQLUserService();
    }
}

public class OracleFactory : IDatabaseFactory
{
    public IDepartmentService CreateDepService()
    {
        return new OracleDepService();
    }
    public IUserService CreateUserService()
    {
        return new OracleUserService();
    }
}

④在客戶端呼叫

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };
    Department dep = new Department() { Id = 1000, Name = "Development" };
   
    IDatabaseFactory DatabaseFactory = new MSSQLFactory();

    //對MSSQL資料庫中的User表操作
    IUserService UserService = DatabaseFactory.CreateUserService();
    UserService.Insert(user);
    UserService.GetUser(00001);

    //對MSSQL資料庫中的Del表操作
    IDepartmentService DepService = DatabaseFactory.CreateDepService();
    DepService.Insert(dep);
    DepService.GetDepartment(1000);
    Console.ReadKey();
}

執行結果:

MSSQL資料庫

假如需要改換為Oracle資料庫,則你只需要將建立具體工廠物件new MSSQLFactory()修改為new OracleFactory(),其他的程式碼無需修改
static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };
    Department dep = new Department() { Id = 1000, Name = "Development" };
   
    IDatabaseFactory DatabaseFactory = new MSSQLFactory();

    //對MSSQL資料庫中的User表操作
    IUserService UserService = DatabaseFactory.CreateUserService();
    UserService.Insert(user);
    UserService.GetUser(00001);

    //對MSSQL資料庫中的Del表操作
    IDepartmentService DepService = DatabaseFactory.CreateDepService();
    DepService.Insert(dep);
    DepService.GetDepartment(1000);
    Console.ReadKey();
}

執行結果:

MSSQL資料庫

3.3 程式類圖

shanzm_抽象工廠

【說明】:

  • MSSQLUserServiceMSSQLDepService是由同一個具體工廠MSSQLFactory建立的,即二者屬於同一產品族。

  • OracleUserServiceOracleDepService是由同一個具體工廠OracleFactory建立的,即二者屬於同一產品族。

  • 而我們需要切換資料庫的時候(即切換產品族),只需要修改建立具體工廠物件:MSSQLFactory物件或OracleFactory物件。這就是抽象工廠模式的最大優點!


4. 重構示例2-使用簡單工廠改進抽象工廠

上述示例專案中,假如再新增一個新的表Student,新增對該表的操作類,則先需要定義一個抽象介面IStudentService介面,派生針對不同資料庫操作的兩個類:MSSQLStudentServiceOracleStudentService,這之後再在IDatabaseFactory介面中新增一個CreateStudentService()方法,接著在兩個具體的工廠類中實現該介面。

我們可以使用簡單工廠模式實現上述的示例2中的專案:

完整演示Demo程式碼下載

①以下介面和類和示例2中一樣
抽象產品A:IUserService ,派生出具體產品:MSSQLUserServiceOracleUserService
抽象產品B:IDepService,派生出具體產品:MSSQLDepServiceOracleDepService

②定義簡單工廠類:
因為這裡有兩個抽象產品,所以和之前的一般的簡單工廠不同的地方就是要建立兩個工廠方法:

public class DatabaseFactory
{
    private static readonly string db = "MSSQL";//若是需要更換資料庫則將字串改"Oracle"

    //針對抽象產品IUserService的工廠方法
    public static IUserService CreateUserService()
    {
        IUserService userService = null;
        switch (db)
        {
            case "MSSQL":
                userService = new MSSQLUserService();
                break;
            case "Oracle":
                userService = new OracleUserService();
                break;
        }
        return userService;
    }

    //針對抽象產品IDepService的工廠方法
    public static IDepartmentService CreateDeprService()
    {
        IDepartmentService depService = null;
        switch (db)
        {
            case "MSSQL":
                depService = new MSSQLDepService();
                break;
            case "Oracle":
                depService = new OracleDepService();
                break;
        }
        return depService;
    }
}

如果需要更換資料庫,則只需要簡單的將 private static readonly string db該欄位賦值改為"Oracle"

③客戶端呼叫

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };
    Department dep = new Department() { Id = 1000, Name = "Development" };

    IUserService userService = DatabaseFactory.CreateUserService();
    userService.Insert(user);

    IDepartmentService depService = DatabaseFactory.CreateDeprService();
    depService.Insert(dep);

    Console.ReadKey();
}

執行結果:

04抽象工廠模式-簡單工廠

【說明】

  • 在這裡使用簡單該廠模式對比使用抽象工廠模式,簡化了許多的類和介面,所有的修改都可以在工廠類中進行修改新增

  • 同樣也實現了客戶端和建立例項過程的分離

④程式類圖

使用簡單工廠改進抽象工廠UML

對比抽象工廠模式,只是將所有的抽象工廠和具體工廠全部簡化為一個工廠類,該工廠類中有兩個工廠方法

5. 重構示例2-反射+簡單工廠

通過使用反射我們可以免去在工廠方法中使用switch語句,
通過反射獲取需要建立例項的物件名,然後建立該類的例項物件(本質上就是依賴注入
看上去好像並沒有變得更加方便,但其實是如有產品族比較多的情況下,switch語句的case語句也相應的變多
所以使用反射,可以省略使用switch還是不錯的。

程式碼實現,在 4. 重構示例2-使用簡單工廠改進抽象工廠的基礎上,修改工廠類:

完整演示Demo程式碼下載

public class DatabaseFactory
{
    //具體產品所在的程式集名稱
    private static readonly string AssemblyName = "04抽象工廠模式-多資料庫連線-反射+簡單工廠";
    private static readonly string db = "MSSQL";//若是需要更換資料庫則將字串改為"Oracle"

    public static IUserService CreateUserService()
    {
        //具體產品的完全限定名
        //注意因為我們的這個專案中有特殊字元,所以程式集的名字和專案名不一致,檢視程式集名和名稱空間名可以右鍵專案屬性
        string className = "_04抽象工廠模式_多資料庫連線_反射_簡單工廠" + "." + db + "UserService";
        return (IUserService)Assembly.Load(AssemblyName).CreateInstance(className);
      
    }
    public static IDepartmentService CreateDeprService()
    {
        string className = "_04抽象工廠模式_多資料庫連線_反射_簡單工廠" + "." + db + "DepService";
        return (IDepartmentService)Assembly.Load(AssemblyName).CreateInstance(className);
    }
}

6. 重構示例2-反射+配置檔案+簡單工廠

5. 重構示例2-反射+簡單工廠若是需更換資料,還是需要修改private static readonly string db = "MSSQL"欄位
即任然需要修改程式碼後在重新編譯,我們可以將需要修改的欄位值放在配置檔案中

完整演示Demo程式碼下載

修改5. 重構示例2-反射+簡單工廠如下:

①首先本專案新增引用"System.Configuration"

②在配置檔案App.Config中新增如下配置

<configuration>
  <appSettings>
    <add key="db" value="MSSQL"/><!--更換資料則<add key="db" value="Oracle"/>-->
  </appSettings>
</configuration>

③修改工廠類中的db欄位

private static readonly string db = ConfigurationManager.AppSettings["db"];//db欄位的值從配置檔案中讀取

【說明】:其實在所有在用到簡單工廠的地方,都可以考慮使用反射技術去除去switch或if,解除分支判斷帶來的耦合

7. 總結分析

整個示例專案,由工廠方法模式-->抽象工廠模式-->簡單工廠模式,你可以仔細的檢視三個實現方式的程式類圖,值得琢磨!

7.1 優點

從UML類圖中就可以發現:

  1. 便於交換產品系列,每一個具體的工廠物件都只是在客戶端中初始化時候實現一次,所以改變一個具體的工廠物件是十分簡單的,所以更換一個產品序列也就變得簡單了。

    簡單的說,就是因為具體產品都是由具體的工廠建立的,所以在更換產品族的時候只需要簡單的修改具體工廠物件即可

  2. 建立具體產品物件的過程和客戶端分離(可以從UML中明顯看出),客戶端通過操作抽象產品介面實現操作具體產品例項,具體產品的類名不會出現在客戶端中。

7.2 缺點

  1. 新增新的產品族是非常簡單的,首先在相應的產品等級結構中新增新的具體產品,然後新增一個具體工廠即可。

  2. 新增新的產品等級是非常麻煩的,首先要新增抽象產品介面,接著派生所有的具體產品,還要在抽象工廠中新增方法,以及所有的具體工廠中實現該方法。

對比以上就明白:
抽象工廠模式的擴充套件有一定的“開閉原則”傾斜性
當增加一個新的產品族時只需增加一個新的具體工廠,不需要修改原始碼,滿足開閉原則。
當增加一個新的產品等級時,則所有的工廠類都需要進行修改,不滿足開閉原則。

7.3 適應場合

系統中有多個產品族,但每次只使用其中的某一族產品。切換產品族只需要修改一下具體工廠物件即可。

比如本文示例中,針對不同資料庫操作我們可以實現不同的產品族,切換資料庫只需要簡單的修改具體工廠物件。


8. 參考及原始碼

相關文章