天天看點

17.建立作戰機關——工廠方法、抽象工廠制作一個大型的遊戲需要許多人的參與,你當然是程式開發方面的專家;另外,還有CG專家、武器專家、空氣動力學專家、機械專家、船隻專家、飛行專家、建築學專家、風水學專家、……

制作一個大型的遊戲需要許多人的參與,你當然是程式開發方面的專家;另外,還有CG專家、武器專家、空氣動力學專家、機械專家、船隻專家、飛行專家、建築學專家、風水學專家、……

“等等! 風水學?!”

哦,不好意思,專家太多了,有點亂了,應該是氣象學專家才對。以前我們都說“來的都是客。”,現在我們說:“來的當然都是專家了!”,哈哈!

不過,我需要聲明,鄭重聲明:我并不認為自己是專家,我們隻是非常願意作為朋友和在大家交流軟體開發中的方方面面而已!

好了,說了這麼多,我隻有一個意思,軟體開發項目中有很多人參加,大家各負其責,各盡其能;現在,你負責的關于遊戲機關的接口與基本類型部分代碼的已經完成了,本想休幾天假,可軟體開發的天敵——“需求變化”又不期而至了。

事情是這樣的,那些使用你的代碼開始建構遊戲機關的家夥們實在是太頑皮了,他們創造了太多的異形機關,什麼東西都是飛來飛去的,因為這樣就沒有陸地和海洋的活動限制了,就連要塞都能沖上天和對方的戰鬥機開戰。試玩者有的興奮、有的想哭;老闆臉都綠了,項目經理更是一臉的無奈。

項目經理找到你說:“老闆的指令,你必須限制遊戲機關的建立!”

權力越大,責任也就越大!看來還真不能給那幫不負責任的家夥随便建立遊戲機關的小自由。

現在你必須開始代碼的重構。

第一步,首先将CUnit類的定義修改為MustInherit,這樣就不能随便建立CUnit的執行個體了。然後,有必要将Behavior屬性和Weapon屬性設定為隻讀,這樣即使在CUnit的子類中也沒有辦法修改這些屬性值了。另外,既然我們不能執行個體化CUnit類了,它的構造函數也就沒用了,删了吧。現在CUnit類的定義就變成如下代碼:

''遊戲機關基類

Public MustInherit Class CUnit

    Implements IUnit

    Protected myCurX, myCurY, mySpeed, myUnitId As Integer

    Protected myName As String '遊戲機關名字

    Protected myBehavior As IBehavior

    Protected myWeapon As IWeapon

    '屬性,行為

    Public ReadOnly Property Behavior As IBehavior Implements IUnit.Behavior

        Get

            Return myBehavior

        End Get

    End Property

    '屬性,目前X坐标

    Public Property CurX As Integer Implements IUnit.CurX

        Get

            Return myCurX

        End Get

        Set(ByVal value As Integer)

            myCurX = value

        End Set

    End Property

    '屬性,目前Y坐标

    Public Property CurY As Integer Implements IUnit.CurY

        Get

            Return myCurY

        End Get

        Set(ByVal value As Integer)

            myCurY = value

        End Set

    End Property

    '屬性,速度

    Public Property Speed As Integer Implements IUnit.Speed

        Get

            Return mySpeed

        End Get

        Set(ByVal value As Integer)

            mySpeed = value

        End Set

    End Property

    '屬性,武器

    Public ReadOnly Property Weapon As IWeapon Implements IUnit.Weapon

        Get

            Return myWeapon

        End Get

    End Property

    '屬性,機關ID

    Public Property UnitId As Integer Implements IUnit.UnitId

        Get

            Return myUnitId

        End Get

        Set(ByVal value As Integer)

            myUnitId = value

        End Set

    End Property

    '屬性,機關名稱

    Public Property Name As String Implements IUnit.Name

        Get

            Return myName

        End Get

        Set(ByVal value As String)

            myName = value

        End Set

    End Property

    '方法,移動

    Public Sub Move(ByVal x As Integer, ByVal y As Integer) Implements IUnit.Move

        Me.myBehavior.Move(x, y)

    End Sub

    '方法,攻擊

Public Sub Attack(ByVal x As Integer, ByVal y As Integer)  _

