是否根据最佳实践检查冗余条件?


16

在过去的三年中,我一直在开发软件,但是最近我才意识到自己对良好实践的无知。这使我开始阅读《清洁代码》一书,这使我的生活变得更好,但我一直在努力了解一些编写程序的最佳方法。

我有一个Python程序,其中...

  1. 使用argparse required=True强制使用两个参数,它们都是文件名。第一个是输入文件名,第二个是输出文件名
  2. 具有readFromInputFile首先检查输入文件名已被输入的功能
  3. 具有writeToOutputFile首先检查查看输入的文件名的功能

我的程序很小,导致我相信#2和#3中的检查是多余的,应将其删除,从而将这两个功能从不必要的if条件中解放出来。但是,我还被认为“双重检查是可以的”,并且可能是一个程序的正确解决方案,在该程序中可以从不进行参数解析的其他位置调用函数。

(此外,如果读取或写入失败,我try except在每个函数中都有一个引发相应的错误消息。)

我的问题是:最好避免所有冗余条件检查?程序的逻辑是否应该如此扎实,以至于检查只需进行一次?有没有很好的例子说明这一点或相反的情况?

编辑:谢谢大家的答案!我从每个人那里学到了一些东西。看到如此多的观点使我对如何解决这个问题以及根据我的需求确定解决方案有了更好的理解。谢谢!


这是您问题的概括版本:softwareengineering.stackexchange.com/questions/19549/…。我不会说它是重复的,因为它有更大的重点,但是也许有帮助。
布朗

Answers:


15

您要求的是“健壮性”,没有正确或错误的答案。它取决于程序的大小和复杂性,其中工作的人数以及检测故障的重要性。

在小型程序中,您是一个人编写并且只能自己编写,与编写一个由多个组件(可能由团队编写)组成的复杂程序相比,健壮性通常要小得多。在这样的系统中,以公共API的形式在组件之间存在边界,并且在每个边界处,验证输入参数通常是一个好主意,即使“程序的逻辑应该如此扎实,以至于那些检查是多余的” ”。这使得错误检测变得非常容易,并有助于缩短调试时间。

对于您的情况,您必须自己决定对程序期望什么样的生命周期。您是否希望该程序可以使用和维护多年?然后添加冗余检查可能会更好,因为将来不太可能会重构您的代码,并且您readwrite函数可能会在不同的上下文中使用。

还是只是为了学习或娱乐目的的小程序?然后,将不再需要那些双重检查。

在“清洁代码”的上下文中,可能会问到双重检查是否违反了DRY原理。实际上,有时至少在某种程度上确实如此:输入验证可以解释为程序业务逻辑的一部分,并且在两个地方使用输入验证可能会导致由于违反DRY引起的常见维护问题。健壮性与DRY的比较通常是一个折衷方案-健壮性要求代码冗余,而DRY则要尽量减少冗余。并且,随着程序复杂性的增加,健壮性比在验证中采用DRY变得越来越重要。

最后,让我举一个例子说明您的情况。让我们假设您的需求变为类似

  • 该程序还应使用一个参数,即输入文件名,如果没有给出输出文件名,则它会通过替换后缀从输入文件名自动构建。

这是否使您可能需要在两个地方更改双重验证?可能不会,这样的要求在调用时会导致一个更改argparse,但在writeToOutputFile以下方面没有任何更改:该函数仍然需要文件名。因此,在您的情况下,我将投票两次进行输入验证,恕我直言,因为有两个更改的地方而导致维护问题的风险,比因检查太少而导致掩盖错误而导致维护问题的风险要低得多。


“……公共API形式的组件之间的边界……”我观察到,“类跨越边界”可以这么说。因此,需要一个类。连贯的业务领域类。我从这个OP推断,“它很简单,因此不需要课程”的普遍原则在这里起作用。可能会有一个简单的类包装“主对象”,从而执行诸如“文件必须具有名称”之类的业务规则,这不仅使现有代码干燥,而且在将来使其保持干燥。
雷达波

@radarbob:我写的内容不仅限于OOP或类形式的组件。这也适用于具有公共API或不面向对象的任意库。
Doc Brown

5

冗余不是罪过。不必要的冗余是。

  1. 如果readFromInputFile()writeToOutputFile()是公共函数(按照Python的命名约定,因为它们的名称不是以两个下划线开头),则有一天某些人可能会完全避免使用argparse来使用这些函数。这意味着当他们放弃参数时,他们看不到您的自定义argparse错误消息。

  2. 如果readFromInputFile()writeToOutputFile()自己检查参数,您将再次显示一条自定义错误消息,说明对文件名的需求。

  3. 如果readFromInputFile()并且writeToOutputFile()不自行检查参数,则不会显示任何自定义错误消息。用户将不得不自己找出产生的异常。

归结为3。编写一些实际上使用这些功能的代码,以避免argparse并产生错误消息。假设您根本没有看过这些函数,只是相信它们的名称可以提供足够的理解以供使用。当您知道所有这些时,是否有任何方式可以使异常混淆?是否需要定制的错误消息?

关闭记住这些功能内部的大脑部分非常困难。如此之多,以至于有人建议在使用的代码之前写出使用代码。这样一来,您就已经知道了从外部看起来是什么样的问题。您不必执行TDD来执行此操作,但是如果您执行TDD,则您已经首先要从外部进入。


4

