定義

抽象工廠模式的實質就是提供介面來建立一系列相關或獨立的物件而不指定這些物件的具體類。

理解

在軟體系統中,經常面臨著“一系列相互依賴的物件”的建立工作;同時由於需求的變化,往往存在著更多系列物件的建立工作。如何應對這種變化?如何繞過常規的物件的建立方法(熟悉的new操作符),提供一種“封裝機制”來避免客戶程式和這種“多系列具體物件建立工作”的緊耦合?這就是我們要說的抽象工廠模式。抽象工廠模式提供了一種方式,可以將一組具有同一主題的單獨的工廠封裝起來。在正常使用中,客戶端程式需要建立抽象工廠的具體實現,然後使用抽象工廠作為介面來建立這一主題的具體物件。客戶端程式不需要知道(或關心)它從這些內部的工廠方法中獲得物件的具體型別,因為客戶端程式僅使用這些物件的通用介面。抽象工廠模式將一組物件的實現細節與他們的一般使用分離開來。

結構

優點

  1. 分離了具體的類;
  2. 易於交換產品的系列;
  3. 有利於保持產品的一致性。

缺點

  1. 在產品族中擴充套件新的產品是很困難的,它需要修改抽象工廠的介面。

適用情形

  1. 一個系統不應該依賴於產品例項如何被建立、組合和表達的細節,這對於所有形態工廠模式的都是重要的;
  2. 提供一個產品類庫,只想提供它的介面而不是它的實現類;
  3. 系統有多餘一個的產品族但是系統只消費其中一個產品;
  4. 設計上要求屬於同一產品族的產品是在一起使用的。

程式碼示例說明

(這裡借用TerryLee大神的例子和程式碼)

中國企業需要一項簡單的財務計算:每月月底,財務人員要計算員工的工資。

員工的工資 = (基本工資 + 獎金 - 個人所得稅)。這是一個放之四海皆準的運演算法則。

為了簡化系統,我們假設員工基本工資總是4000美金。

中國企業獎金和個人所得稅的計算規則是:

         獎金 = 基本工資(4000) * 10%

         個人所得稅 = (基本工資 + 獎金) * 40%

我們現在要為此構建一個軟體系統(代號叫Softo),滿足中國企業的需求。

此時採用最簡單的方式,類圖如下所示:

程式碼如下所示:

using System;

namespace ChineseSalary
{
    /// <summary>
    /// 計算中國個人所得稅
    /// </summary>
    public class ChineseTax
    {
        public double Calculate()
        {
            return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4;
        }
    }
}

using System;

namespace ChineseSalary
{
    /// <summary>
    /// 計算中國個人獎金
    /// </summary>
    public class ChineseBonus
    {
        public double Calculate()
        {
            return Constant.BASE_SALARY * 0.1;
        }
    }
}

namespace ChineseSalary
{
    /// <summary>
    /// 公用的常量
    /// </summary>
    public class Constant
    {
        ;
    }
}

using System;

namespace ChineseSalary
{
    /// <summary>
    /// 客戶端程式呼叫
    /// </summary>
    public class Calculator
    {
        public static void Main(string[] args)
        {
            ChineseBonus bonus = new ChineseBonus();
            double bonusValue  = bonus.Calculate();

            ChineseTax tax = new ChineseTax();
            double taxValue = tax.Calculate();

             + bonusValue - taxValue; 

            Console.WriteLine("Chinaese Salary is:" + salary);
            Console.ReadLine();
        }
    }
}

美國企業工資的計算與中國大致相同,但是在獎金和個人所得稅計算規則不同於中國:

獎金 = 基本工資 * 15 %

個人所得稅 = (基本工資 * 5% + 獎金 * 25%)

我們僅僅將ChineseTax、ChineseBonus修改為AmericanTax、AmericanBonus。 只是修改了部分類的名稱和類方法的內容,結構沒有什麼變化,修改後的類圖如下所示:

程式碼如下所示:

using System;

namespace AmericanSalary
{
    /// <summary>
    /// 計算美國個人獎金
    /// </summary>
    public class AmericanBonus
    {
        public double Calculate()
        {
            return Constant.BASE_SALARY * 0.1;
        }
    }
}

using System;

namespace AmericanSalary
{
    /// <summary>
    /// 計算美國個人所得稅
    /// </summary>
    public class AmericanTax
    {
        public double Calculate()
        {
            return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4;
        }
    }
}

using System;

namespace AmericanSalary
{
    /// <summary>
    /// 公用的常量
    /// </summary>
    public class Constant
    {
        ;
    }
}

using System;

namespace AmericanSalary
{
    /// <summary>
    /// 客戶端程式呼叫
    /// </summary>
    public class Calculator
    {
        public static void Main(string[] args)
        {
            AmericanBonus bonus = new AmericanBonus();
            double bonusValue  = bonus.Calculate();

            AmericanTax tax = new AmericanTax();
            double taxValue = tax.Calculate();

             + bonusValue - taxValue; 

            Console.WriteLine("American Salary is:" + salary);
            Console.ReadLine();
        }
    }
}

開始只考慮將Softo系統運行於中國企業,但隨著MaxDO公司業務向海外拓展, MaxDO需要將該系統移植給美國使用。需要將上面中國和美國兩種情況整合在一個系統。移植時,MaxDO不得不拋棄中國企業的業務規則類ChineseTax和ChineseBonus, 然後為美國企業新建兩個業務規則類: AmericanTax,AmericanBonus。最後修改了業務規則呼叫Calculator類。

結果我們發現:每當Softo系統移植的時候,就拋棄原來的類。現在,如果中國華為要購買該系統,我們不得不再次拋棄AmericanTax,AmericanBonus,修改回原來的業務規則。一個可以立即想到的做法就是在系統中保留所有業務規則模型,即保留中國和美國企業工資運算規則。