Implements IUnit.Attack

        Me.myWeapon.Attack(x, y)

    End Sub

End Class

(項目:FactoryMethodPatternDemo    檔案:CUnit.vb)

請注意,在本章的例子中,我們修改了IUnit接口的定義,将Move()方法、Attack()方法、UnitId屬性和Name屬性都添加到了IUnit接口中。

現在,我們再來讨論如何制定建立遊戲機關的規則。

最簡單的方法,我們可以建立一系列的CreateXXX()方法,用于建立不同的遊戲機關;或者我們使用一個方法CreateUnit(unitType),根據指定的參數來建立具體的遊戲機關。但是,這兩個方法有一個很大問題,那就是我們已經将CUnit類設定為MustInherit,現在已經沒有可執行個體化的遊戲機關類了;另外一個問題是,如果真的有辦法通過方法來建立所有遊戲機關的話,我們将天上飛的、地上跑的、水裡遊的和趴在窩裡的這些機關都放在一起,随着時間的推移,不斷地有新的機關加入,不需要的機關還要去掉,最終,連你自己都沒辦法維護這些代碼了。

“不過,‘天上飛的、地上跑的、水裡遊的和趴在窩裡的’,這句話有點意思,我們是不是可以按這四種類型建立遊戲機關呢?”

好的,我們就是要這麼幹。

使用工廠方法模式

我們現在在動手修改代碼之前,先整理一下遊戲機關的分類:

l         天上飛的:包括飛機類,即戰鬥機、轟炸機和運輸機。

l         地上跑的:包括人類(士兵、醫生、工程兵)和車輛類(吉普和坦克)。

l         水裡遊的:艦船類,包括驅逐艦和運輸船。

l         趴在窩裡的:建築物,包括工廠、船塢和要塞。

分了友善在程式中辨別這些遊戲機關,我們最好建立EUnit枚舉類型來辨別這些遊戲機關,代碼如下:

''遊戲機關辨別枚舉

Public Enum EUnit

    Soldier = 1001 '士兵

    Doctor = 1002 '醫生

    Engineer = 1003 '工程兵

    Jeep = 2001 '吉普

    Tank = 2002 '坦克

    ''空中機關枚舉

    Fighter = 3001 '戰鬥機

    Bomber = 3002 '轟炸機

    TransportPlane = 3003 '運輸機

    ''海上機關枚舉

    Distroyer = 4001 '驅逐艦

    TransportShip = 4002 '運輸船

    ''建築物機關枚舉

    Factory = 5001 '工廠

    Boatyard = 5002 '船塢

    Fort = 5003 '要塞

End Enum

(項目:FactoryMethodPatternDemo    檔案:Enums.vb)

下一步應該怎麼做?有個哥們說:“可以使用工廠方法模式(Factory Method Pattern)”。這個模式是這麼回事?找了找資料,它的定義是這樣的:

定義一個建立對象的接口,讓子類決定執行個體化哪個類,即建立哪個類型的對象;目的是将對象的建立推遲到子類中完成。

使用工廠方法模式時,我們需要兩組類,即建立者(Creator)和建立的内容(Content);在我們的例子中,建立的内容就是遊戲單元,我們已經有了它的基類,現在可以建立各種遊戲機關類了,下面就是士兵、驅逐艦和戰鬥機的類:

''士兵類

Public Class CSoldier

    Inherits CUnit

    Public Sub New()

        myBehavior = New CLandBehavior

        myWeapon = New CMachineGunWeapon

        mySpeed = 15

        UnitId = EUnit.Soldier

    End Sub

End Class

''驅逐艦

Public Class CDistroyer

    Inherits CUnit

    Public Sub New()

        myBehavior = New CSeaBehavior

        myWeapon = New CCannonWeapon

        mySpeed = 40

        UnitId = EUnit.Distroyer

    End Sub

End Class

''戰鬥機

Public Class CFighter

    Inherits CUnit

    Public Sub New()

        myBehavior = New CAirBehavior

        myWeapon = New CMachineGunWeapon

        mySpeed = 100

        UnitId = EUnit.Fighter

    End Sub

End Class

