当子类化,而不是区分行为 [英] When to Subclass instead of differentiating the behaviour

查看:182
本文介绍了当子类化,而不是区分行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有困难的时候决定我应该继承,而不是仅仅增加一个代表类的不同的模式,然后让类行为的方法,根据所选模式的一个实例变量。

I'm having difficulties deciding when I should be subclassing instead of just adding an instance variable that represents different modes of the class and then let the methods of the class act according to the selected mode.

举例来说,假设我有一个的基地车的类。在我的节目,我会处理三种不同类型的汽车。的赛车总线的和的家庭模式的。每次都会有自己的执行档,他们是如何把和座位设置。我应该继承我的车到三种不同型号,或者我应该创建一个类型的变量,使齿轮,回转和休息通用的,所以他们会根据选择的是哪一车型行事不同?

For example, say I've a base car class. In my program I'll deal with three different types of cars. Race cars, busses and family models. Each will have their own implementation of gears, how they turn and seat setup. Should I subclass my car into the three different models or should I create a type variable and make the gears, turning and seating generic so they would act different depending on which car type was selected?

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

In my current situation I'm working on a game, and I've come to realise that it's starting to get a bit messy, so I ask advice on possibly refactoring of my current code. Basically there are different maps, and each map can be one of three modes. Depending on which mode the map is defined as there will be different behaviour and the map will be built in a different way. In one mode I might have to give out rentals to players and spawn creatures on a timeout basis, wherein another the player is responsable for spawning the creatures and yet in another there might be some automated spawned creatures alongside with player spawned ones and players constructing buildings. So I'm wondering whether it would be best to have a base map class, and then subclass it into each of the different modes, or whether to continue down my current path of adding differentiated behaviour depending on what the map type variable is set to.

推荐答案

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

All credits to AtmaWeapon of http://www.xtremevbtalk.com answering in this thread

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

Core to both situations is what I feel is the fundamental rule of object-oriented design: the Single Responsibility Principle. Two ways to express it are:

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



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

SRP is an ideal that can't always be met, and following this principle is hard. I tend to shoot for "A class should have as few responsibilities as possible." Our brains are very good at convincing us that a very complicated single class is less complicated than several very simple classes. I have started doing my best to write smaller classes lately, and I've experienced a significant decrease in the number of errors in my code. Give it a shot for a few projects before dismissing it.

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

I first propose that instead of starting the design by creating a map base class and three child classes, start with a design that separates the unique behaviors of each map into a secondary class that represents generic "map behavior". This post is concerned with proving this approach is superior. It is hard for me to be specific without a fairly intimate knowledge of your code, but I'll use a very simple notion of a map:

Public Class Map
    Public ReadOnly Property MapType As MapType

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

的MapType 的指示哪个三种地图类型地图表示的。当您要更改地图类型,你叫加载()与您要使用的地图类型;这样做不管它需要做清除当前地图状态,重新设置背景等地图加载后,的开始()的被调用。如果地图有类似重生暴龙X每Y秒的任何行为的开始()的负责配置这些行为。

MapType indicates which of the three map types the map represents. When you want to change the map type, you call Load() with the map type you want to use; this does whatever it needs to do to clear the current map state, reset the background, etc. After a map is loaded, Start() is called. If the map has any behaviors like "spawn monster x every y seconds", Start() is responsible for configuring those behaviors.

这是你现在,你是明智的,认为这是一个坏主意。既然我提到SRP,让我们来算的地图

This is what you have now, and you are wise to think it's a bad idea. Since I mentioned SRP, let's count the responsibilities of Map.


  • 它来管理所有三个状态信息地图类型。 (3+职责*)

  • 加载()必须了解如何清除状态,对所有三种地图类型以及如何设置为所有三种地图类型(6职责)的初始状态

  • 开始()已知道要为每个地图类型做。 (3职责)

  • It has to manage state information for all three map types. (3+ responsibilities*)
  • Load() has to understand how to clear the state for all three map types and how to set up the initial state for all three map types (6 responsibilities)
  • Start() has to know what to do for each map type. (3 responsibilities)

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

