当然这要视情况而定。但是,当较低级别的对象或系统与较高级别的系统通信时,是否应首选回调或事件而不是将指针指向较高级别的对象?
例如,我们有一个world
具有成员变量的类vector<monster> monsters
。当monster
类与进行通信时world
,我应该更喜欢使用回调函数,还是应该在world
类内部具有指向该类的指针monster
?
当然这要视情况而定。但是,当较低级别的对象或系统与较高级别的系统通信时,是否应首选回调或事件而不是将指针指向较高级别的对象?
例如,我们有一个world
具有成员变量的类vector<monster> monsters
。当monster
类与进行通信时world
,我应该更喜欢使用回调函数,还是应该在world
类内部具有指向该类的指针monster
?
Answers:
一个班级可以在不紧密联系的情况下与另一班级进行对话的三种主要方式:
这三个是密切相关的。在许多方面,事件系统只是回调列表。回调或多或少是具有单个方法的接口。
在C ++中,我很少使用回调:
C ++对保留其this
指针的回调没有很好的支持,因此很难在面向对象的代码中使用回调。
回调基本上是一种不可扩展的单方法接口。随着时间的流逝,我发现我几乎总是总是需要多个方法来定义该接口,而单个回调几乎是不够的。
在这种情况下,我可能会做一个界面。在您的问题中,您实际上并没有明确说明monster
与之进行交流的实际需求world
。猜测一下,我会做类似的事情:
class IWorld {
public:
virtual Monster* getNearbyMonster(const Position & position) = 0;
virtual Item* getItemAt(const Position & position) = 0;
};
class Monster {
public:
void update(IWorld * world) {
// Do stuff...
}
}
class World : public IWorld {
public:
virtual Monster* getNearbyMonster(const Position & position) {
// ...
}
virtual Item* getItemAt(const Position & position) {
// ...
}
// Lots of other stuff that Monster should not have access to...
}
这里的想法是,您只需输入IWorld
(这是一个cr脚的名字)Monster
需要访问的最低限度的最低要求。它对世界的看法应尽可能狭窄。
不要使用回调函数来隐藏正在调用的函数。如果要放置一个回调函数,并且该回调函数将分配一个并且只有一个函数,那么您根本就不会破坏这种耦合。您只是用另一层抽象掩盖了它。您没有获得任何收益(也许不是编译时间),但是却失去了清晰度。
我不会确切地将其称为最佳实践,但是这是一种常见的模式,即某些对象包含的实体具有指向其父对象的指针。
话虽这么说,也许值得您花点时间使用界面模式为怪物提供他们可以在世界上调用的功能的有限子集。
通常,我会尽量避免双向链接,但是如果必须要使用双向链接,则我绝对要确保有一种方法可以使它们失效,而要使它们断开,这样就永远不会出现不一致的情况。
通常,您可以通过在必要时传递数据来完全避免双向链接。一个简单的重构就是使它成为怪物而不是保持与世界的链接,而是通过引用需要它的怪物方法来传递世界。更好的办法是只为怪物严格需要的世界传递接口,这意味着怪物不再依赖于世界的具体实现。这对应于接口隔离原理和依赖反转原理,但并未开始引入有时可以通过事件,信号+插槽等获得的多余抽象。
在某种程度上,您可以辩称使用回调是一种非常专业的微型接口,这很好。您必须决定是通过接口对象中的一组方法还是不同回调中的几种方法来更有意义地实现目标。
显然,您不会没有事件就走得很远,但是在甚至开始编写(设计)事件系统之前,您应该问一个真正的问题:为什么怪物会与世界一流的人交流?应该真的吗?
让我们以一个“经典”情况为例,一个怪物攻击玩家。
怪物在攻击:世界可以很好地识别英雄在怪物旁边的情况,并告诉怪物进行攻击。因此,Monster中的函数为:
void Monster::attack(LivingCreature l)
{
// Call to combat system
}
但是怪物并不需要了解这个世界(已经知道这个怪物)。实际上,怪物可以忽略类世界的存在,这可能更好。
当怪物移动时,这也是一样(我让子系统获取生物并为其处理移动计算/意图,怪物只是一袋数据,但是很多人会说这不是真正的OOP)。
我的观点是:事件(或回调)固然很好,但是它们并不是您将要面对的每个问题的唯一答案。
只要有可能,我都会尝试将对象之间的通信限制为请求和响应模型。在我的程序中的对象上有一个隐含的部分排序,因此在任意两个对象A和B之间,可能有一种方法A可以直接或间接调用B的方法,或者B可以直接或间接调用A的方法。 ,但A和B永远不可能互相调用彼此的方法。当然,有时您希望与方法的调用者进行向后通信。我有几种方法可以做到这一点,但它们都不是回调。
一种方法是在方法调用的返回值中包含更多信息,这意味着在过程将控制权返回给它之后,客户端代码可以决定如何处理它。
另一种方法是调出共同的子对象。也就是说,如果A在B上调用方法,并且B需要向A传递一些信息,则B在C上调用方法,其中A和B都可以调用C,但是C无法调用A或B。负责在B将控制权交还给A之后从C获取信息。请注意,这与我提出的第一种方式并没有本质上的区别。对象A仍然只能从返回值中检索信息。B或C都不会调用对象A的方法。此技巧的一种变体是将C作为方法的参数传递,但对C与A和B的关系的限制仍然适用。
现在,重要的问题是为什么我坚持用这种方式做事。主要有以下三个原因:
self
一个方法执行时唯一可以改变其内部状态的代码是一个好方法,而没有其他方法。这与可能导致将互斥对象放在并发对象上的推理相同。我不反对使用回调。为了与我永不“调用调用方”的政策保持一致,如果对象A在B上调用方法并向其传递回调,则该回调可能不会更改A的内部状态,并且该内部状态包括由A和A上下文中的对象。换句话说,回调只能在B赋予的对象上调用方法。实际上,回调受到与B相同的限制。
最后一个松散的结局是,无论我一直在谈论这种局部排序,我都将允许调用任何纯函数。纯函数与方法有些不同,因为它们不能更改或依赖于可变状态或副作用,因此不必担心它们会引起混乱。
亲身?我只使用一个单例。
是的,好的,不良的设计,不是面向对象的,等等。你知道吗?我不在乎。我在写游戏,而不是技术展示。没人会在代码上给我评分。目的是制作一款有趣的游戏,而妨碍我前进的一切都将导致这款游戏变得不那么有趣。
您会同时运行两个世界吗?也许!也许你会的。但是,除非您现在能想到这种情况,否则您可能不会。
因此,我的解决方案是:创建一个World单例。调用函数就可以了。完成整个混乱。您可以为每个函数传递一个额外的参数-毫无疑问,这就是原因。或者,您可以编写有效的代码。
以这种方式执行此操作需要一些纪律,以便在杂乱无章时(即“何时”,而不是“ if”)进行清理,但是您无法防止代码杂乱无章-您遇到的是意大利面条问题,还是成千上万种的抽象层问题。至少这样,您不会编写大量不必要的代码。
而且,如果您决定不再需要单例,则摆脱它通常很简单。进行一些工作,需要传递十亿个参数,但是无论如何这些都是您需要传递的参数。