(項目:FactoryMethodPatternDemo    檔案:Units.vb)

現在,我們需要不同類型機關的建立者了。

我們就是使用這些建立者來限制可以建立哪些遊戲機關,而不是随意建立異形機關。先來看一下建立者的基類:

Public MustInherit ClassCUnitCreator

    Public MustOverride FunctionCreateUnit(ByVal unitType As EUnit) As IUnit

End Class

(項目:FactoryMethodPatternDemo    檔案:Creator.vb)

我們首先建立陸地機關建立者類,代碼如下:

''陸地機關建立者

Public Class CLandUnitCreator

    Inherits CUnitCreator

    Public Overrides Function CreateUnit(ByVal unitType As EUnit) As IUnit

        Select Case unitType

            Case EUnit.Jeep

                Return New CJeep

            Case EUnit.Tank

                Return New CTank

            Case EUnit.Doctor

                Return New CDoctor

            Case EUnit.Engineer

                Return New CEngineer

            Case Else

                Return New CSoldier

        End Select

    End Function

End Class

(項目:FactoryMethodPatternDemo    檔案:Creator.vb)

在CLandUnitCreator類中,我們重寫了CreateUnit()方法,在此類的本方法中,我們隻能建立陸地機關(那幫不負責任的家夥再也不能亂來了),下面我們對這些代碼進行測試。

Module Module1

    Sub Main()

        Dim landUnitCreator As New CLandUnitCreator

        Dim unit As IUnit

        unit = landUnitCreator.CreateUnit(EUnit.Soldier)

        Console.WriteLine("士兵A")

        unit.Move(100, 100)

        unit.Attack(110, 130)

        unit = landUnitCreator.CreateUnit(EUnit.Tank)

        Console.WriteLine("T-34坦克")

        unit.Move(150, 150)

        unit.Attack(200, 210)

        Console.ReadLine()

    End Sub

End Module

(項目:FactoryMethodPatternDemo    檔案:Module1.vb)

本代碼運作結果如下圖:

通過這樣的形式組織代碼,我們可以達到限制建立遊戲機關的目的。在使用的過程中,如果建立陸地機關就使用CLandUnitCreator類,建立空中機關使用CAirUnitCreator類,建立海上機關使用CSeaUnitCreator類,建立建築物則使用CBuildUnitCreator類。

當然,建立遊戲機關時,程式員們還是可以使用New關鍵字建立一系列的遊戲機關對象。但如果這樣做,他們必須要先知道有哪些機關類型。我們使用工廠方法模式時,在開發環境中,鍵入代碼的時候會有機關類型枚舉的提示,這樣就不會出現程式員不知道建立什麼機關的問題了。

還有一個小秘密,如果不告訴其他程式員你建立了這些遊戲機關的類,他們根本就覺察不出這些類的存在。幹了這麼多活,卻不讓别人知道,真不知道是好事還是壞事?

好了,現在看看本節我們建立的代碼結構吧。

請了解,CreateUnit()方法就是工廠方法模式中的“工廠方法”。在一系列的建立者類中,這個方法用于建立不同系列的機關,這是限制,同時也是規範。現在,老闆要求所有的程式員:必須使用CreateUnit()方法建立遊戲機關,否則就要扣獎金。

關于抽象工廠模式

既然我們用到了工廠方法模式,就順便介紹一下另一個與工廠有關的模式——抽象工廠模式;這個模式不單獨收費,就當是買一送一,跳樓大甩賣的哪種。

抽象工廠模式(Abstract Factory Pattern)的定義是這樣的:定義一個接口,用于建立一系列相關或互相依賴的對象,而不需要指定它們的具體類。

在本節的例子中,我們将使用不同品牌的CPU和記憶體(Memory)來組裝電腦,首先是各種CPU和記憶體類型的定義,代碼如下:

''電腦配件

Public Interface ICpu

    Property Brand As String

End Interface

Public Interface IMemory

    Property Brand As String

End Interface

Public Class Cpu1

    Implements ICpu

    Dim myName As String

    Public Sub New()

        myName = "CPU-1"

    End Sub

    Public Property Brand As String Implements ICpu.Brand

        Get

            Return myName

        End Get

        Set(ByVal value As String)

            myName = value

        End Set

    End Property

