正确的设计对象层次结构方法

3
在我的游戏Bitfighter中,我有一个名为AbstractTeam的类,它有两个子类:Team和EditorTeam。它们共享许多方法,但是Team跟踪生成点(实现addSpawn()和getSpawns()方法),而EditorTeam根本不关心生成点。
我可以看到两种设计来实现这一点:
1. 我可以在Team中实现addSpawn()和getSpawns(),但不在EditorTeam中实现,在访问方法之前,将AbstractTeam转换为Team。 2. 我可以在AbstractTeam中实现addSpawn()和getSpawns(),使它们不起作用,然后在Team中重写它们。这将消除类型转换的需要,但会暗示EditorTeam在某种程度上关心生成点,因为它现在有了这两个(虚假)方法。
所以我的问题是哪种方法更好?
该代码使用C++编写,如果上面不清楚,我可以提供一些示例。
5个回答

1

还有另一种选择,你可能会想到。如果你将团队视为与一堆能力相关联的团队,例如处理生成的能力。并且能力向团队注册。因此,您可以一直与团队合作,并仅在他们有可用能力时进行验证。

这种注册设计的外观(例如依赖注入或其他)是另一回事。

在游戏方面,团队、玩家或其他数据结构总是会随着时间的推移而发展,因此将事物打包成不太静态的类层次结构更加方便灵活。这将导致模型较少复杂,从而最终减少痛苦。

对于你的问题。如果我只限于这两个选项,我会更喜欢第一个选项,因为我不想拥有一个持有大量方法的类,而我实际上并不需要每个子类。而且幸运的是,您不必将子类强制转换为父类,以便将它们放入集合中。


我喜欢这个想法,但它不适用于这个特定的问题。然而,我可以想到一些其他地方可能会有用! - Watusimoto

1
使用编号1。这就是dynamic_cast的作用。您绝对不应在基类中定义成员,而所有派生类都不会实现该成员。

虽然我认为,如果程序的其余部分设计良好,几乎永远不应该使用这个。需要一个“团队”协作的功能,不应该有接受“抽象团队”的接口(尽管我承认这并非总是可能的,或者权衡变得太糟糕)。 - Björn Pollex
@Space_C0wb0y:哦,我同意。但那部分是楼主的问题-他陷入了#1 vs #2的困境。 - Puppy
我有另一个跟踪团队的对象,为了处理Team和EditorTeam,它分发抽象团队。由于这是我获取操作团队的主要方式,所以当我需要特定于团队的方法时,我经常进行强制转换。 - Watusimoto

0
在你的AbstractTeam类中有一个虚方法。
public interface ISpawn
{
  void Spawn1();
  void Spawn2();
}

public class Team : AbstractTeam, ISpawn
{
  // implement ISpawn and AbstracTeam in here...
}

public class EditorTeam : AbstractTeam
{
  // implement AbstracTeam in here...
}

// usage....
ISpawn team = getSpawn();
if(team != null)
{
   team.Spawn1();
   team.Spawn2();
}

这让我避免了强制转换,但是这是否意味着SpawnMethod1()是您可能想在EditorTeam上调用的内容? - Watusimoto
@Watusimoto 这个方式怎么样? - zbugs
1
我从未说过这是C++。语言只是一种工具。底层设计是相同的。这是用C#编写的,没有任何区别。 - zbugs
@zbugs:我认为你会发现 OP 标记了 C++,他明确表示他正在使用 C++,因此要求人们发布 C++ 代码并不过分。 - Puppy
@DeadMG同意。我会更加小心地发表我的观点。 - zbugs

0

还有第三个选项,您可能想考虑一下。如果跟踪生成点确实是与AbstractTeam的概念完全分离的东西,那么您可以将其从该类层次结构中完全剥离出来。

一种方法是让Team从接口类继承,如下所示:

class ISpawnTracker
{
public:
    Point SpawnPoint()=0;
};

class Team : public ISpawnTracker, public AbstractTeam
{
    Point SpawnPoint()
    {
        // ...
    }
};

通过进行 dynamic_cast<ISpawnTracker> 并测试 NULL,这使您可以有条件地调用它的方法。

另一种方法是将生成点跟踪拆分为自己的类,并在创建对象时注入一个;现在,Team 类使用 SpawnTracker 类来执行其生成点跟踪,而 EditorTeam 完全不需要知道它:

class ISpawnTracker
{
public:
    void TrackSpawnPoints()=0;
};

class ConcreteSpawnTracker : public ISpawnTracker { /* ... */ };
class TestingSpawnTracker : public ISpawnTracker { /* ... */ };



class Team : AbstractTeam
{
public:
    Team(ISpawnTracker *spawnTracker)
        : mSpawnTracker(spawnTracker)
    { /* ... */ }

private:
    ISpawnTracker mSpawnTracker;
};

这种最后的方法有一个附带好处,那就是使得TeamConcreteSpawnTracker更容易进行单元测试。


你的第一个方法仍然需要转换类型,并且在我的情况下,与我的第一个提议相比没有额外的好处。当我向我的主游戏对象请求团队列表时,它返回一个AbstractTeams列表,但是根据上下文(如果我在编辑器中或者在游戏中),我知道它们将是EditorTeams或Teams。因此,我不需要测试它们--我可以盲目地进行转换并确信我会得到什么。我对你的第二个提议的理解稍微少一些,但我认为我仍然需要转换类型,虽然它可能在更复杂的情况下很有用,但对于我的目的来说似乎有点过度设计。但还是谢谢你的想法! - Watusimoto

0

你应该问自己的问题是:除了Team类之外,我是否需要在其他地方使用add/getSpawn功能?如果不需要,那么就把它留在那里。

如果需要,在一些AbstractSpawnListenerTeam类中移动该功能(理想情况下,它不应实现AbstractTeam),并使团队从中继承(以及实现AbstractTeam)。

如果您想要重用生成功能,则组合比继承更好。仅将继承保留为实际接口实现的包装部分。

如果您想要使用继承来完成此操作,请记住,与其他语言一样,在C++中多重继承是安全的,只要您从至多一个实现(AbstractSpawnListenerTeam)和一个或多个接口继承即可。

如果您绝对需要一个继承路径,请让AbstractSpawnListenerTeam实现AbstractTeam,但将所有虚方法= 0


我理解你的观点,但希望你在术语上更加小心,因为这个问题被标记为C++。在C++中没有接口,也不能实现接口,只能从抽象基类继承。 - Björn Pollex
是的,但有一点是关于如何实现某个设计元素的技术术语,另一个则是设计元素本身。正如您正确地断言的那样,在C ++中创建接口的方法是编写一个类,其中所有方法都是虚拟的并声明为纯虚函数,等等。 - lurscher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接