当子类,而不是区分行为(When to Subclass instead of differenti

2019-08-02 18:27发布

我有困难决定何时我应该代替子类的只是增加代表类的不同模式,然后让类的行为的方法,根据所选择的模式的一个实例变量。

例如,假设我有一个基地车类。 在我的计划,我会处理三种不同类型的汽车。 赛车公共汽车家庭模式 。 每个都会有自己的执行档,他们是如何转身座椅设置。 我应该继承我的车开进了三种不同的模式,或者我应该创建一个类型的变量,使齿轮转动和休息通用的,所以他们会选择具体取决于车型行动有什么不同?

在我目前的状况,我工作的一个游戏,我已经认识到,它开始变得有点乱,所以我就可能是我目前的代码重构寻求建议。 基本上有不同的地图,并且每个地图可以是三种模式中的一种。 根据该模式,地图上被定义为会有不同的行为和地图将建成以不同的方式。 在一个模式,我可能不得不放弃了租借球员和超时的基础上,其中另一玩家RESPONSABLE在另一个产卵的动物,但在产卵动物可能有一些自动化的催生与玩家催生者和玩家建造建筑物旁边的动物。 所以我不知道是否会是最好有一个基本地图类,然后将其子类到每个不同的模式,还是继续往下我现在加入这取决于地图类型变量被设置为差异化的行为路径。

Answer 1:

所有学分的AtmaWeapon http://www.xtremevbtalk.com回答在这个线程

核心两种情况是什么,我觉得是面向对象设计的基本原则:单一职责原则。 两种方式来表达它是:

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

SRP的是,不能总是得到满足的理想,并按照这一原则很难 。 我倾向于拍了“A类应该有尽可能少的责任可能。” 我们的大脑是在说服我们,一个非常复杂的单一类是以上几种非常简单的类不太复杂的非常好。 我已经开始尽我所能去最近写小班的,我已经经历在我的代码中的错误的数量显著下降。 解雇之前给它几个项目的一个镜头。

我首先提出的,而不是通过创建一个映射的基类和三个子类在开始设计的,与每个地图的独特的行为分成代表通用的“地图行为”二次类设计开始。 这篇文章关注的是证明这种做法是优越的。 这是我很难具体没有你的代码的一个相当贴心的知识,但我会用一个地图的一个非常简单的概念:

Public Class Map
    Public ReadOnly Property MapType As MapType

    Public Sub Load(mapType)
    Public Sub Start()
End Class

地图类型指示所述三种地图类型的地图表示的。 当你要更改地图类型,你可以调用Load()与您要使用的地图类型; 这样做不管它需要一个地图加载后做清除当前地图状态,重新设置背景等,start()被调用。 如果地图有像任何行为“重生的怪物X每Y秒”, 启动()负责配置这些行为。

这是你现在拥有什么,你是明智的,认为这是一个坏主意。 由于我提到SRP,让我们计算地图的职责。

  • 它用于管理所有三种地图类型的状态信息。 (3+职责*)
  • Load()必须了解如何清除状态所有三种地图类型,以及如何设置初始状态为所有三种地图类型(6项职责)
  • Start()必须知道要为每个地图类型做。 (3项职责)

**技术上每个变量是一种责任,但我已经简化它。*

对于总决赛,如果加上第四地图类型会发生什么? 你必须增加更多的状态变量(1+责任),更新Load()以能够清楚和初始化状态(2项职责),以及更新Start()来处理新的行为(1名责任)。 所以:

Map职责:12+

新地图需要改变的次数:4+

还有其他的问题了。 奇怪的是,几个地图类型都会有类似的状态信息,所以你会在国家之间的共享变量。 这使得它更可能是Load()会忘记设置或清除一个变量,因为你可能不记得,一个地图使用_foo为了一个目的,另一个则使用它完全不同的目的。

这并不容易,以测试这一点,无论是。 假设你想要写的情景测试“当我创建一个‘重生的怪物’地图,地图应该催生一个新的怪物每五秒钟。” 这很容易,讨论你怎么可能测试:创建地图,将其类型,启动它,等待一点点时间超过五秒钟,然后检查敌人计数。 然而,我们的界面目前还没有“敌数”属性。 我们可以添加它,但如果这是什么,有一个敌人算唯一的地图吗? 如果我们添加属性,我们将有一个在的情况下2/3无效的属性。 这也是不是很清楚,我们没有阅读测试的代码测试“重生的怪物”的地图,因为所有的测试将要测试的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

这是好多了。 那我们的测试场景? 我们在更好的状态,但仍然不完全正确。 我们可以逃脱把财产“的敌人数量”我们的派生类,因为每个类是独立的,我们可以转换为特定的地图类型,如果我们需要的特定信息。 不过,如果你有RentalMapSlowRentalMapFast ? 你要复制你的测试对每个类的,因为每个人都有不同的逻辑。 所以,如果你已经有了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有它自己的一套测试。 但由于HardPlayerSpawnMapPlayerSpawnMap都使用PlayerSpawnBehavior ,那么如果我测试过PlayerSpawnBehavior我没有写对于使用行为的地图的任何行为相关的测试! 让我们比较测试方案。

在这种情况下“与一类参数一类”,如果有3种行为3个难度级别,每个行为有10次测试,你会写90个测试(不包括测试,看看从各行为人要到另一个作品。)在“派生类”的情况下,你将有9类,每个需要10个测试:90次测试。 在“行为类”的情况下,你会写的每一个行为10次:30次测试。

这里的负责理货:地图有1个职责:跟踪行为的。 行为2个职责:维护国家和执行操作。

总数的每个类的职责:3

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

所以,我的意见是,“行为类”的场景是没什么难度比“派生类”的情况来写,但它可以显著减少测试的负担。 我读过有关这样的技术,并驳回他们是“太麻烦”了多年,最近才实现了自己的价值。 这就是为什么我写了近10,000个字符来解释它,证明它。



Answer 2:

你应该继承无论你的孩子的类型是某种父类型的专业化。 换句话说,你应该避免继承,如果你只是需要的功能。 作为里氏替换原则规定:“如果S是T的子类型,则在节目类型T的对象可以与S型的对象替换,而不改变任何节目的期望的性质的”



Answer 3:

在你的情况,我会用一种混合的方法去(这可能是所谓的组成,我不知道),你的地图模式变量实际上是一个单独的对象,所有相关的数据/存储行为到地图的模式。 这样,你喜欢不真正做太多的地图类,你可以有很多方式。

gutofb7钉它的头,当你想继承的东西。 给予更具体的例子:在您的汽车类,它会在你的程序是什么类型的车你处理它无论在任何地方? 现在,如果你的子类地图,多少代码将您必须编写特定的子类的交易?



Answer 4:

在你谈到与地图和产卵的具体问题,我认为这是要赞成的情况下, 在继承组成 。 当你想想看,它们并不完全三种不同类型的地图。 相反,它们是同一个地图产卵三种不同的策略。 所以,如果可能的话,你应该做的产卵功能的单独的类,有一个产卵类的实例作为地图中的一员。 如果在“模式”为您的地图中的所有其他差异性质相似,你可能没有在所有子类的地图,虽然继承了不同的组件(即有spawn_strategy基类继承从产卵的三种类型) ,或至少给他们一个共同的接口,可能是必要的。

鉴于您的评论说,图的每一个类型,就是要在概念上不同,那么我会建议子类,因为这似乎满足里氏的替代原则。 然而,这并不是说你应该组成完全放弃。 对于其中每种类型的地图都有,但可能有不同的行为/实现这些属性,你应该考虑你的基类有他们的部件。 这样,你仍然可以混合和匹配功能,如果你需要,而使用继承保持关注点分离。



Answer 5:

我不计划在C#中,但在Ruby on Rails的,Xcode中,和MooTools的(JavaScript的OOP架构)相同的问题可以问。

我不喜欢当达到一定的,永久的,财产是错误的,将永远不会被使用的方法。 一样,如果它是一个大众的Bug,某些挡位将永远不会被打开。 这是愚蠢的。

如果我发现这样的一些方法,我尝试抽象的东西展现出来,可我的所有不同的“车”到父类之间共享,有方法和属性的各种汽车的使用,然后用定义子类的具体方法。



文章来源: When to Subclass instead of differentiating the behaviour
标签: c# oop