有必要遵循防御性编程实践以获取永远不会公开获得的代码?


45

我正在编写纸牌游戏的Java实现,因此我创建了一种特殊的Collection类型,称为“区域”。不支持Java的Collection的所有修改方法,但是Zone API中有一种方法,该方法move(Zone, Card)可将Card从给定的Zone移到其自身(由package-private技术实现)。这样,我可以确保没有任何卡被带出区域并消失。它们只能移到另一个区域。

我的问题是,这种防御性编码有必要吗?这是“正确的”,并且感觉像是正确的做法,但这并不意味着Zone API永远不会成为某些公共库的一部分。这只是给我的,所以这有点像是在保护我自己的代码,以免仅使用标准Collections可能会提高效率。

我应该把这个区域创意带到多远?谁能给我一些建议,让我考虑在我编写的类中保存合同,特别是对于那些不会真正公开可用的类,应该考虑些什么?


4
=〜s /必要/推荐/ gi
GrandmasterB 2013年

2
数据类型在构造上应该是正确的,否则您将要建立什么?应该以某种方式封装它们,无论可变与否,它们只能永远处于有效状态。仅当不可能强制执行此操作(或不合理地困难)时,才引发运行时错误。
乔恩·普迪

1
永远不要把话说绝了。除非您的代码从未被使用过,否则您将永远无法确定代码将在哪里结束。;)
Izkata 2013年

1
@codebreaker GrandmasterB的评论是一个替换表达式。这意味着:用“推荐”替换“必要”。
里卡多·索扎

1
在这里,无代码#116 信任无人可能特别合适。

Answers:


72

我不会解决设计问题-只是在非公开API中是否“正确”执行操作的问题。

这只是给我的,所以有点像我在保护自己的代码

这就是重点。也许那里的编码人员会记住他们曾经编写的每个类和方法的细微差别,并且永远不会以错误的合同错误地调用它们。我不是其中之一。我经常忘记编写的代码应该在编写后的几个小时内起作用。如果您认为问题已经得到了它的权利一次,你的头脑会倾向于齿轮切换到你的工作问题现在

您有打击该问题的工具。这些工具包括(无特定顺序)约定,单元测试和其他自动化测试,前提条件检查和文档。我本人已发现单元测试非常宝贵,因为它们既迫使您考虑如何使用合同,又在以后提供有关接口设计方式的文档。


很高兴知道。过去,我过去只是尽可能高效地进行编程,所以有时很难适应这样的想法。我很高兴我朝着正确的方向前进。
破解者

15
“有效”可能意味着许多不同的东西!以我的经验,新手(不是说您是一个新手)经常会忽略他们支持该程序的效率。通常,代码在产品生命周期的支持阶段所花费的时间要比“编写新代码”阶段所花费的时间长得多,因此,我认为这是一种效率,应该仔细考虑。
查理·基利安

2
我绝对同意。但是,回到大学后,我再也不必考虑这一点。
密码破解者2013年

25

我通常遵循一些简单的规则:

  • 尝试始终按合同进行编程。
  • 如果一种方法是公开可用的或从外界获得输入,请采取一些防御措施(例如IllegalArgumentException)。
  • 对于只能在内部访问的所有其他内容,请使用断言(例如assert input != null)。

如果一个客户真的很喜欢它,他们总是会找到一种使您的代码行为异常的方法。他们至少可以总是通过反思来做到这一点。但这就是合同设计的美。您不赞成这种使用代码的方式,因此不能保证它会在这种情况下运行。

对于您的特定情况,如果Zone不应由外部人使用和/或访问,则可以将类设为package-private(可能是final),或者最好使用Java已经提供给您的集合。它们已经过测试,您不必重新发明轮子。请注意,这不会阻止您在整个代码中使用断言来确保一切正常。


1
+1提及合同设计。如果您不能完全禁止行为(这很难做到),至少您要明确表示,不能保证行为良好。我也喜欢抛出IllegalStateException或UnsupportedOperationException。
user949300 2013年

@ user949300可以。我想相信此类例外是出于有意义的目的而引入的。履行合同似乎适合这种角色。
afsantos 2013年

16

防御性编程是一件非常好的事情。
直到它开始妨碍编写代码。那就不是一件好事了。


讲得更务实一点...

听起来您就将事情推到了极限。挑战(以及对问题的答案)在于理解什么是业务规则或程序要求。

以您的纸牌游戏API为例,在某些环境下,防止欺诈的所有措施都很关键。可能涉及大量的真实货币,因此有意义的是进行大量检查以确保不会发生作弊行为。

另一方面,您需要牢记SOLID原则,尤其是单一责任。要求容器类有效地审核卡片的去向可能会有些麻烦。在卡容器和接收移动请求的功能之间最好有一个审计/控制器层。


