建模真实世界还是适当的面向对象编程世界?还是两者都要?

8
我正在编写一款游戏,其中鼠标驱动的控制器对象会点击玩家对象使其执行某些操作。
有两种启动鼠标和玩家之间交互的方式:
1. 控制器调用玩家的函数: 控制器监听鼠标事件。当屏幕上发生鼠标单击时,控制器搜索所有在单击点下的对象。如果其中之一是玩家对象并且它的“可单击性”属性为真,则调用相应的函数。
2. 玩家调用控制器的函数: 玩家监听鼠标事件。当鼠标单击发生在玩家身上且玩家自己的“可单击性”属性为真时,则调用控制器的相应函数。
我的困惑在于,第一种选项似乎更符合我对现实情况发生方式的想象,但第二种选项在正确的面向对象设计方面更加直观,因为它不需要查看另一个对象的属性,这在一定程度上违反了封装(控制器必须查看玩家以读取其“可单击性”属性)。此外,第二种选项似乎与“控制器”设计模式一致。
这对我来说总是很棘手——我违抗正确的面向对象设计(如选项1),还是使用看起来与现实世界相悖的实现(如选项2)?
我希望有某种折中方案我还没有想到。

1
为什么选项1会违反正确的面向对象设计?一个对象肯定可以读取其他对象的公共属性。这就是它们为什么是公共的原因! - Vilx-
另一个问题 - 如果玩家对象直接监听鼠标事件,那么控制器对象的意义是什么? - Vilx-
1
选项1违反了面向对象编程的原则,因为当一个对象查看另一个对象的内部时(例如通过“getter”函数),这通常是责任分配出现问题的明显信号。良好的面向对象编程原则是“不问为什么-告诉我”,因为无论哪个对象对于任务拥有信息,都应该利用该信息,因为它们是信息的“专家”,正如Craig Larman的“应用UML和模式”所述。 - Pup
@Vilx:控制器对象提供了如何操作玩家的行为。这是“控制器”模式的一个例子,其中控制器可以随意交换。例如,如果我的玩家在某个时候不想被控制,它可以选择调用控制器进行操作。 - Pup
@Pup,如果要采取这种绝对主义的观点,那么公共属性的目的是什么呢? - Nona Urbiz
11个回答

8
这对我来说一直是个问题——我是违反正确的面向对象设计(例如选项1),还是使用一个看起来与现实世界相悖的实现(例如选项2)?
我认为,面向对象的目的并不是模拟现实世界。
我认为,面向对象模型通常遵循现实世界的模型的原因之一是现实世界变化不大:因此选择现实世界作为模型意味着软件不会改变太多,即维护成本较低。
忠实于现实世界并不是设计目标本身:相反,您应该尝试最大化其他指标的设计,例如简单性。
“面向对象”中的“对象”是软件对象,而不一定是现实世界中的对象。

1
这对我很有帮助!我忘记了计算机交流和语言交流之间的基本二分法--计算互动非常不人性化;当你与对象互动时,你要告诉它如何处理自己。而在英语语法中,主语承担对其直接宾语的行动责任;你不会说直接宾语(例如球)正在执行这个动作。相同的哲学也适用于这里--不要指望行动的发起者来执行它,相反,将由对象自行行动。仍未决定。 - Pup
+1 面向对象编程(OOP)从未被设计用于模拟现实世界,而是为了更多地重用代码(但这并没有取得很好的效果)。当你设计某个东西时,不应该试图模拟现实世界(否则,大多数面向对象编程模式将毫无意义)。只要它能够正常工作并且合乎逻辑,就不必费心使其符合特定的设计教条。 - Steven Evers
1
@SnOrfus 请核实你的事实。面向对象编程语言的代码重用效果非常好。几乎所有GUI工具包都是由面向对象语言驱动的。如果没有重用,这些工具包就没有多大用处。面向对象语言是第一个提供内置可重用代码库的语言。Smalltalk、Java、Ruby等都带有现成的代码,无需自己编写。代码重用是面向对象语言取得的一个很好的成果。 - chubbsondubs

5