End Class

Public Class Cpu2

    Implements ICpu

    Dim myName As String

    Public Sub New()

        myName = "CPU-2"

    End Sub

    Public Property Brand As String Implements ICpu.Brand

        Get

            Return myName

        End Get

        Set(ByVal value As String)

            myName = value

        End Set

    End Property

End Class

Public Class Memory1

    Implements IMemory

    Dim myName As String

    Public Sub New()

        myName = "Memory-1"

    End Sub

    Public Property Brand As String Implements IMemory.Brand

        Get

            Return myName

        End Get

        Set(ByVal value As String)

            myName = value

        End Set

    End Property

End Class

Public Class Memory2

    Implements IMemory

    Dim myName As String

    Public Sub New()

        myName = "Memory-2"

    End Sub

    Public Property Brand As String Implements IMemory.Brand

        Get

            Return myName

        End Get

        Set(ByVal value As String)

            myName = value

        End Set

    End Property

End Class

(項目:AbstractFactoryDemo    檔案:AbstractFactory.vb)

代碼中,我們定義了ICpu和IMemory兩種接口作為兩種電腦配件類型,然後,我們定義了具體的兩種CPU類型(Cpu1和Cpu2)和兩種記憶體類型(Memory1和Memory2)。

接下來,我們知道不同的電腦公司生産電腦時,都會使用CPU和記憶體等标準配件,隻是使用這些配件類型的組合不一樣罷了。

根據這些情況,我們首先建立一個接口,接口中必須指定組合電腦時使用的是哪一種CPU和哪一種記憶體。這就是抽象工廠接口IAbstractFactory,其定義如下:

''電腦組裝工廠接口(抽象工廠)

Public Interface IAbstractFactory

    Function CreateCPU() As ICpu

    Function CreateMemory() As IMemory

End Interface

(項目:AbstractFactoryDemo    檔案:AbstractFactory.vb)

在IAbstractFactory接口中,我們定義了兩個方法CreateCPU()和CreateMemory(),分别用于确定CPU類型和記憶體類型。然後,根據某些權威機構的測試報告,我們确定了各種CPU與記憶體之間搭配方案,這些方案有效的保證了不同類型CPU與記憶體之間的相容性問題。根據這些方案,确定了三種相容的搭配方式,分别是AbstractFactoryA(Cpu1與Memory1)、AbstractFactoryB(Cpu2與Memory2)和AbstractFactoryC(Cpu1與Memory2),其定義代碼如下:

''搭配方式A

Public Class AbstractFactoryA

    Implements IAbstractFactory

    Public Function CreateCPU() As ICpu Implements IAbstractFactory.CreateCPU

        Return New Cpu1

    End Function

Public Function CreateMemory() As IMemory  _

Implements IAbstractFactory.CreateMemory

        Return New Memory1

    End Function

End Class

''搭配方式B

Public Class AbstractFactoryB

    Implements IAbstractFactory

    Public Function CreateCPU()As ICpu Implements IAbstractFactory.CreateCPU

        Return New Cpu2

    End Function

Public Function CreateMemory()As IMemory  _

Implements IAbstractFactory.CreateMemory

        Return New Memory2

    End Function

End Class

''搭配方式C

Public Class AbstractFactoryC

    Implements IAbstractFactory

    Public Function CreateCPU() As ICpu Implements IAbstractFactory.CreateCPU

        Return New Cpu1

    End Function

Public Function CreateMemory() As IMemory _

Implements IAbstractFactory.CreateMemory

        Return New Memory2

    End Function

End Class

(項目:AbstractFactoryDemo    檔案:AbstractFactory.vb)

接下來,本地區有三家電腦公司開足馬力開始生産電腦了,電腦的基本結構是一樣的,隻是不同的公司使用了不同的CPU和記憶體等配件類型搭配方案;面且,每個公司還是要在電腦上打上自己品牌的辨別,并使用不同的型号來區分電腦外觀或其它配件的不同(或者隻是為了吸引顧客);為了滿足這些需求,我們還需要區分電腦的品牌和型号。現在,我們建立所有品牌電腦的基類(Computer類),它的定義如下:

''電腦

