规避巫师和战士中的规则


9

这一系列博客文章中,Eric Lippert使用向导和战士作为示例描述了面向对象设计中的问题,其中:

abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }

abstract class Player 
{ 
  public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }

然后添加一些规则:

  • 战士只能使用剑。
  • 向导只能使用人员。

然后,他继续演示如果您尝试使用C#类型系统强制执行这些规则(例如,让Wizard类负责确保向导只能使用人员)会遇到的问题。您违反了Liskov替代原则,冒着运行时异常的危险,或者最终遇到了难以扩展的代码。

他提出的解决方案是Player类不进行任何验证。它仅用于跟踪状态。然后,不要通过以下方式为玩家提供武器:

player.Weapon = new Sword();

状态由Commands并根据Rules 修改:

...我们制作了一个Command名为的Wield对象,它带有两个游戏状态对象a Player和a Weapon。当用户向系统发出命令“此向导应使用那把剑”时,该命令将在一组Rules 的上下文中进行评估,这会产生一系列Effects。我们有一个Rule说,当玩家尝试挥舞一种武器时,其效果是丢弃了现有武器(如果有的话),而新武器成为了玩家的武器。我们还有另一条规则增强了第一条规则,即当向导尝试挥舞剑时,第一条规则的效果不适用。

我原则上喜欢这个想法,但对如何在实践中使用它有所关注。

似乎没有什么可以通过简单地将on 设置为来阻止开发人员规避Commandsand Rule的。该属性需要可通过命令访问,因此无法进行设置。WeaponPlayerWeaponWieldprivate set

那么,是什么防止开发人员这样做呢?他们只需要记住不要吗?