使方法独立和可重复使用的程度是一件好事。这意味着方法应该接受它们所接受的内容,并且它们应该具有定义明确的输出(精确返回的结果)。这也意味着他们应该能够妥善处理传递给他们的所有内容,而不会对输入的性质,质量,时间等做出任何假设。

如果程序员习惯于编写一些方法来假设传入的内容,则基于诸如“如果这被打破了,我们还有更大的事情要担心”或“参数X不能具有值Y,因为其余的代码阻止了它”,那么突然之间您实际上不再拥有独立的,分离的组件。您的组件本质上取决于更广泛的系统。这是一种微妙的紧密耦合,随着系统复杂性的增加,总拥有成本将成倍增加。

请注意,这可能意味着您要多次验证同一信息。但这没关系。每个组件都以自己的方式负责自己的验证。这并不违反DRY,因为验证是通过解耦的独立组件完成的,并且一个验证中的更改不必完全复制到另一个中。这里没有冗余。X有责任检查其输入是否满足其自身需求,然后将某些输入传递给Y。Y有责任检查其自身的输入是否满足其需求


1

假设您有一个函数(在C中)

void readInputFile (const char* path);

而且您找不到有关该路径的任何文档。然后看一下实现,它说

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

这不仅会测试该函数的输入,而且还会告诉该函数的用户该路径不允许为NULL或空字符串。


0

通常,双重检查并不总是好事或坏事。在您所依赖的特定案例中,问题始终有很多方面。在您的情况下:

  • 该程序有多大?值越小,呼叫者做正确的事情就越明显。当程序变大时,准确指定每个例程的先决条件和后置条件变得越来越重要。
  • 参数已由argparse模块检查。使用一个库然后自己完成它的工作通常是一个坏主意。那为什么要使用图书馆呢?
  • 在调用者不检查参数的情况下,您的方法被重用的可能性有多大?可能性越大,验证参数就越重要。
  • 如果缺少论点怎样?找不到输入文件可能会完全停止处理。这可能是很明显的故障模式,很容易纠正。潜在的错误是那些程序在不引起您注意的情况下一直保持正常运行并产生错误结果的错误。

0

您的双重检查似乎是在很少使用的地方。因此,这些检查只是使您的程序更强大:

一张支票太多不会受伤,一张支票可能会少一点。

但是,如果您要在经常重复的循环中进行检查,则应考虑删除冗余,即使在大多数情况下,与检查后进行的检查相比,检查本身的花费并不大。


而且,由于您已经拥有了它,因此不值得将其删除,除非它处于循环状态或其他状态。
StarWeaver

0

也许您可以改变观点:

如果出了问题,结果是什么?会对您的应用程序/用户造成伤害吗?

当然,您总是可以争论,检查的好坏是好还是坏,但这是一个相当学术的问题。而且,由于您要处理真实世界的软件,因此会有真实世界的后果。

从上下文中您可以得出:

  • 一个输入文件A
  • 一个输出文件B

我假设您正在执行从AB的转换。如果AB很小,而变换也很小,那么后果是什么?

1)您忘记了指定从何处读取:那么结果什么都没有。并且执行时间将比预期的短。您查看结果-或更好:查看丢失的结果,查看您以错误的方式调用了该命令,重新开始,一切都很好

2)您忘记指定输出文件。这导致了不同的情况:

a)立即读取输入。比转换开始时,应该写入结果,但是您收到一个错误。根据时间的不同,您的用户必须等待(取决于应处理的数据量),这可能很烦人。

b)逐步读取输入。然后,写入过程立即像(1)中一样退出,并且用户再次重新开始。

在某些情况下,不严格检查可能被认为是可以的。这完全取决于您的用例以及您的意图。

另外:您应该避免偏执狂,并且不要做过多的重复检查。


0

我认为测试不是多余的。

  • 您有两个需要使用文件名作为输入参数的公共函数。验证其参数是适当的。这些功能可能会在需要其功能的任何程序中使用。
  • 您有一个程序,需要两个必须是文件名的参数。碰巧使用了这些功能。该程序适合检查其参数。

在两次检查文件名的同时,出于不同目的检查文件名。在一个可以信任该功能参数的小型程序中,该功能的检查可能被认为是多余的。

一种更强大的解决方案将具有一个或两个文件名验证器。

  • 对于输入文件,您可能需要验证参数是否指定了可读文件。
  • 对于输出文件,您可能需要验证参数是可写文件还是可以创建和写入的有效文件名。

我使用两条规则来决定何时执行操作:

  • 尽早做。这对于总是需要的东西效果很好。从该程序的角度来看,这是对argv值的检查,并且程序逻辑中的后续验证将是多余的。如果将函数移到库中,则它们不再是多余的,因为库不能相信所有调用方都已验证参数。
  • 尽可能晚地做。这对于很少需要的东西非常有效。从该程序的角度来看,这是对功能参数的检查。

0

该检查是多余的。但是,要解决此问题,需要删除readFromInputFile和writeToOutputFile并将其替换为readFromStream和writeToStream。

在代码接收文件流的那一点上,您知道您已经将有效流连接到有效文件,或者可以将任何其他流连接到该文件。这样可以避免重复检查。

然后,您可能会问,嗯,您仍然需要在某处打开流。是的,但这在参数解析方法内部发生。您在那里进行了两项检查,一项检查是否需要文件名,另一项检查是在给定上下文中检查文件名所指向的文件是否为有效文件(例如,输入文件存在,输出目录可写)。这些检查是不同类型的检查,因此它们不是多余的,它们发生在参数解析方法(应用程序范围)内而不是核心应用程序内。

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.