17.创建作战单位——工厂方法、抽象工厂

制作一个大型的游戏需要许多人的参与,你当然是程序开发方面的专家;另外,还有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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值