为什么不选择选项3,它与选项1类似?

  • 控制器调用播放器函数:
    控制器监听鼠标事件。当鼠标在屏幕上任何位置点击时,控制器搜索被点击点下面的所有对象。如果其中一个对象实现了IClickable或继承了Clickable(或其他),则调用其适当的函数(或触发事件,取决于情况)。

实际上,我正在使用这个选项。我在示例中排除了接口,因为它们似乎只会掩盖我的困境的核心。 - Pup
1
@Pup,这两种选择有很大的区别,我宁愿选择选项3而不是选项1。 - strager
3
这并不掩盖你的斗争。实现 iClickable(或等价物)使得可点击性成为对象接口的一部分,因此查询对象的可点击性不会违反封装性。斗争结束。此外,我理解“告诉而不是询问”原则是指应该告诉玩家已经被点击,而不是询问是否已经被点击。 - Waquo
另外,不要忘记面向对象设计中最重要的原则之一是多态性。这无疑是从理论上和实用角度来看100%正确的选择。 - AviD
Slashmais - 虽然你的回答有一部分是正确的,但缺少的是控制器通常不知道采取什么行动!因此,玩家对象才是需要做出决定的对象。例如,一种类型的对象可能会跳跃,另一种类型的对象将被选择并闪烁,另一种类型的对象可能只是爆炸。控制器不知道,也确实不能知道应该采取哪种行动。此外,控制器 不应 知道。这就是控制权委派的全部意义。 - AviD
另一方面,更好的做法可能是将玩家类与处理UI分离,但不要将其放入控制器中 - 或许可以将每个类拆分为两个(根据需要):一个用于处理UI(类的可点击部分),这个类将知道如何解释这些事件以及在第二个应用逻辑部分的类上调用哪些方法。 - AviD

4
MVC概念通常是一种良好的实践,在游戏设计中仍应作为一个通用原则使用。但由于游戏UI的交互性质,您还应该考虑基于事件的架构。如果经过精心设计,MVC不会与基于事件的架构相矛盾。(以PureMVC为例。)
我建议您在所有显示对象上使用可观察模式,以便它们都可以监听/触发事件。这将在以后为您节省大量麻烦。当您的代码库变得更复杂时,您最终必须使用更多的解耦技术,例如选项2所描述的技术。此外,调停者模式 也会有所帮助。
编辑: 调停者模式 通常是组织应用程序级别事件的好方法。
这是一篇关于在游戏编程中使用MVC、事件和调停者的博客:

http://ezide.com/games/writing-games.html


你能详细说明一下吗?或者提供一个简短的例子?我知道事件驱动架构,你会发现这两种情况都使用了事件和监听器(从而实现了“观察者”)。所以,我认为我正在使用它。 - Pup
修改了我的帖子以提供更多信息,希望这有所帮助。另外,我认为你正在使用观察者模式。 - Aaron Qian

4
第二种方法是更具代表性的Flash方式。AS3将事件模型构建到了EventDispatcher中,所有的DisplayObjects都继承自它。这意味着任何Bitmap、Sprite或MovieClip都会立即知道自己是否被点击了。
把Flash Player当作你的控制器。当我在Flash中使用MVC时,我几乎从不写控制器,因为Flash Player已经为你完成了这个任务。如果Flash Player已经知道了,那么你再去浪费时间去确定点击的是什么就显得有些多余了。
var s:Sprite = new Sprite();
s.addEventListener(MouseEvent.CLICK, handleMouseClick);
function handleMouseClick(event:MouseEvent):void
{
    // do what you want when s is clicked
}

我建议不要直接从精灵(sprite)中访问控制器(controller)(应该在视图中实现)。相反,我会调度一个事件(可能是与情况相匹配的特定自定义事件)。根据每帧发生的次数做出决策。响应用户交互(例如鼠标点击)通常使您无需担心事件系统的开销。
最后 - 我建议这样做的原因与OOD或OOP的象牙塔概念无关。此类原则存在是为了帮助您而不是束缚您。当涉及实际问题时,请选择不会在以后给您带来麻烦的最简单解决方案。有时意味着使用面向对象编程(OOP),有时意味着函数式编程,有时意味着命令式编程。

