何时进行子类而不是区分行为

我难以决定何时应该进行子类化而不是仅添加表示类的不同模式的实例变量,然后让类的方法根据所选模式进行操作。

例如,假设我有一个基础车类。 在我的计划中,我会处理三种不同类型的汽车。 赛车公共汽车家庭模型 。 每个都有自己的齿轮实现,它们如何转动和座椅设置。 我是否应该将我的汽车子类化为三种不同的型号,或者我应该创建一个类型变量并使齿轮,车削和座椅通用,以便它们根据所选择的车型而有所不同?

在我目前的情况下,我正在开发一款游戏,并且我已经意识到它开始变得有点混乱,所以我会询问有关可能重构我当前代码的建议。 基本上有不同的地图,每个地图可以是三种模式之一。 根据地图定义的模式,将会有不同的行为,地图将以不同的方式构建。 在一种模式中,我可能必须在超时的基础上向玩家发放租金并产生生物,其中另一个玩家负责产生生物,而在另一个模式中可能存在一些自动生成的生物以及玩家衍生的生物和建造建筑物的玩家。 所以我想知道是否最好有一个基本地图类,然后将其子类化为每个不同的模式,或者是否继续沿着我当前添加差异化行为的路径,具体取决于地图类型变量设置为。

http://www.phremevbtalk.com的 AtmaWeapon所有学分都在这个post中回答

我认为这两种情况的核心是面向对象设计的基本规则:单一责任原则。 表达它的两种方式是:

"A class should have one, and only one, reason to change." "A class should have one, and only one, responsibility." 

SRP是一种无法始终满足的理想,遵循这一原则很难 。 我倾向于拍摄“一堂课应该尽可能少的责任。” 我们的大脑非常善于说服我们一个非常复杂的单个类比几个非常简单的类复杂。 我最近开始尽力编写较小的类,并且我的代码中的错误数量显着减少。 在解雇之前先试一下几个项目。

我首先提出,不是通过创建一个地图基类和三个子类来开始设计,而是从一个设计开始,该设计将每个地图的独特行为分成一个代表通用“地图行为”的二级类。 这篇文章关注的是certificate这种方法是优越的。 如果没有对代码的相当熟悉的知识,我很难具体,但我将使用一个非常简单的地图概念:

 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,并为每种类型的map派生一个新类型。 现在, Load()的责任在其他地方,因为对象不能用不同的实例替换自己。 您也可以为此制作工厂类:

 Class MapCreator Public Function GetMap(mapType) As Map End Class 

现在我们的Map层次结构看起来像这样(为简单起见,只定义了一个派生地图):

 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() ,所以你已经将国家管理的职责移到了个别class级。 我们再做一次SRP检查:

映射基类 0职责

映射派生类 – 必须管理状态(1) – 必须执行某些特定于类型的工作(1)

总计:2个职责

添加新地图 (与上述相同)2职责

每class职责总数: 2

添加新地图类的成本: 2

这要好得多。 我们的测试场景怎么样? 我们的状态更好,但仍然不太对劲。 我们可以在派生类上放置一些“敌人”属性,因为每个类都是独立的,如果我们需要特定的信息,我们可以转换为特定的地图类型。 如果你有RentalMapSlowRentalMapFast怎么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 

现在使用map会给它一个特定的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有自己的一组测试。 但由于HardPlayerSpawnMapPlayerSpawnMap都使用PlayerSpawnBehavior ,那么如果我测试了PlayerSpawnBehavior我就不必为使用该行为的地图编写任何与行为相关的测试! 让我们比较测试场景。

在“一个带有类型参数的类”的情况下,如果3个行为有3个难度级别,并且每个行为有10个测试,那么您将编写90个测试(不包括测试以查看是否从每个行为转到另一个行为) 。)在“派生类”场景中,你将有9个类,每个类需要10个测试:90个测试。 在“行为类”场景中,您将为每个行为编写10个测试:30个测试。

这是责任理由:地图有1个责任:跟踪行为。 行为有两个职责:维护状态和执行操作。

每class职责总数: 3

添加新地图类的成本: 0(重用行为)或2(新行为)

因此,我认为“行为类”场景并不比“派生类”场景更难编写,但它可以显着减轻测试负担。 我已经读过这样的技术并且多年来一直认为它们“太麻烦”,而且最近才意识到它们的价值。 这就是为什么我写了将近10,000个字符来解释它并certificate它的合理性。

您应该在子类型是父类型的某种特化的任何地方进行子类化。 换句话说,如果您只需要function,则应该避免inheritance。 正如Liskov替换原则所述:“如果S是T的子类型,则程序中类型T的对象可以用类型S的对象替换而不改变该程序的任何所需属性”

在你的情况下,我将使用混合方法(这可能被称为组合,我不知道),其中您的地图模式变量实际上是一个单独的对象,将所有相关的数据/行为存储到地图的模式。 通过这种方式,您可以拥有任意数量的模式,而无需对Map类进行过多操作。

gutofb7将它钉在头上,以便何时想要inheritance某些东西。 给出一个更具体的例子:在你的汽车课上,在你的程序中的任何地方,你正在处理什么类型的汽车? 现在如果您将Map子类化,那么您需要编写多少代码才能处理特定的子类?

在您讨论的地图和产生的特定问题中,我认为这是一个您希望合成优于inheritance的情况 。 当你考虑它时,它们不完全是三种不同类型的地图。 相反,它们是相同的地图,有三种不同的产卵策略。 因此,如果可能,您应该将产卵函数设置为单独的类,并将产卵类的实例作为地图的成员。 如果你的地图的“模式”中的所有其他差异在本质上是相似的,你可能根本不必对地图进行子类化,尽管对不同的组件进行子类化(即具有spawn_strategy基类并从中inheritance三种类型的产生) ,或者至少给它们一个共同的界面,可能是必要的。

鉴于你的评论认为每种类型的地图在概念上都是不同的,那么我建议进行子类化,因为这似乎符合Liskov的替代原则。 但是,这并不是说你应该完全放弃作曲。 对于每种类型的映射具有但可能具有不同行为/实现的那些属性,您应该考虑使您的基类将它们作为组件。 这样,如果需要,您仍然可以混合和匹配function,同时使用inheritance来保持关注点的分离。

我不用C#编程,但是在Ruby on Rails,Xcode和Mootools(javascript OOP框架)中可以提出同样的问题。

我不喜欢有一种方法,当某个永久性的属性是错误的时,它永远不会被使用。 就像它是一个VW Bug一样,某些齿轮永远不会转动。 那太傻了。

如果我找到一些这样的方法,我会尝试抽象出所有可以在我所有不同的“汽车”中共享到父类中的东西,每种汽车都使用方法和属性,然后用它们来定义子类。具体方法。