我难以决定何时应该进行子类化而不是仅添加表示类的不同模式的实例变量,然后让类的方法根据所选模式进行操作.
例如,假设我有一个基础车类.在我的计划中,我会处理三种不同类型的汽车.赛车,公共汽车和家庭模型.每个人都有自己的齿轮实现,如何转动和座椅设置.我应该将我的汽车继承到三个不同的车型,还是应该创建一个类型变量并使齿轮,车削和座椅通用,以便它们根据所选择的车型而有所不同?
在我目前的情况下,我正在开发一款游戏,而且我已经意识到它开始变得有点混乱,所以我会询问有关可能重构我当前代码的建议.基本上有不同的地图,每个地图可以是三种模式之一.根据地图定义的模式,将会有不同的行为,地图将以不同的方式构建.在一种模式中,我可能必须在超时基础上向玩家和产生生物的租金,其中另一个玩家负责产生生物,而在另一个模式中可能有一些自动生成的生物与玩家产生的生物和建造建筑物的玩家.所以我想知道是否最好有一个基本地图类,然后将其子类化为每个不同的模式,或者是否继续沿着当前添加差异化行为的路径,具体取决于地图类型变量设置为.
http://www.xtremevbtalk.com的 AtmaWeapon所有学分回答此主题
我认为这两种情况的核心是面向对象设计的基本规则:单一责任原则.表达它的两种方式是:
"A class should have one, and only one, reason to change."
"A class should have one, and only one, responsibility."
Run Code Online (Sandbox Code Playgroud)
SRP是一种不能始终满足的理想,遵循这一原则很难.我倾向于拍摄"一个班级应尽可能少负责".我们的大脑非常善于说服我们一个非常复杂的单个类比几个非常简单的类复杂.我最近开始尽力编写较小的类,并且我的代码中的错误数量显着减少.在解雇之前先试一试几个项目.
我首先提出,不是通过创建一个地图基类和三个子类来启动设计,而是从一个设计开始,该设计将每个地图的独特行为分成一个代表通用"地图行为"的二级类.这篇文章关注的是证明这种方法更优越.如果没有对代码的相当熟悉的知识,我很难具体,但我将使用一个非常简单的地图概念:
Public Class Map
Public ReadOnly Property MapType As MapType
Public Sub Load(mapType)
Public Sub Start()
End Class
Run Code Online (Sandbox Code Playgroud)
MapType指示地图表示的三种地图类型中的哪一种.如果要更改地图类型,请Load()使用要使用的地图类型进行调用; 这可以做任何事情来清除当前的地图状态,重置背景等.加载地图后,调用Start().如果地图有任何行为,例如"每隔y秒生成一个怪物x",则Start()负责配置这些行为.
这就是你现在所拥有的,你认为这是一个坏主意是明智的.自从我提到SRP以来,让我们来看看Map的职责.
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
Run Code Online (Sandbox Code Playgroud)
现在我们的Map层次结构可能看起来像这样(为简单起见,只定义了一个派生地图):
Public MustInherit Class Map
Public MustOverride Sub Start()
End Class
Public Class RentalMap
Inherits Map
Public Overrides Sub Start()
End Class
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
现在使用地图涉及给它一个特定的MapBehavior和调用Start(),它委托给行为Start().所有状态信息都在行为对象中,因此地图实际上不必了解它.但是,如果你想要一个特定的地图类型,那么创建一个行为然后创建一个地图似乎不方便,对吧?所以你派出了一些类:
Public Class PlayerSpawnMap
Public Sub New()
MyBase.New(New PlayerSpawnBehavior())
End Sub
End Class
Run Code Online (Sandbox Code Playgroud)
就是这样,新类的一行代码.想要一个硬玩家产生地图吗?
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
Run Code Online (Sandbox Code Playgroud)
那么,这与派生类的属性有何不同?从行为的角度来看,没有太大的不同.从测试的角度来看,这是一个重大突破.PlayerSpawnBehavior有自己的一套测试.但是既然HardPlayerSpawnMap又PlayerSpawnMap使用了PlayerSpawnBehavior,那么如果我已经测试过,PlayerSpawnBehavior我就不必为使用该行为的地图编写任何与行为相关的测试!让我们比较测试场景.
在"带有类型参数的一个类"的情况下,如果3个行为有3个难度级别,并且每个行为有10个测试,那么您将编写90个测试(不包括测试以查看是否从每个行为转到另一个行为) .)在"派生类"场景中,你将有9个类,每个类需要10个测试:90个测试.在"行为类"场景中,您将为每个行为编写10个测试:30个测试.
这是责任理由:地图有1个责任:跟踪行为.行为有两个职责:维护状态和执行操作.
每班职责总数: 3
添加新地图类的成本: 0(重用行为)或2(新行为)
因此,我认为"行为类"场景并不比"派生类"场景更难编写,但它可以显着减轻测试负担.我已经读过这样的技术并且多年来一直认为它们"太麻烦",而且最近才意识到它们的价值.这就是为什么我写了近10,000个字符来解释它并证明它的合理性.