Public Class Computer

    Public CPU As ICpu

    Public Memory As IMemory

    Public Brand As String '電腦品牌

    Public Model As String '型号

    '顯示電腦資訊

    Public Sub ShowInfo()

        Console.WriteLine("電腦品牌:" & Brand)

        Console.WriteLine("電腦型号:" & Model)

        Console.WriteLine("CPU品牌:" & CPU.Brand)

        Console.WriteLine("記憶體品牌:" & Memory.Brand)

    End Sub

End Class

(項目:AbstractFactoryDemo    檔案:AbstractFactory.vb)

想了解電腦的品牌、型号或配置資訊?調用ShowInfo()方法就行了。現在,我們建立A品牌電腦類ComputerA,代碼如下:

''A品牌電腦

Public Class ComputerA

    Inherits Computer

    '生産電腦

    Public Shared Function CreateComputer(ByVal strModel As String) As Computer

        Dim f As New AbstractFactoryA

        Dim c As New Computer

        c.CPU = f.CreateCPU()

        c.Memory = f.CreateMemory()

        c.Brand = "A品牌"

        c.Model = strModel

        Return c

    End Function

End Class

(項目:AbstractFactoryDemo    檔案:AbstractFactory.vb)

代碼中,我們使用CreateComputer()方法生産真正的A品牌電腦,并使用參數指定型号;在這個方法中,我們知道它必須使用方案A的标準裝配CPU和記憶體,是以,我們使用方案A的标準來建立電腦的CPU和記憶體類型;然後,我們将電腦貼上“A品牌”的商标(Brand),電腦型号(Model)則由方法的參數指定。為了使用友善,我們将這個方法設定為共享(Shared)方法。

現在,我們就來生産一台某型号的A品牌電腦,代碼如下:

Dim c1 As Computer = ComputerA.CreateComputer("2012A型")

(項目:AbstractFactoryDemo    檔案:Module1.vb)

B品牌和C品牌的電腦類應該不難建立。

好了,現在在本地電腦市場上,三家公司生産的電腦占有率可謂是三分天下,平分秋色;好在我們是搞軟體的,不會和他們有直接的競争關系。

我們的三名好朋友Tom、Jerry和Merry在不同的時間、不同的地點,不小心分别買了三家公司生産的電腦;下面,我們看看這三位好朋友的電腦都是什麼配置的:

Module Module1

    Sub Main()

        Dim c1 As Computer = ComputerA.CreateComputer("2012A型")

        Dim c2 As Computer = ComputerB.CreateComputer("陽光I型")

        Dim c3 As Computer = ComputerC.CreateComputer("12-1")

        Console.WriteLine("Tom的電腦")

        c1.ShowInfo()

        Console.WriteLine()

        Console.WriteLine("Jerry的電腦")

        c2.ShowInfo()

        Console.WriteLine()

        Console.WriteLine("Merry的電腦")

        c3.ShowInfo()

        Console.ReadLine()

    End Sub

End Module

(項目:AbstractFactoryDemo    檔案:Module1.vb)

運作的結果如下圖:

實際上,你還可以使用各種配件組裝自己的電腦,如下面的代碼:

Module Module1

    Sub Main()

        Dim myComputer As New Computer

        Console.WriteLine("我的組裝電腦")

        With myComputer

            .Brand = "DIY電腦"

            .Model = "光能2000"

            .CPU = New Cpu2

            .Memory = New Memory1

            .ShowInfo()

        End With

        Console.ReadLine()

    End Sub

End Module

(項目:AbstractFactoryDemo    檔案:Module1.vb)

代碼運作結果如下圖:

好吧,我們也不知道CPU-2和Memory-1在一起使用是否存在相容性問題,它們到底能不能正常的工作,隻有用用才知道了。

不過,在這個構架中也可以不允許随便組裝電腦,我們應該怎麼做呢?

我想,首先,應将電腦類型做成一個接口,如IComputer;第二步,将Computer類做成一個必須繼承的類(MustInherit),這樣就不能随便建立電腦對象了,不過别忘了它要實作IComputer接口;然後,将A、B、C三個品牌的電腦類改寫,我們需要将CreateComputer()方法的傳回值設定為IComputer接口。

