我很难决定何时应该进行子类化,而不仅仅是添加一个代表该类的不同模式的实例变量,然后让该类的方法根据所选模式起作用。
例如,说我有基础汽车课。在我的程序中,我将处理三种不同类型的汽车。赛车,公共汽车和家庭模型。每个人都有自己的齿轮实现方式,如何旋转和座椅设置。我应该将我的汽车分为三个不同的模型,还是应该创建一个类型变量,并使齿轮,转弯和座椅通用,以便根据所选择的汽车类型而有所不同?
在目前的情况下,我正在开发一款游戏,但我逐渐意识到它开始变得有些混乱,因此我就可能重构当前代码的问题征求意见。基本上有不同的地图,每个地图可以是三种模式之一。根据定义哪种地图,将有不同的行为,并且将以不同的方式构建地图。在一种模式下,我可能不得不在超时的基础上向玩家出租并生成生物,其中另一种模式是玩家负责生成生物,而在另一种模式下,可能会有一些自动生成的生物以及玩家生成的生物和建造建筑物的玩家。因此,我想知道是否最好有一个基本的地图类,然后将其子类化为每种不同的模式,或者是否继续按照当前的地图类型变量设置不同的方式添加差异化行为。
最佳答案
http://www.xtremevbtalk.com回答in this thread的所有AtmaWeapon积分
我认为两种情况的核心都是面向对象设计的基本规则:单一责任原则。表达它的两种方法是:
"A class should have one, and only one, reason to change."
"A class should have one, and only one, responsibility."
SRP是无法始终满足的理想,因此很难遵循此原则。我倾向于拍摄“一个班级应该承担的责任越少”。我们的大脑非常擅长说服我们一个非常复杂的单一类比几个非常简单的类复杂。最近,我开始尽力编写较小的类,并且代码中的错误数量大大减少了。在解散之前,先尝试一些项目。
我首先提出,与其通过创建地图基类和三个子类来开始设计,不如从将每个地图的独特行为分离为代表通用“地图行为”的第二类的设计开始。这篇文章是关于证明这种方法优越的。在没有相当熟悉您的代码的情况下,很难做到具体,但是我将使用一个非常简单的映射概念:
Public Class Map
Public ReadOnly Property MapType As MapType
Public Sub Load(mapType)
Public Sub Start()
End Class
MapType表示地图代表三种地图类型中的哪一种。当您要更改地图类型时,请使用要使用的地图类型调用
Load()
;这会执行清除当前地图状态,重置背景等所需的操作。加载地图后,将调用Start()。如果地图上有任何行为,例如“每y秒产生一个怪兽x”,则Start()负责配置这些行为。这就是您现在拥有的,并且明智地认为这是一个坏主意。自从我提到SRP以来,让我们计算一下Map的职责。
它必须管理所有三种地图类型的状态信息。 (3个以上职责*)
Load()
必须了解如何清除所有三种地图类型的状态以及如何为所有三种地图类型设置初始状态(6个职责)Start()
必须知道每种地图类型的处理方法。 (3个职责)**从技术上讲,每个变量都是责任,但我已经简化了它。*
对于最终总数,如果添加第四个地图类型会怎样?您必须添加更多状态变量(1个以上的职责),更新
Load()
以能够清除和初始化状态(2个职责),并更新Start()
以处理新行为(1个职责)。所以:Map
职责数:12+新地图所需的更改数量:4+
还有其他问题。奇怪的是,几种地图类型将具有相似的状态信息,因此您将在状态之间共享变量。这使
Load()
更有可能忘记设置或清除变量,因为您可能不记得一个映射将_foo用于一个目的而另一个映射将它完全用于另一个目的。要测试这一点也不容易。假设您要针对以下场景编写测试:“当我创建“生成怪物”地图时,该地图应每五秒钟生成一个新怪物。”很容易讨论如何进行测试:创建地图,设置地图类型,启动地图,等待五秒钟以上并检查敌人人数。但是,我们的界面当前没有“敌人计数”属性。我们可以添加它,但是如果这是唯一一张有敌人数的地图怎么办?如果添加该属性,则将有一个属性在2/3的情况下无效。还不是很清楚我们是否在不阅读测试代码的情况下测试“ spawn monsters”地图,因为所有测试都将测试
Map
类。您当然可以使
Map
为抽象基类Start()
MustOverride,并为每种地图类型派生一个新类型。现在,Load()
的责任在其他地方,因为对象无法用其他实例替换自身。您也可以为此设置工厂类:Class MapCreator
Public Function GetMap(mapType) As Map
End Class
现在我们的地图层次结构可能看起来像这样(为简单起见只定义了一个派生地图):
Public MustInherit Class Map
Public MustOverride Sub Start()
End Class
Public Class RentalMap
Inherits Map
Public Overrides Sub Start()
End Class
由于已经讨论的原因,不再需要
Load()
。 MapType
在地图上是多余的,因为您可以检查对象的类型以查看其内容(除非您有几种RentalMap
类型,然后它再次变得有用。)Start()
在每个派生类中均被覆盖,因此,您已将状态管理的职责移至各个类。让我们再进行一次SRP检查:地图基类
0职责
地图派生类
-必须管理状态(1)
-必须执行某些类型特定的工作(1)
总计:2个职责
新增地图
(同上)2个职责
每班职责总数:2
添加新地图类的费用:2
这样好多了。那我们的测试场景呢?我们的状态更好,但仍然不太正确。我们可以在派生类中放置“敌人数量”属性,因为每个类都是独立的,如果需要特定信息,我们可以转换为特定地图类型。不过,如果您有
RentalMapSlow
和RentalMapFast
怎么办?您必须为每个类重复测试,因为每个类都有不同的逻辑。因此,如果您有4个测试和12个不同的地图,则将编写并稍微调整48个测试。我们该如何解决?创建派生类时我们做了什么?我们确定了每次更改的类的一部分,并将其下推到子类中。如果我们创建了一个单独的
MapBehavior
类而不是子类,而该类可以随意交换,该怎么办呢?让我们看一下一种派生行为的外观:Public Class Map
Public ReadOnly Property Behavior As MapBehavior
Public Sub SetBehavior(behavior)
Public Sub Start()
End Class
Public MustInherit Class MapBehavior
Public MustOverride Sub Start()
End Class
Public Class PlayerSpawnBehavior
Public Property EnemiesPerSpawn As Integer
Public Property MaximumNumberOfEnemies As Integer
Public ReadOnly Property NumberOfEnemies As Integer
Public Sub SpawnEnemy()
Public Sub Start()
End Class
现在,使用映射涉及为其赋予特定的
MapBehavior
并调用Start()
,该委托将行为的Start()
委托给该映射。所有状态信息都在行为对象中,因此地图实际上不需要了解任何信息。但是,如果您想要特定的地图类型,那么先创建行为再创建地图似乎不方便,对吧?因此,您可以派生一些类:Public Class PlayerSpawnMap
Public Sub New()
MyBase.New(New PlayerSpawnBehavior())
End Sub
End Class
就是这样,一个新类的一行代码。想要硬玩家生成地图吗?
Public Class HardPlayerSpawnMap
Public Sub New()
' Base constructor must be first line so call a function that creates the behavior
MyBase.New(CreateBehavior())
End Sub
Private Function CreateBehavior() As MapBehavior
Dim myBehavior As New PlayerSpawnBehavior()
myBehavior.EnemiesPerSpawn = 10
myBehavior.MaximumNumberOfEnemies = 300
End Function
End Class
那么,这与在派生类上具有属性有何不同?从行为的角度来看,并没有太大的区别。从测试的角度来看,这是一个重大突破。
PlayerSpawnBehavior
有自己的一组测试。但是由于HardPlayerSpawnMap
和PlayerSpawnMap
都使用PlayerSpawnBehavior
,所以如果我已经测试过PlayerSpawnBehavior
,则不必为使用行为的映射编写任何与行为相关的测试!让我们比较测试方案。在“一个带有类型参数的类”的情况下,如果3个行为的难度级别为3,并且每个行为具有10个测试,则您将编写90个测试(不包括测试,以查看是否从每个行为转换为另一个行为。)在“派生类”方案中,您将有9个类,每个类需要10个测试:90个测试。在“行为类”场景中,您将为每种行为编写10个测试:30个测试。
这是责任统计:
地图有1个责任:跟踪行为。
行为有2个责任:维持状态和执行动作。
每班职责总数:3
添加新的地图类的成本:0(重用行为)或2(新行为)
因此,我的观点是,“行为类”方案的编写并不比“派生类”方案更难编写,但是它可以大大减轻测试的负担。我已经读过关于这种技术的文章,多年来视它们为“太多麻烦”,直到最近才意识到它们的价值。这就是为什么我写了将近10,000个字符来说明它并为其辩护的原因。