2
我不认为这个问题是特定于语言的(C#),因为它确实是有关OOP设计的问题。请考虑删除C#标签。
Maybe_Factor

1
@maybe_factor c#标签很好,因为发布的代码是c#。
CodingYoshi

为什么不直接问@EricLippert?他似乎不时出现在此站点上。
布朗

@Maybe_Factor-我确实对C#标记不满意,但是决定保留它,以防出现特定于语言的解决方案。
本L

1
@DocBrown-我确实在他的博客上发布了这个问题(诚然只有两天前;我没有等那么久才得到答复)。有没有办法让我的问题引起他的注意?
本L

Answers:


9

博客文章系列引发的整个争论在第五部分

我们没有理由相信C#类型系统的设计具有足够的通用性,可以对《龙与地下城》的规则进行编码,那么我们为什么还要尝试呢?

我们已经解决了“表示系统规则的代码在哪里?”的问题。它进入代表系统规则的对象,而不进入代表游戏状态的对象;状态对象的关注点在于保持其状态的一致性,而不是评估游戏规则。

武器,角色,怪物和其他游戏对象概不负责检查其可以做什么或不能做什么。规则系统对此负责。该Command对象也没有对游戏对象执行任何操作。它只是代表他们做某事的尝试。然后,规则系统检查命令是否可行,以及何时是规则系统通过在游戏对象上调用适当的方法来执行命令。

如果开发人员想要创建第二个规则系统,该第二个规则系统使用第一个规则系统不允许的角色和武器来做事,那么他们可以这样做,因为在C#中,您(没有讨厌的反射黑客)无法找出方法调用的来源从。

某些情况下可能可行的解决方法是将游戏对象(或它们的接口)与规则引擎一起放在一个程序集中,并将任何更改方法标记为。任何需要只读访问游戏对象的系统都将位于不同的程序集中,这意味着它们将只能访问方法。这仍然留下了游戏对象相互调用内部方法的漏洞。但是这样做会产生明显的代码味道,因为您同意游戏对象类应该是愚蠢的状态持有者。internalpublic


4

原始代码的明显问题是它正在执行数据建模而不是对象建模。请注意,在链接的文章中绝对没有提及实际的业务需求!

我将从尝试获得实际的功能要求开始。例如:“任何玩家都可以攻击其他任何玩家,...”。这里:

interface Player {
    void Attack(Player enemy);
}

“玩家可以使用攻击中使用的武器,奇才可以使用法杖,战士使用剑”:

public class Wizard: Player {
    ...
    public void Wield(Staff weapon) { ... }
    ...
}
public class Warrior: Player {
    ...
    public void Wield(Sword sword) { ... }
    ...
}

“每种武器都对被攻击的敌人造成伤害”。好的,现在我们必须有一个用于武器的通用接口:

interface Weapon {
    void dealDamageTo(Player enemy);
}

等等...为什么没有Wield()Player?因为没有要求任何玩家都可以使用任何武器。

我可以想象,有一个要求说:“任何人都Player可以尝试挥舞任何东西Weapon。” 但是,这将是完全不同的事情。我可能会这样建模:

interface Player {
    void Attack(Player enemy);
    void TryWielding(Weapon weapon); // Throws UnwieldableException
}

摘要:仅对需求进行建模。不要做数据建模,那不是oo建模。


1
你读过系列吗?也许您想告诉该系列的作者不是要对数据建模,而是要对需求建模。您的答案中的要求是您的补充要求,而不是构建C#编译器时作者的要求。
CodingYoshi

2
埃里克·利珀特(Eric Lippert)详细介绍了该系列中的技​​术问题,这很好。这个问题是关于实际问题的,而不是C#功能。我的观点是,在实际项目中,我们应该遵循业务需求(为此我提供了虚构的示例,是的),而不是假设关系和属性。这就是您获得适合的模型的方式。这是quesetion。
罗伯特·布劳蒂加姆(RobertBräutigam),

那是我读该系列书时想到的第一件事。作者只是想出了一些抽象,从不对其进行进一步的评估,只是坚持使用。试图机械地解决问题,一次又一次。显然,应该首先考虑而不是考虑有用的领域和抽象。我的投票
Vadim Samokhin

这是正确的答案。该文章表达了冲突的要求(一个要求说玩家可以使用[任何]武器,而其他要求则说并非如此。)然后详细说明系统正确表达冲突的难度。唯一正确的答案是消除冲突。在这种情况下,这意味着取消了玩家可以使用任何武器的要求。
Daniel T.

2

一种方法是将Wield命令传递给Player。然后,播放器执行Wield命令,该命令检查适当的规则并返回WeaponPlayer然后使用设置其自己的“武器”字段。这样,“武器”字段可以有一个私人设置器,并且只能通过将Wield命令传递给玩家来设置。


实际上,这不能解决问题。制作命令对象的开发人员可以传递任何武器,而玩家将对其进行设置。阅读该系列文章,因为问题比您想象的要难。实际上,他之所以撰写该系列文章,是因为在开发Roslyn C#编译器时遇到了这个设计问题。
CodingYoshi

2

没有什么可以阻止开发人员这样做的。实际上,埃里克·利珀特(Eric Lippert)尝试了许多不同的技术,但是它们都有缺点。那是该系列文章的全部重点,要阻止开发人员这样做并不容易,而且他尝试的一切都有缺点。最后,他决定将Command对象与规则结合使用是可行的方法。

使用规则,您可以将a的Weapon属性设置为a WizardSword但是当您要求Wizard挥舞武器(Sword)并进行攻击时,它将没有任何效果,因此不会更改任何状态。正如他在下面说的:

我们还有另一条规则增强了第一条规则,即当向导尝试挥舞剑时,第一条规则的效果不适用。这种情况的影响是“发出令人难忘的长号声音,用户此回合失去动作,没有游戏状态发生变化

换句话说,我们不能通过type关系来执行这样的规则,他尝试了许多不同的方式,但是要么不喜欢它,要么不起作用。因此,他说我们唯一能做的就是在运行时对其进行处理。抛出异常是不好的,因为他不认为这是异常。

他最终选择了上述解决方案。该解决方案基本上说您可以设置任何武器,但是当您放弃它时,如果不是正确的武器,则基本上是无用的。但是不会抛出异常。

我认为这是一个很好的解决方案。尽管在某些情况下,我也会使用try-set模式。


This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.我在那个系列中找不到它,您能指出这个解决方案的地方吗?
Vadim Samokhin

@zapadlo他间接地说。我已经在答案中复制了该部分并引用了它。又来了。他在报价中说:当巫师试图挥舞剑时。如果未设置剑,向导将如何挥剑?它必须已设置。然后,如果巫师挥舞着剑,那么这种情况的后果是“发出悲伤的长号声音,用户
此刻

嗯,我认为挥舞剑道基本上意味着应该将其设置,不是吗?当我阅读该段落时,我将其解释为第一条规则的效果是that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon。虽然第二条规则that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.使我认为有一条规则可以检查武器是否为剑,所以它不能被向导使用,因此它没有设置。相反,会发出悲伤的长号。
Vadim Samokhin

我认为违反某些命令规则会很奇怪。这就像对问题的模糊处理。为什么在已经违反规则并将向导设置为无效状态之后处理此问题?向导不能拥有剑,但是可以!为什么不让它发生呢?
Vadim Samokhin

我同意@Zapadlo的解释Wield。我认为该命令的名称有点误导。像ChangeWeapon这样的东西会更准确。我猜您可能有一个可以设置任何武器的模型,但是当您放弃它时,如果没有合适的武器,它将基本上是无用的。听起来很有趣,但是我不认为这是埃里克·利珀特(Eric Lippert)所描述的。
本L

2

作者的第一个废弃解决方案是用类型系统表示规则。类型系统在编译时进行评估。如果将规则从类型系统中分离出来,则编译器将不再检查它们,因此没有什么可以阻止开发人员本身犯错误。

但是,编译器没有检查的每一个逻辑/建模都面临着这个问题,对此的一般回答是(单元)测试。因此,作者提出的解决方案需要强大的测试工具来规避开发人员的错误。为了强调需要强大的测试工具来处理只能在运行时检测到的错误的观点,请参阅Bruce Eckel的这篇文章,文章提出了一个论点,即您需要将强类型转换为动态语言中的更强测试。

总之,可以防止开发人员犯错误的唯一事情是进行一组(单元)测试,检查所有规则是否得到遵守。


您将使用一个API,并且该API应该确保您将进行单元测试或做出该假设?整个挑战在于建模,因此即使使用该模型的开发人员不小心,该模型也不会破坏。
CodingYoshi

1
我要说明的一点是,没有什么可以阻止开发人员犯错误。在建议的解决方案中,规则是从数据中分离出来的,因此,如果您不创建自己的检查,那么没有什么会阻止您在不应用规则的情况下使用数据对象。
larsbe

1

我可能在这里错过了一些微妙之处,但是我不确定问题出在类型系统上。也许这是C#中的约定。

例如,您可以通过在中Weapon保护setter 来使此完全类型安全Player。然后加setSword(Sword)setStaff(Staff),以WarriorWizard分别是通话的保护制定者。

这样,Player/ Weapon关系就会被静态检查,而无关紧要的代码只能使用a Player来获取Weapon


埃里克·利珀特(Eric Lippert)不想抛出异常。你读过系列吗?解决方案必须满足要求,并且该要求在系列中有明确说明。
CodingYoshi

@CodingYoshi为什么会抛出异常?它是安全类型,即在编译时可检查。
Alex

抱歉,一旦我意识到您没有抛出任何遗憾,就无法更改我的评论。但是,您这样做破坏了继承。看到作者试图解决的问题是您不能像现在那样添加一个方法,因为现在不能对类型进行多态处理。
CodingYoshi

@CodingYoshi多态性要求玩家具有武器。在此方案中,玩家确实拥有武器。没有继承被打破。仅当您制定正确的规则时,此解决方案才会编译。
亚历克斯

@CodingYoshi现在,这并不意味着您无法编写需要运行时检查的代码,例如,如果您尝试向添加Weapona Player。但是没有类型系统在编译时您不知道具体类型可以在编译时作用于这些具体类型。根据定义。这种方案意味着只有在运行时才需要处理这种情况,因此实际上比任何Eric方案都要好。
Alex

0

那么,是什么阻止了开发人员这样做呢?他们只需要记住不要吗?

这个问题与称为“ 验证位置 ”的相当神圣的话题实际上是相同的(很可能也注意到了ddd)。

因此,在回答这个问题之前,您应该问自己:要遵循的规则的本质是什么?它们是用石头雕刻并定义实体吗?违反这些规则是否会导致实体不再是实体?如果是,除了将这些规则保留在命令验证中外,还将它们放在一个实体中。因此,如果开发人员忘记了验证命令,则您的实体将不会处于无效状态。

如果不是,那么,这自然就意味着该规则是特定于命令的,不应驻留在域实体中。因此,违反此规则将导致不应执行的动作,但不会处于无效的模型状态。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.