有关最终总,会发生什么,如果你添加第四个地图类型?你必须添加的更多的状态变量(1+责任),更新加载(),能够清除和初始化状态(2职责)和更新开始()来处理新的行为(1负责)。所以:

For the final total, what happens if you add a fourth map type? You have to add more state variables (1+ responsibilities), update Load() to be able to clear and initialize state (2 responsibilities), and update Start() to handle the new behavior (1 responsibility). So:

地图数职责: 12 +

Number of Map responsibilities: 12+

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

还有其他的问题了。奇怪的是,几个地图类型将有类似的状态信息,因此,您在各州之间共享变量。这使得它更可能是加载()会忘记设置或清除一个变量,因为你可能不记得,一个地图使用*的的对目的只有一个,另一个则使用它完全是一个不同的目的。

There's other problems too. Odds are, several of the map types will have similar state information, so you'll share variables among the states. This makes it more likely that Load() will forget to set or clear a variable, since you might not remember that one map uses *foo for one purpose and another uses it for a different purpose entirely.

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

It's not easy to test this, either. Suppose you want to write a test for the scenario "When I create a 'spawn monsters' map, the map should spawn one new monster every five seconds." It's easy to discuss how you might test this: create the map, set its type, start it, wait a little bit longer than five seconds, and check the enemy count. However, our interface currently has no "enemy count" property. We could add it, but what if this is the only map that has an enemy count? If we add the property, we'll have a property that's invalid in 2/3 of the cases. It's also not very clear that we are testing the "spawn monsters" map without reading the test's code, since all tests will be testing the Map class.

您当然可以让地图一个抽象基类,开始() MustOverride,并从中获得其中新型每种类型的地图。现在,责任加载()是别的地方,因为对象不能用不同的实例来替换自己。你不妨做一个工厂类这样的:

You could certainly make Map an abstract base class, Start() MustOverride, and derive one new type for each type of map. Now, the responsibility of Load() is somewhere else, because an object can't replace itself with a different instance. You may as well make a factory class for this:

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

现在我们的地图层次结构可能是这个样子(只有一个派生图的定义为简单起见):

Now our Map hierarchy might look something like this (only one derived map was defined for simplicity):

Public MustInherit Class Map
    Public MustOverride Sub Start()
End Class

Public Class RentalMap
    Inherits Map

    Public Overrides Sub Start()
End Class

加载(),不再需要对已经讨论的原因。 地图类型是在地图上多余的,因为你可以检查对象的类型,看看它是什么(除非你有几种类型的 RentalMap ,然后重新变得有用。)开始()在每个派生类中重写,所以你移动的状态管理的责任落实到各个班级。让我们做另外一个SRP检查:

Load() isn't needed anymore for reasons already discussed. MapType is superfluous on a map because you can check the type of the object to see what it is (unless you have several types of RentalMap, then it becomes useful again.) Start() is overridden in each derived class, so you've moved the responsibilities of state management to individual classes. Let's do another SRP check:

地图基类
0责任

Map base class 0 responsibilities

地图派生类
- 必须管理状态(1)
- 必须执行某些特定类型的工作(1)

Map derived class - Must manage state (1) - Must perform some type-specific work (1)

共2职责

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

Adding a new map (Same as above) 2 responsibilities

的每个类的职责总数: 2

添加一个新的地图类的成本 2

Cost of adding a new map class: 2

这是好多了。那我们的测试场景?我们在更好的状态,但仍然不完全正确。我们可以逃脱把一个财产的敌人数量我们的派生类,因为每个类是独立的,我们可以转换为特定的地图类型,如果我们需要的具体信息。不过,如果你有什么 RentalMapSlow RentalMapFast ?你要复制你的测试每个类,因为每个人都有不同的逻辑。所以,如果你已经有了4次测试和12个不同的地图,你会写,略调整48测试。我们如何解决这个问题?

