(请参阅更新#1以获得问题的简明版本。)
我们有一个名为Games
的(抽象)类,它有子类,比如BasketBall
和Hockey
(以后可能会有更多)。
另一个类GameSchedule
必须包含各种GamesCollection
个对象的集合Games
。问题是我们有时只想通过BasketBall
的{{1}}对象进行迭代,并调用特定于它的函数(并且在GamesCollection
类中没有提到)。
也就是说,Games
处理大量属于GameSchedule
类的对象,因为它们做具有正在被访问的常用函数;同时,还有更多的粒度来处理它们。
我们希望提出一种避免不安全向下转换的设计,并且在Games
或其任何现有子类下创建许多子类的意义上是可扩展的,这些子类必须不需要添加太多代码来处理这个要求。
示例:
我提出的一个笨拙的解决方案,根本不做任何向下转换,就是在每个子类的Games
类中都有虚函数必须从Game
调用的特定函数。这些虚函数将在适当的子类中具有覆盖实现,实际上需要实现它。
我们可以为GameSchedule
的各个子类而不是单个容器显式维护不同的容器。但是当子类数量增加时,这需要Games
中的大量额外代码。特别是如果我们需要遍历所有GameSchedule
个对象。
有这样一种巧妙的方法吗?
注意:代码是用C ++编写的
更新#1:我意识到可以用更简单的方式提出问题。是否可以为属于类层次结构的任何对象创建容器类?此外,此容器类必须能够从层次结构中选择属于(或派生自)特定类的元素并返回适当的列表。
在上述问题的上下文中,容器类必须具有Games
,GetCricketGames
,GetTestCricketGames
等函数,
答案 0 :(得分:5)
这正是为"Tell, Don't Ask"原则创建的问题之一。
您正在描述一个对象,该对象保留对其他对象的引用,并希望在告诉他们需要做什么之前询问它们是什么类型的对象。从上面链接的文章:
问题在于,作为调用者,您不应该根据被调用对象的状态做出决定,这会导致您更改对象的状态。您实现的逻辑可能是被调用对象的责任,而不是您的责任。对于你在对象之外做出决定违反了它的封装。
如果违反encapsulation的规则,您不仅会引入猖獗的向下转发所带来的运行时风险,而且还会使组件更容易紧密耦合,从而使系统的可维护性大大降低。
现在就在那里,让我们来看看“告诉,不要问”如何应用于你的设计问题。
让我们通过你陈述的约束(没有特别的顺序):
GameSchedule
需要遍历所有游戏,执行常规操作GameSchedule
需要迭代所有游戏的子集(例如Basketball
),以执行特定于类型的操作Game
子类遵循“告诉,不要问”原则的第一步是确定将在系统中发生的操作。这让我们退后一步,评估系统应该做的 ,而不会陷入如何这样做的细节。
你在@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的一个例子。与大多数开发人员最初学习的方法相比,这是解决问题的完全不同的方式。
这种方法通过以下方式处理约束:
GameSchedule
可以毫无问题地遍历GameSchedule
列表Game
不再需要知道其GameSchedule
s Game
方法)。编辑:以下是一个示例,作为概念验证。
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)
我在这里使用策略模式。
每种游戏类型都有自己的调度策略,该策略源自游戏计划类使用的通用策略,并将特定游戏和游戏计划之间的依赖性分离。