访问父类对象集合成员的子类函数

时间:2014-04-23 17:17:35

标签: c++ oop design-patterns inheritance

(请参阅更新#1以获得问题的简明版本。)

我们有一个名为Games的(抽象)类,它有子类,比如BasketBallHockey(以后可能会有更多)。

另一个类GameSchedule必须包含各种GamesCollection个对象的集合Games。问题是我们有时只想通过BasketBall的{​​{1}}对象进行迭代,并调用特定于它的函数(并且在GamesCollection类中没有提到)。

也就是说,Games处理大量属于GameSchedule类的对象,因为它们具有正在被访问的常用函数;同时,还有更多的粒度来处理它们。

我们希望提出一种避免不安全向下转换的设计,并且在Games或其任何现有子类下创建许多子类的意义上是可扩展的,这些子类必须不需要添加太多代码来处理这个要求。

示例:

  • 我提出的一个笨拙的解决方案,根本不做任何向下转换,就是在每个子类的Games类中都有虚函数必须从Game调用的特定函数。这些虚函数将在适当的子类中具有覆盖实现,实际上需要实现它。

  • 我们可以为GameSchedule的各个子类而不是单个容器显式维护不同的容器。但是当子类数量增加时,这需要Games中的大量额外代码。特别是如果我们需要遍历所有GameSchedule个对象。

有这样一种巧妙的方法吗?

注意:代码是用C ++编写的

更新#1:我意识到可以用更简单的方式提出问题。是否可以为属于类层次结构的任何对象创建容器类?此外,此容器类必须能够从层次结构中选择属于(或派生自)特定类的元素并返回适当的列表。

在上述问题的上下文中,容器类必须具有GamesGetCricketGamesGetTestCricketGames等函数,

5 个答案:

答案 0 :(得分:5)

这正是为"Tell, Don't Ask"原则创建的问题之一。

您正在描述一个对象,该对象保留对其他对象的引用,并希望在告诉他们需要做什么之前询问它们是什么类型的对象。从上面链接的文章:

  

问题在于,作为调用者,您不应该根据被调用对象的状态做出决定,这会导致您更改对象的状态。您实现的逻辑可能是被调用对象的责任,而不是您的责任。对于你在对象之外做出决定违反了它的封装。

如果违反encapsulation的规则,您不仅会引入猖獗的向下转发所带来的运行时风险,而且还会使组件更容易紧密耦合,从而使系统的可维护性大大降低。


现在就在那里,让我们来看看“告诉,不要问”如何应用于你的设计问题。

让我们通过你陈述的约束(没有特别的顺序):

  1. GameSchedule需要遍历所有游戏,执行常规操作
  2. GameSchedule需要迭代所有游戏的子集(例如Basketball),以执行特定于类型的操作
  3. 没有垂头丧气
  4. 必须轻松容纳新的Game子类
  5. 遵循“告诉,不要问”原则的第一步是确定将在系统中发生的操作。这让我们退后一步,评估系统应该做的 ,而不会陷入如何这样做的细节。

    你在@MarkB的答案中做了以下评论:

      

    如果有TestCricket类继承自Cricket,并且它有许多与比赛各局的时间有关的特定属性,我们想初始化所有{{1}的值对象的计时属性为某个预设值,我需要一个循环来选择所有TestCricket个对象并调用一些函数,如TestCricket

    在这种情况下,操作为:“将所有setInningTimings(int inning_index, Time_Object t)个游戏的内部时间初始化为预设值。”

    这是有问题的,因为想要执行此初始化的代码无法区分TestCricket游戏和其他游戏(例如TestCricket)。但也许它不需要......

    大多数游戏都有一些时间因素:篮球比赛有时间限制,而棒球比赛基本上没有时间(基本上)。每种类型的游戏都可以拥有自己完全独特的配置。这不是我们想要卸载到a single class上的内容。

    而不是向每个游戏询问它是什么类型的Basketball,然后告诉它如何进行初始化,考虑如果Game简单告诉每个GameSchedule,事情将如何运作{1}}要初始化的对象。这将初始化的责任委托给Game的子类 - 具有字面的类,它最了解它是什么类型的游戏。

    一开始会觉得很奇怪,因为Game对象放弃了对另一个对象的控制。这是Hollywood Principle的一个例子。与大多数开发人员最初学习的方法相比,这是解决问题的完全不同的方式。

    这种方法通过以下方式处理约束:

    1. GameSchedule可以毫无问题地遍历GameSchedule列表
    2. Game不再需要知道其GameSchedule s
    3. 的子类型
    4. 不需要向下转换,因为子类本身正在处理特定于子类的逻辑
    5. 当添加新的子类时,不需要在任何地方更改逻辑 - 子类本身实现必要的细节(例如,Game方法)。

    6. 编辑:以下是一个示例,作为概念验证。

      InitializeTiming()

答案 1 :(得分:1)

您已经确定了我想到的前两个选项:让基类具有相关方法,或者为每种游戏类型维护单独的容器。

您认为这些不合适的事实让我相信您在Game基类中提供的“抽象”界面可能过于具体。我怀疑你需要做的是退后一步,查看基本界面。

你没有给出任何具体的例子来帮助,所以我打算做一个。假设你的篮球课有NextQuarter方法,曲棍球有NextPeriod。相反,向基类添加NextGameSegment方法,或者抽象出游戏特定细节的东西。 所有特定于游戏的实现细节应隐藏在子类中,并且只需要计划类所需的游戏通用接口。

答案 2 :(得分:0)

C#支持反射,通过使用“is”关键字或GetType()成员函数,您可以轻松完成这些操作。如果您使用非托管C ++编写代码,我认为最好的方法是在基类中添加一个GetType()方法(Games?)。反过来又返回一个枚举,包含从中派生的所有类(因此你也必须创建一个枚举)。这样您就可以通过基本类型安全地确定您要处理的类型。以下是一个例子:

enum class GameTypes { Game, Basketball, Football, Hockey };

class Game
{
public:
    virtual GameTypes GetType() { return GameTypes::Game; }
}

class BasketBall : public Game
{
public:
    GameTypes GetType() { return GameTypes::Basketball; }
}

你为剩下的比赛(例如足球,曲棍球)做到这一点。然后你只保留一个Game对象的容器。当您获得Game对象时,您可以调用其GetType()方法并有效地确定其类型。

答案 3 :(得分:0)

你想要拥有一切,你不能那样做。 :)要么你需要做一个向下转换,要么你需要利用访问者模式之类的东西,然后每次你创建一个新的Game实现时都需要你做工作。或者你可以从根本上重新设计东西,以消除从一系列游戏中挑选个人篮球的需要。

FWIW:向下转换可能很难看,但只要您使用指针并检查null,它就不会安全:

for(Game* game : allGames)
{
    Basketball* bball = dynamic_cast<Basketball*>(game);
    if(bball != nullptr)
        bball->SetupCourt();
}

答案 4 :(得分:0)

我在这里使用策略模式。

每种游戏类型都有自己的调度策略,该策略源自游戏计划类使用的通用策略,并将特定游戏和游戏计划之间的依赖性分离。