我可能不会直接从精灵内部访问控制器(可能在视图中)。相反,我会派发一个事件(可能是与情况匹配的特定自定义事件)。所以,你的意思是你会从玩家对象派发一个自定义事件,控制器会监听显示列表吗? - Pup
这真的取决于你的对象层次结构。我倾向于不要嵌套太深(这本来就是个好主意),这样我可以轻松地手动将事件冒泡到可行的级别。我还创建了“视图”类,它们在逻辑上聚合多个显示对象。因此,通常一个视图会监听它的DisplayObjects,并代表它们与Model进行交互。 - James Fassett

2
根据《应用UML和模式》(Craig Larman)所述,用户界面(鼠标事件)不应直接与应用程序类交互,也就是说,用户界面不应直接驱动业务逻辑。相反,应该定义一个或多个控制器作为用户界面的中间层,因此选项1实际上遵循了良好的面向对象方法。
如果你仔细考虑,尽可能减少与UI耦合的类对于使业务逻辑尽可能独立于UI是有意义的。

2
这对我来说总是个挑战——我是违反正确的面向对象设计(例如选项1),还是使用看似违反现实世界的实现(例如选项2)呢?
现实可能是塑造或演化设计的好起点,但将OO设计建模到现实中总是一个错误。
OO设计关乎接口、实现它们的对象以及它们之间的互动(它们之间传递的消息)。接口是两个组件、模块或软件子系统之间的契约协议。OO设计有许多特性,但对我来说最重要的特性是可替换性。如果我有一个接口,那么实现代码最好遵守它。但更重要的是,如果实现被替换,那么新实现也最好遵守它。最后,如果实现是多态的,则多态实现的各种策略和状态也最好遵守它。
例1
在数学中,一个正方形一个矩形。听起来从类矩形继承类正方形是个好主意。你这样做了,但结果却很糟糕。为什么?因为客户的期望或信念被违背了。宽度和高度可以独立变化,但是正方形违反了这个约定。我有一个长宽为(10,10)的矩形,我将宽度设置为20。现在我认为我有一个长宽为(20,10)的矩形,但实际上实例是一个长宽为(20,20)的正方形实例,我作为客户将会遇到一些真正的大惊喜。所以现在我们违反了最小惊讶原则。

现在你有了有缺陷的行为,这导致客户端代码变得复杂,需要使用if语句来解决有缺陷的行为。你可能还会发现你的客户端代码需要RTTI来解决有缺陷的行为,通过测试具体类型来检查是否为正方形实例(我有一个对矩形的引用,但我必须检查它是否真的是一个正方形实例)。

示例2

在现实生活中,动物可以是肉食动物或草食动物。在现实生活中,肉类和蔬菜是食品类型。因此,您可能认为将类Animal作为不同动物类型的父类是一个好主意。您还认为将FoodType父类分别用于Meat类和Vegetable类是一个好主意。最后,您的类Animal支持一个名为eat()的方法,该方法接受一个FoodType作为形式参数。
一切都编译通过、静态分析通过并链接。您运行程序。当动物的子类型(比如草食动物)收到一个属于Meat类的FoodType实例时,会发生什么?欢迎来到协变和逆变的世界。这是许多编程语言面临的问题。对于语言设计者来说,这也是一个有趣而具有挑战性的问题。
总之...
那么你该怎么办呢?从你的问题域、用户故事、用例和需求开始。让它们驱动设计。让它们帮助你发现需要建模为类和接口的实体。当您这样做时,您会发现最终结果并不基于现实。
查看Martin Fowler的《分析模式》(Analysis Patterns),在其中你会看到是什么驱动了他的面向对象设计。这主要基于他的客户(医疗人员、金融人员等)如何执行其日常任务。它与现实存在重叠,但并不是基于或由现实驱动的。

1

这通常是个人偏好的问题。逻辑游戏设计往往与良好的面向对象编程设计相冲突。我倾向于在游戏世界范围内有意义的设计,但对于这个问题并没有绝对正确的答案,每个问题都应该根据其自身的价值来解决。

这有点类似于争论驼峰式命名法的利弊。


1