与这些问题相关,您需要了解API的哪些组件公开(因此容易受到攻击)与私有的组件和较少公开的组件。我并非完全主张“内部具有柔软内部的硬质涂层”,但最好的回报是使API的外部变硬。

我认为,库的最终用户并不会对确定要部署多少防御性程序的批评那么严格。即使使用我自己编写的模块,我仍然采取适当的检查措施,以确保将来我在调用该库时不会犯一些无意的错误。


2
+1表示“直到开始妨碍编写代码。” 特别是对于短期个人项目,防御性编码可能花费的时间远远超过其价值。
科里

2
同意,尽管我想补充一点,能够/能够/进行防御性编程是一件好事,但是以原型方式进行编程也是至关重要的。两者都具有的能力使您可以选择最合适的操作,这比我所知道的很多只能防御性编程的程序员要好得多。
David Mulder 2013年

13

防御性编码不仅仅是公共代码的好主意。对于不立即丢弃的任何代码来说,这都是一个主意。当然,您知道现在应该如何调用它,但是您不知道从现在起六个月后回到项目时会如何记得。

与较低级的或解释性的语言(例如C或Javascript)相比,Java的基本语法为您提供了很多可靠的防御。假设您清楚地命名方法并且没有外部“方法排序”,则可以通过简单地将参数指定为正确的数据类型并在合理类型的数据仍然无效的情况下包含明智的行为来摆脱困境。

(顺便说一句,如果纸牌总是必须位于区域中,我认为通过将所有正在使用的纸牌都引用到游戏对象的全局集合中,您会获得更好的性价比,并且将Zone设为的属性每张卡。但是由于除了持卡外,我不知道您的区域有什么用途,因此很难知道这是否合适。)


1
我认为区域是卡的属性,但是由于我的卡可以更好地用作不可变对象,因此我认为这种方式是最好的。谢谢你的建议。
密码破解者2013年

3
@codebreaker在这种情况下可以帮助解决的一件事是将卡封装在另一个对象中。黑桃王牌就是它。位置未定义其身份,因此卡可能应该是不变的。也许有一个区域包含纸牌:也许有一个CardDescriptor包含纸牌,其位置,面朝上/朝下状态,甚至是关心该游戏的旋转的纸牌。这些都是可变属性,不会更改卡的身份。

1

首先创建一个保留区域列表的类,这样您就不会丢失区域或其中的卡片。然后,您可以检查传输是否在ZoneList中。此类可能是一种单例,因为您只需要一个实例,但是以后可能需要一组Zones,因此请保持打开状态。

其次,除非您期望使用Zone或ZoneList实现Collection或其他任何东西,否则不要使用它。也就是说,如果将Zone或ZoneList传递给期望Collection的对象,实现它。您可以通过让它们抛出异常(UnimplementedException或类似的东西)或让它们简单地不执行任何操作来禁用一堆方法。(在使用第二个选项之前,请三思而后行。如果这样做,因为它很容易,您会发现自己丢失了可能早早发现的错误。)

关于什么是“正确的”,确实存在疑问。但是,一旦弄清楚了它是什么,您就会想用这种方式做事。两年之内,您会忘记所有这些,如果您尝试使用该代码,则会对以这种违反直觉的方式编写该代码并且什么都没解释的家伙感到非常恼火。


2
您的答案过于集中在眼前的问题上,而不是OP在总体上对防御性编程提出的更广泛的问题。

实际上,我确实将Zones传递给采用Collections的方法,因此实现是必需的。但是,在游戏中对区域进行某种注册很有趣。
密码破解者2013年

@ GlenH7:我发现处理特定的例子通常比抽象理论更有帮助。OP提供了一个相当有趣的选项,因此我同意了。
RalphChapin 2013年

1

API设计中的防御性编码通常是关于验证输入并仔细选择适当的错误处理机制。其他答案提到的事情也值得注意。

这实际上与您的示例无关。出于非常特定的原因,您在那里限制了API的面。正如GlenH7所提到的,当要在实际游戏中使用一组纸牌时,例如具有(“二手”和“未二手”)卡组,一张桌子和一只手,您肯定要检查一下以确保每张纸牌都到位。集合中的卡片仅出现一次。

您使用“区域”设计此对象是一个任意选择。根据实现方式(在上面的示例中,区域只能是手,甲板或桌子),它可能是一个全面的设计。

但是,该实现听起来像是一组具有更多相似性的Collection<Card>卡,并且具有较少的API限制。例如,当您要构建一手价值计算器或一台AI时,您肯定希望自由选择要迭代的每张卡的数量和数量。

因此,如果该API的唯一目标是确保每张卡始终位于区域中,则最好公开这种限制性API。

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.