This is much better. What about our test scenario? We're in better shape but still not quite right. We can get away with putting a "number of enemies" property on our derived class because each class is separate and we can cast to specific map types if we need specific information. Still, what if you have RentalMapSlow and RentalMapFast? You have to duplicate your tests for each of these classes, since each has different logic. So if you've got 4 tests and 12 different maps, you'll be writing and slightly tweaking 48 tests. How do we fix this?

我们做了什么,当我们做出的派生类?我们确定了这是每次改变类的一部分,并推它分解成子类。如果,而不是子类,我们创建了一个单独的 MapBehavior 类,我们可以随意换入或换出?让我们来看看这可能是什么样子一个派生行为:

What did we do when we made the derived classes? We identified the part of the class that was changing each time and pushed it down into sub-classes. What if, instead of subclasses, we created a separate MapBehavior class that we can swap in and out at will? Let's see what this might look like with one derived behavior:

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 并调用开始(),它委托给行为的开始()。所有状态信息的行为对象,所以在地图并不真正需要知道任何事情。不过,如果你想要一个特定的地图类型,似乎不方便,要创建一个行为,然后创建一个地图,对吧?所以,你得到了一些类:

Now using a map involves giving it a specific MapBehavior and calling Start(), which delegates to the behavior's Start(). All state information is in the behavior object, so the map doesn't really have to know anything about it. Still, what if you want a specific map type, it seems inconvenient to have to create a behavior then create a map, right? So you derive some classes:

Public Class PlayerSpawnMap
    Public Sub New()
        MyBase.New(New PlayerSpawnBehavior())
    End Sub
End Class

就这样一的代码线的新类。希望有一个努力的球员产卵地图?

That's it, one line of code for a new class. Want a hard player spawn map?

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 我没有写对于使用行为的地图的行为相关的测试!让我们比较测试方案。

So, how is this different from having properties on derived classes? From a behavioral standpoint there's not much different. From a testing viewpoint, this is a major breakthrough. PlayerSpawnBehavior has its own set of tests. But since HardPlayerSpawnMap and PlayerSpawnMap both use PlayerSpawnBehavior, then if I've tested PlayerSpawnBehavior I don't have to write any behavior-related tests for a map that uses the behavior! Let's compare test scenarios.

在的情况下有一个类型参数一个类,如果有3行为3个难度级别,每个行为的有10个测试,你会在写90测试(不包括测试,看看每个行为到另一个作品去)在派生类的情景,你就会有9类,每个需要10个测试:90测试。在行为类的情景,你会写每一行为10个测试:30测试

In the "one class with a type parameter" case, if there are 3 difficulty levels for 3 behaviors, and each behavior has 10 tests, you'll be writing 90 tests (not including tests to see if going from each behavior to another works.) In the "derived classes" scenario, you'll have 9 classes that need 10 tests each: 90 tests. In the "behavior class" scenario, you'll write 10 tests for each behavior: 30 tests.

下面是负责理货:
地图有1个责任:跟踪的一种行为。
行为已经2职责:维护国家和执行操作

Here's the responsibility tally: Map has 1 responsibility: keep track of a behavior. Behavior has 2 responsibilities: maintain state and perform actions.

的每个类的职责总数: 3

添加一个新的地图类的费用: 0(重复使用行为)或2(新行为)

Cost of adding a new map class: 0 (reuse a behavior) or 2 (new behavior)

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

So, my opinion is that the "behavior class" scenario is no more difficult to write than the "derived classes" scenario, but it can significantly reduce the burden of testing. I've read about techniques like this and dismissed them as "too much trouble" for years and only recently realized their value. This is why I wrote nearly 10,000 characters to explain it and justify it.

这篇关于当子类化,而不是区分行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