我认为,将输入逻辑与应用程序逻辑分离很重要...一种方法是将输入事件(无论是用户输入还是通过套接字/本地连接等到达的数据)转换为应用程序事件(在抽象意义上的事件)...这种转换由我称之为“前端控制器”来完成,因为没有更好的术语...

所有这些前端控制器只是简单地转换事件,因此完全独立于应用程序逻辑如何响应特定事件...另一方面,应用程序逻辑与这些前端控制器解耦...“协议”是一组预定义的事件...当涉及到通知机制时,你可以选择使用AS3事件分派从前端控制器获取事件到应用程序控制器,或者构建它们针对某些指定的接口,它们将调用该接口...

人们倾向于将应用程序逻辑编写到按钮的单击处理程序中...有时甚至手动触发这些处理程序,因为他们不想整理东西...这并不是我没见过的事情...

是的,这绝对是第一版......在这种情况下,仅处理鼠标输入的前端控制器只需知道显示列表,并具有有关何时分派哪个事件的逻辑......并且应用程序控制器需要能够处理某种PlayerEvent.SELECT事件......(如果以后您决定使用一些教程模式或其他内容,您可以简单地移动虚拟鼠标并在伪点击的情况下触发此事件,或者您可以在某种重播中重复所有内容,或者您可以将其用于记录宏,当不涉及游戏时...只是为了指出一些分离有所帮助的情况)

希望这可以帮到你...;)


1

我不同意你对这两个选项的评估。选项2是更糟糕的面向对象编程,因为它将Player对象与特定的用户界面实现紧密耦合在一起。当您想在屏幕外或使用无鼠标界面的地方重用Player类时会发生什么?

虽然选项1仍有改进的空间。早期建议使用iClickable接口或Clickable超类是一个巨大的改进,因为它允许您实现多种类型的可点击对象(不仅仅是Player),而无需给控制器提供一个巨大的列表,“这个对象是这个类吗?它是那个类吗?”。

你对选项1的主要反对意见似乎是它检查了Player的“clickable”属性,你觉得这违反了封装性。但事实并非如此。它检查的是作为Player公共接口的一部分定义的属性。这与从公共接口调用方法没有区别。

是的,我明白,“clickable”属性目前实现方式仅是一个简单的getter,它不会执行任何操作,只是查询播放器的内部状态。但事实上这并非必须如此。该属性可以在明天重新定义,以完全不同的方式确定其返回值,而与内部状态无关。只要仍然返回布尔值(即公共接口保持不变),使用Player.clickable的代码仍将正常工作。这就是属性和直接检查内部状态之间的区别,这是个很大的区别。

如果您仍然感到不安,那么消除控制器检查Player.clickable是很容易的:只需将点击事件发送到鼠标下实现iClickable /从Clickable派生的每个对象即可。如果该对象在接收到点击时处于不可单击状态,则可以忽略它。


1
在任何面向对象编程语言中,当存在模拟真实生活的方法时,遵循惯用方法几乎总是正确的做法。因此,回答你的问题,第二种方法几乎肯定更好,或者随着你深入设计或后来感到需要改变或添加它,可能会证明更好。
这并不应该阻止你寻找其他解决方案。但是尽量始终遵循语言习惯用法。面向对象编程不能以1对1的关系转化为现实生活,也不擅长模拟它。一个说明性的例子是经典的矩形和正方形对象关系,你可能已经非常了解了。在现实生活中,正方形是一个矩形。在面向对象编程(或至少在正确的面向对象编程)中,这种关系无法很好地转化为基础派生关系。因此,你感到需要摆脱真实生活的模拟,因为语言习惯用法更高。要么就是这样,要么当你开始认真实现矩形和正方形或后来希望对它们进行更改时,将会面临痛苦的世界。

矩形/正方形的关系对我来说似乎没问题... 你能解释一下或者引用另一个类比吗? - Pup
在大多数情况下,如果您认为正方形是矩形的一种类型,就会遇到问题。也就是说,如果您从矩形派生正方形,则将在is-a规则中映射分类关系。但是:https://dev59.com/YHNA5IYBdhLWcg3wQ7i4 - Alexandre Bell

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