如果有公司想擴充自己的産品線,生産不同的CPU和内容搭配方案的電腦又應該怎麼辦呢?

我們可以修改或重載一個電腦公司類(如ComputerA類)中的CreateComputer()方法,在方法中設定一個抽象工廠類型(IAbstractFactory接口)的參數就可以完成這個功能。

自己動手試試吧!

在介紹抽象工廠模式的最後,再讓我們來看看本例代碼中,這些電腦配件、組合方案、電腦公司和電腦的關系是怎樣的。

比較工廠方法與抽象工廠

工廠方法模式和抽象工廠模式,它們有什麼共同點、又有差別呢?

既然都叫“工廠”,那它們都是要生産東西的,實際上,應用工廠方法模式和抽象工廠模式的目的就是将對象的建立與其具體的類型解耦;在實際應用中,我們可以通過這兩個模式,在不了解具體類型的情況下建立對象。

首先來看看工廠方法模式,我們在重構戰争遊戲代碼的過程中使用了這個模式;在這個模式中,我們分别建立了地面、空中、海上和建築物四種類型的遊戲機關“工廠”,在工廠方法模式中将它們稱為“建立者(Creator)”,這四類建立者有着共同的基類CUnitCreator類。在這些建立者類中必須重寫的CreateUnit()方法,而這個方法就是工廠方法,它建立了不同系列的遊戲機關對象,這些對象都基于CUnit類型。在工廠方法模式中,我們需要定義很多被建立對象的類型,它們一般都基于一個超類,如一個基類或一個接口類型。這些具體的類則會是代碼維護上的一個挑戰。

在抽象工廠模式中,“抽象工廠”們也使用了一些CreateXXX()方法,實際上這也是一種工廠方法,隻不過這些方法建立的都是對象的“零件”。抽象工廠模式的關鍵就在于,它建立了統一的抽象工廠接口(如IAbstractFactory接口),此接口制定了标準化的“零件”結構組成;然後,定義一系列的抽象工廠類(如AbstractFactoryA、AbstractFactoryB和AbstractFactoryC類),這些抽象工廠類制定了一套套的對象“零件”配置方案。然後,我們必須使用其中一種方案組裝出标準化的對象;而這些對象一般隻有一個基本類型(類或接口),如上節示例中的Computer類型。

下面的表中,我們給出了工廠方法模式與抽象工廠模式之間的對比資訊:

對比内容 工廠方法模式 抽象工廠模式
建立者/抽象工廠

建立者類都繼承建立者基類,并且必須要重寫類中的“工廠方法”。

建立者類中的标準方法(工廠方法)建立最終的對象。

抽象工廠接口制定了一套标準化的對象的“零件”組成結構。

抽象工廠類都實作相同的抽象工廠接口;用于建立不同的“零件”組合的對象。

建立的對象

建立者類建立不同系列的對象。

這些對象一般都繼承同一個超類(一個基類或一個接口)。

通過不同的組合方案(抽象工廠類),建立出同一類型的對象。最終對象中“零件”組合的差異取決于采用了哪個抽象工廠。

雖然是同一類型,但通過使用不同的抽象工廠類,可以建立出不同結構的标準化對象。

優  點

建立的都是具體類型的對象,這些對象都繼承于同一個超類(接口或基類)。

需要時可以建立更多的類型,擴充比較靈活。

可以按标準化結建構立同一類型,但不同結構的對象。

可以有效地對元件中的結構進行标準化控制。并減少實際建立對象的具體類型的維護工作。

缺  點 太多的對象類型。包括建立者類和大量建立内容類,它們的維護都會加大工作量。 如果标準結構(抽象工廠接口)發生變化,需要修改每個抽象工廠類,以及最終對象類型的實作,代碼維護工作量比較大。

小結

本章我們介紹了兩種工廠模式——工廠方法模式與抽象工廠模式,它們在大型軟體的架構組織中非常有效,但我們必須區分它們的差異,将它們用在合适的地方。

請思考,為什麼在本章戰争遊戲代碼的重構過程中使用抽象工廠模式是不合适的。

出自:http://www.caohuayu.com/books/B0003/B0003.aspx

繼續閱讀