前面系統的整合問題在於:當系統在客戶在美國和中國企業間切換時仍然需要修改Caculator程式碼。

一個維護性良好的系統應該遵循“開閉原則”。即:封閉對原來程式碼的修改,開放對原來程式碼的擴充套件(如類的繼承,介面的實現)

我們發現不論是中國企業還是美國企業,他們的業務運規則都採用同樣的計算介面。 於是很自然地想到建立兩個業務介面類Tax,Bonus,然後讓AmericanTax、AmericanBonus和ChineseTax、ChineseBonus分別實現這兩個介面,類圖如下所示:

具體程式碼如下所示:

using System;

namespace InterfaceSalary
{
    /// <summary>
    /// 獎金抽象類
    /// </summary>
    public abstract class Bonus
    {
        public abstract double Calculate();
    }
}

using System;

namespace InterfaceSalary
{
    /// <summary>
    /// 計算中國個人獎金
    /// </summary>
    public class ChineseBonus:Bonus
    {
        public override double Calculate()
        {
            return Constant.BASE_SALARY * 0.1;
        }
    }
}

using System;

namespace InterfaceSalary
{
    /// <summary>
    /// 個人所得稅抽象類
    /// </summary>
    public abstract class Tax
    {
        public abstract double Calculate();
    }
}

using System;

namespace InterfaceSalary
{
    /// <summary>
    /// 計算中國個人所得稅
    /// </summary>
    public class ChineseTax:Tax
    {
        public override double Calculate()
        {
            return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4;
        }
    }
}

using System;

namespace InterfaceSalary
{
    /// <summary>
    /// 公用的常量
    /// </summary>
    public class Constant
    {
        ;
    }
}

using System;

namespace InterfaceSalary
{
    /// <summary>
    /// 客戶端程式呼叫
    /// </summary>
    public class Calculator
    {
        public static void Main(string[] args)
        {
            Bonus bonus = new ChineseBonus();
            double bonusValue  = bonus.Calculate();

            Tax tax = new ChineseTax();
            double taxValue = tax.Calculate();

             + bonusValue - taxValue; 

            Console.WriteLine("Chinaese Salary is:" + salary);
            Console.ReadLine();
        }
    }
}

然而,上面增加的介面幾乎沒有解決任何問題,因為當系統的客戶在美國和中國企業間切換時Caculator程式碼仍然需要修改。只不過修改少了兩處,但是仍然需要修改ChineseBonus,ChineseTax部分。致命的問題是:我們需要將這個移植工作轉包給一個叫Hippo的軟體公司。 由於版權問題,我們並未提供Softo系統的原始碼給Hippo公司,因此Hippo公司根本無法修改Calculator,導致實際上移植工作無法進行。

為此,我們考慮增加一個工具類(命名為Factory),程式碼如下:

using System;

namespace FactorySalary
{
    /// <summary>
    /// Factory類
    /// </summary>
    public class Factory
    {
        public Tax CreateTax()
        {
            return new ChineseTax();
        }

        public Bonus CreateBonus()
        {
            return new ChineseBonus();
        }
    }
}

修改後的客戶端程式碼:

using System;

namespace FactorySalary
{
    /// <summary>
    /// 客戶端程式呼叫
    /// </summary>
    public class Calculator
    {
        public static void Main(string[] args)
        {
            Bonus bonus = new Factory().CreateBonus();
            double bonusValue  = bonus.Calculate();

            Tax tax = new Factory().CreateTax();
            double taxValue = tax.Calculate();

             + bonusValue - taxValue; 

            Console.WriteLine("Chinaese Salary is:" + salary);
            Console.ReadLine();
        }
    }
}

不錯,我們解決了一個大問題,設想一下:當該系統從中國企業移植到美國企業時,我們現在需要做什麼?

答案是: 對於Caculator類我們什麼也不用做。我們需要做的是修改Factory類。

很顯然,前面的解決方案帶來了一個副作用:就是系統不但增加了新的類Factory,而且當系統移植時,移植工作僅僅是轉移到Factory類上,工作量並沒有任何縮減,而且還是要修改系統的原始碼。 從Factory類在系統移植時修改的內容我們可以看出: 實際上它是專屬於美國企業或者中國企業的。名稱上應該叫AmericanFactory,ChineseFactory更合適.

解決方案是增加一個抽象工廠類AbstractFactory,增加一個靜態方法,該方法根據一個配置檔案(App.config或者Web.config) 一個項(比如factoryName)動態地判斷應該例項化哪個工廠類,這樣,我們就把移植工作轉移到了對配置檔案的修改。修改後的類圖如下:

抽象工廠類的程式碼如下(使用反射模式):

using System;
using System.Reflection;

namespace AbstractFactory
{
    /// <summary>
    /// Factory類
    /// </summary>
    public abstract class AbstractFactory
    {
//        public AbstractFactory GetInstance()
//        {
//            string factoryName = Constant.STR_FACTORYNAME.ToString();
//
//            AbstractFactory instance;
//
//            if(factoryName == "ChineseFactory")
//                instance = new ChineseFactory();
//            else if(factoryName == "AmericanFactory")
//                instance = new AmericanFactory();
//            else
//                instance = null;
//
//            return instance;
//        }

        public AbstractFactory GetInstance()
        {
            string factoryName = Constant.STR_FACTORYNAME.ToString();

            AbstractFactory instance;

            if(factoryName != "")
                instance = (AbstractFactory)Assembly.Load(factoryName).CreateInstance(factoryName);
            else
                instance = null;

            return instance;
        }

        public abstract Tax CreateTax();

        public abstract Bonus CreateBonus();
    }
}

配置檔案如下所示: