在Alex Papadimoulis的这篇文章中,您可以看到以下代码段:

private void attachSupplementalDocuments()
{
  if (stateCode == "AZ" || stateCode == "TX") {

    //SR008-04X/I are always required in these states
    attachDocument("SR008-04X");
    attachDocument("SR008-04XI");
  }

  if (ledgerAmnt >= 500000) {
    //Ledger of 500K or more requires AUTHLDG-1A
    attachDocument("AUTHLDG-1A");
  }

  if (coInsuredCount >= 5  && orgStatusCode != "CORP") {
    //Non-CORP orgs with 5 or more co-ins require AUTHCNS-1A
    attachDocument("AUTHCNS-1A");
  }
}


我真的不理解这篇文章。


如果每个业务规则常量都存储在某个配置文件中,那么每个人都很难维护该软件:[会有很多代码文件]共享一个大文件(或者相反,共享许多小配置文件);部署对业务规则的更改不需要新代码,而是手动更改配置文件;而调试则要困难得多。 br />
这是一个不好的做法吗?

在此代码段中,“ 500000”不是数字。例如,它不同于:

int doubleMe(int a) { return a * 2;}


其中2是不需要抽象的数字。它的用法很明显,并且不代表以后可以重用的内容。相反,“ 500000”不是简单的数字。这是一个重要的价值,代表了功能断点的想法。该号码可以在多个地方使用,但不是您正在使用的号码。这就是限制/边界线的概念,下面是一个规则,在上面是另一个规则。

如何从配置文件甚至#defineconst或您的语言提供的内容中引用它,比包含其价值还差吗?如果程序的稍后版本或其他程序员也需要该界限,那么该软件会为您带来另一种选择,这很麻烦(因为更改时,不能保证在两个文件中都会更改)。对于调试而言,这显然更糟。

另外,如果明天政府要求“从5/3/2050起,您需要添加AUTHLDG-122B而不是AUTHLDG-1A”,则此字符串常量不是简单的字符串常量。它代表一个想法。这只是该想法的当前值(这是“如果分类帐超过500k,则添加的内容”)。

让我澄清一下。我并不是说这篇文章是错误的;我就是不明白。

我确实知道,用常量,定义或配置变量替换所有可能的字符串文字或数字值不仅是没有必要的,但会使事情复杂化,但是这个特定示例似乎不属于此类。您怎么知道以后将不需要它?还是其他人呢?

评论

拼图:这些数字的好名字是什么?我想您会发现名称要么不增加任何值,要么描述代码已经描述的所有内容,并且经常在添加歧义的同时(“ LedgerLimitForAuthDlg1A”?)。我之所以发现这篇文章很棒,恰恰是因为它与此相关。我维护了同时使用这两种方法的系统,我可以告诉您业务规则属于代码-它使它们更易于跟踪,维护和理解。使用配置时,最好让它算账-价格昂贵得多。

配置应保留给需要配置的内容。如果业务规则通常是不可配置的,则将其放在配置中总不会给您带来任何好处。

对于适当的高级语言,配置采用实际子例程的形式,而不是字符串。

#1 楼

作者警告不要过早抽象。

if (ledgerAmt > 500000)行看起来像是您对大型复杂业务系统所期望看到的那种业务规则,这些系统的需求极其复杂,但精确且有据可查。

通常,这些需求是例外/边缘情况,而不是有用的可重用逻辑。这些需求通常是由业务分析师和主题专家拥有和维护的,而不是工程师

(请注意,在这种情况下,业务分析师/专家对需求的“所有权”通常发生在开发人员从事专家工作的情况下。领域没有足够的领域专业知识;尽管我仍然希望开发人员和领域专家之间能够进行充分的沟通/合作,以防止模棱两可或写得不好的需求。)
对于边缘情况和高度复杂的逻辑,通常没有办法有效地抽象该逻辑或使其更具可维护性。尝试构建抽象的尝试很容易适得其反-不仅浪费时间,而且导致可维护性降低。


如何从配置文件甚至#define,const或您的语言提供的任何内容中引用它,而不是包含它的值?如果程序的稍后版本或其他程序员也需要该界限,那么该软件会为您带来另一种选择,这很麻烦(因为更改时,不能保证在两个文件中都会更改)。对于调试来说,这显然更糟。即,当开发人员知道500000图在需求中出现两次时,该开发人员也知道它在代码中出现了两次。

考虑另一种(同样可能)的场景,其中500000出现在需求文档中的多个位置,但是主题专家决定仅更改其中一个。那里的风险更大,有人更改const的值可能不会意识到500000是用来表示不同的意思的-因此开发人员将其更改为一个,仅将其放置在代码中,最终破坏了某些内容他们没有意识到他们已经改变了。

这种情况在定制的法律/金融软件(例如,保险报价逻辑)中经常发生-编写此类文档的人不是工程师,他们没有问题,无需复制+粘贴整个规范,修改一些单词/数字,但大部分保持不变。

在这种情况下,处理复制粘贴要求的最佳方法是编写复制粘贴代码,并使该代码看起来与要求相似(包括对所有数据进行硬编码)

这种要求的现实是,它们通常不会长时间保持复制+粘贴状态,并且值有时会定期更改,但通常不会一并更改,因此,试图将这些需求合理化或抽象化或以任何方式简化它们最终会导致更多的维护难题,而不仅仅是将需求逐字翻译为代码。

评论


域特定语言(DSL)是使代码更像需求文档那样阅读的好方法。

–伊恩
16年4月8日在13:41

DSL的另一个优点是,也使得将应用程序,表示或持久性逻辑与业务规则意外混合变得更加困难。

– Erik Eidt
16-4-8在15:18



认为您的应用程序足够特殊以保证自己的DSL通常是自大的。

–brian_o
16年4月8日在17:50

这些要求通常由业务分析人员和主题专家拥有并维护,而不是由工程师拥有和维护,这并不总是一个好主意。有时,将那些需求转换为代码的行为会揭示出一些极端的情况,这些需求要么没有很好地定义,要么以与业务利益背道而驰的方式定义。如果业务分析师和开发人员可以合作实现共同的目标,那么可以避免很多问题。

–卡巴斯德
16年4月8日在19:58

@BenCottrell我并不是建议更改规则以使其更易于编写软件。但是,如果您在规则中有很多条件,那么当首先定义规则时,很可能会错过它们之间的某些交互。但是,当您将规范转换为代码时,开发人员一定会注意到这些条件之间可能存在相互作用。在这一点上,开发人员可能会发现,对规范的严格解释会导致意外价格,从而使客户可以使用该系统。

–卡巴斯德
16年4月9日在5:01

#2 楼

这篇文章很有道理。将常量提取到配置文件怎么可能是个坏习惯?如果不必要地使代码复杂化,则可能是一个不好的做法。直接在代码中拥有价值比从配置文件中读取价值要简单得多,而且编写的代码也易于遵循。


另外,明天,政府将“从5/3/2050开始,您需要
添加AUTHLDG-122B而不是AUTHLDG-1A“。


是的,然后更改代码。本文的重点是,更改代码并不比更改配置文件复杂。

如果您获得更复杂的逻辑,本文中描述的方法将无法扩展,但要点是您必须做出判断,有时最简单的解决方案就是最好的解决方法。 >

您如何知道以后不再需要它?还是其他人



这就是YAGNI原则的重点。不要为未知的未来而设计,这可能会与现在完全不同。您是正确的,如果在程序中的多个位置使用了值500000,则应将其提取为常数。但这不是所讨论的代码中的情况。

软件编码实际上是关注点分离的问题。您对可能会独立于核心应用程序逻辑进行更改的信息进行软编码。您永远不会将连接字符串硬编码到数据库,因为您知道它可能独立于应用程序逻辑而改变,并且需要针对不同的环境进行区分。在Web应用程序中,我们希望将业务逻辑与html模板和样式表分开,因为它们可能会独立更改,甚至可能由不同的人更改。

但是在代码示例中,硬编码的字符串和数字是应用程序逻辑的组成部分。可以想象一个文件可能会因您无法控制的某些策略更改而更改其名称,但是也可以想象,我们需要为不同的条件添加一个新的if分支检查。在这种情况下,提取文件名和编号实际上会破坏凝聚力。

评论


通常,更改代码比配置文件要复杂得多。您可能需要前者的开发人员和构建系统/发布周期,而后者只需要在友好的配置UI中的框中更改数字即可。

–橙色狗
16年4月8日在16:52

@OrangeDog是的,一开始就是这样。但是,如果您做这样的事情,则配置界面将变得非常友好,其中包含数百个完全无意义的文本框,询问您谁知道什么。现在,您必须构建UI并将其记录下来。提醒您,这并不意味着配置绝不是一个好方法-在某些情况下,绝对是正确的选择。但本文中没有任何示例。上一次立法仅更改数字是什么时候?上次更改增值税规则时,无论如何我们都必须重做所有计算。

–罗安
16年4月8日在18:37

@OrangeDog:在这里,您假设该软件的配置为您提供了进行所需检查所需的钩子。请注意,OP中的每个if如何基于不同的变量!如果无法从配置中访问所需的变量,则仍然需要修改软件。

– Matthieu M.
16年4月8日在18:43

@OrangeDog,因此,您建议应该对软件应用程序的逻辑进行重大更改,而无需进行开发/质量保证/发布周期和适当的测试?

–NPSF3000
16年4月8日在19:23

@OrangeDog:好的,您可以在示例中使用YAML来配置逻辑。由于逻辑包括条件规则,因此您找到了一种在YAML中表示这些条件的方法。恭喜,您已经重新发明了Python。那为什么不用Python编写整个应用程序呢?

–雅克B
16年4月9日在9:56

#3 楼

本文继续讨论“企业规则引擎”,这可能是他所反对的一个更好的例子。它包含自己的编程语言。

例如,示例中到文档映射的状态代码可以移动到配置文件中。但是您随后需要表达一个复杂的关系。

 <statecode id="AZ">
    <document id="SR008-04X"/>
    <document id="SR008-04XI"/>
</statecode>
 


也许您也可以提出

 <statecode id="ALL">
    <document id="AUTHLDG-1A" rule="ledgerAmt >= 50000"/>
</statecode>
 


很快就会发现您正在使用新语言进行编程您已经发明了该代码并将其保存在没有源代码或变更控制的配置文件中。

请注意,本文来自2007年,当时这种事情是一种常见的方法。

如今,我们很可能可以通过依赖项注入(DI)解决此问题。即,您将拥有一个“硬编码”

InvoiceRules_America2007 : InvoiceRules


,您可以将其替换为一个硬编码或更可配置的

InvoiceRules_America2008 : InvoiceRules

/>
法律或业务要求发生变化时。

评论


也许您应该定义“ DI”。也许再解释一下。

–罗勒·布尔克
16年4月8日在19:29

为什么该文件不在源控制系统中?

–JDługosz
16年4月8日在20:05

如果是特定于客户的,那么编码版本的if语句是否会给每个客户提供不同的值?这听起来像应该在配置文件中。处于一种或另一种文件中,而其他所有条件相同,则不是不控制/跟踪/备份文件的原因。 @ewan似乎在说,由于某种原因,DSL的文件不能保存为项目的一部分,即使图像,声音文件和文档等非代码资产也肯定存在。

–JDługosz
16年4月9日在1:51

您应该真正从XML中重构出“ 50000”值,并将其放在单独的配置文件中,您认为吗? ...顺便说一下,应该是500000

–通配符
16年4月9日在7:02

@jdlugosz ERE的概念是您购买系统,然后根据需要进行配置。也许是因为内部开发人员正在与这些“灵活”系统竞争,所以他们会尝试模仿它们。即使在像IBM这样的大公司的系统中,更改配置控制也常常是事后的想法。卖点是快速改变

–伊万
16年4月9日在16:38

#4 楼


相反,“ 500000”不仅仅是数字。这是一个重要的
值,代表了功能断点的概念。
这个数字可以在多个地方使用,但不是您正在使用的
数字,这是极限/边界线的概念,在下面的规则中
,在另一个规则之上。注释是多余的):

 if (ledgerAmnt >= 500000) {
    //Ledger of 500K or more requires AUTHLDG-1A
    attachDocument("AUTHLDG-1A");
  }


这只是重复代码的作用:

LEDGER_AMOUNT_REQUIRING_AUTHLDG1A=500000
if (ledgerAmnt >= LEDGER_AMOUNT_REQUIRING_AUTHLDG1A) {
    //Ledger of 500K or more requires AUTHLDG-1A
    attachDocument("AUTHLDG-1A");
}


注意作者假定500000的含义与此规则相关;它不是在其他地方或可能在其他地方重用的值:


先前的软编码
可以解释的唯一且唯一的业务规则更改是更改所需的分类帐金额
表格AUTHLDG-1A。任何其他业务规则更改都将需要甚至更多的工作–配置,文档,代码等


我认为本文的主要观点是有时数字只是一个数字:除了代码中所传达的内容外,它没有其他意义,并且不太可能在其他地方使用。因此,仅为了避免使用硬编码值而笨拙地以可变名称概括代码(现在)正在做什么,充其量是不必要的重复。

评论


如果引入了常量LEDGER_AMOUNT_REQUIRING_AUTHLDG1A,则不会再将注释写入代码中。程序员对注释的维护不好。如果金额更改,则if条件和注释将不同步。相反,常量LEDGER_AMOUNT_REQUIRING_AUTHLDG1A永远不会与自身不同步,并且它无需解释就可以说明其目的。

–ZeroOne
16-4-9的12:59

@ZeroOne:除非业务规则更改为“ 500K或更高的账本需要AUTHLDG-1A和AUTHLDG-2B”,否则添加attachDocument(“ AUTHLDG-2B”)的人很有可能;行将无法同时更新常量名称。在这种情况下,我认为代码非常清晰,没有注释,也没有解释变量。 (尽管约定通过代码注释指示业务需求文档的适当部分可能是有意义的。在这样的约定下,在这里适合的代码注释就可以了。)

–ruakh
16年4月10日在0:49

@ruakh,好,然后我将常量重构为LEDGER_AMOUNT_REQUIRING_ADDITIONAL_DOCUMENTS(我可能应该首先完成)。我还习惯于将业务需求ID放入Git提交消息中,而不是源代码中。

–ZeroOne
16年4月10日在10:55

@ZeroOne:但是对于AUTHLDG-3C,分类帐数量实际上是最大的。对于AUTHLDG-4D,适当的分类帐数量取决于状态。 (您明白了吗?对于这种类型的代码,您希望您的代码反映业务规则,而不是尝试一些抽象的业务规则,因为没有理由期望业务规则的演变与业务规则保持一致。您采用的抽象。)

–ruakh
16年4月10日在17:50

就个人而言,我不反对在代码中添加幻数,我反对对代码进行结构化,因此它需要这些注释。如果是我,我将使用其自己的attachIfNecessary()方法使每个文档成为一个枚举实例,然后遍历所有文档。

– David Moles
16年4月11日在4:15

#5 楼

其他答案是正确的,周到的。但是,这是我的简短回答。

   Rule/value          |      At Runtime, rule/value…
  appears in code:    |   …Is fixed          …Changes
----------------------|------------------------------------
                      |                 |
  Once                |   Hard-code     |   Externalize
                      |                 |   (soft-code)
                      |                 |
                      |------------------------------------
                      |                 |
  More than once      |   Soft-code     |   Externalize
                      |   (internal)    |   (soft-code)
                      |                 |
                      |------------------------------------
 


如果规则和特殊值出现在代码中的一个位置,并且在运行时不会更改,然后按照问题所示进行硬编码。

如果规则或特殊值出现在代码中的多个位置,并且在运行时不会更改,然后进行软编码。规则的软编码可能是我定义特定的类/方法或使用Builder模式。对于值,软编码可能意味着为要在整个代码中使用的值定义单个常量或枚举。

如果规则或特殊值可能在运行时更改,则必须将其外部化。通常通过更新数据库中的值来完成。或通过用户输入数据来手动更新内存中的值。也可以通过将值存储在文本文件(XML,JSON,纯文本等)中进行重复扫描以查找文件修改日期-时间更改。

评论


我喜欢您的回答,但我认为您还应该考虑它在实施时是否会发生变化。如果事物是​​一种将在许多组织中使用的产品,则这主要是相关的,例如,对于主管是否需要批准X上的退款等有不同的规则,等等。

–打破酒吧
16年4月10日在15:57

同意这个答案和关于实现的评论。我从事的工作是由许多组织实施的,其中许多组织具有不同的价值要求。我们倾向于将这些“设置”存储在数据库中,而不是配置文件中,但是原则是我们不想为每个实施该软件的公司制作不同的软件版本(然后在每次升级时重复这些不同的版本) 。

– RosieC
16-4-14在21:23



#6 楼

当我们试图说明一个真实的问题时,这是我们遇到的陷阱,当我们使用玩具问题然后仅提出稻草人解决方案时。

在给出的示例中,这并没有什么不同给出的值是硬编码为内联值,还是定义为const。

周围的代码使该示例成为维护和编码恐怖。如果没有周围的代码,那么至少在恒定重构的环境中,该代码段就可以了。在不太可能进行重构的环境中,该代码的维护者已经死了,原因很快就会变得很明显。

看看,如果周围有代码,那么显然会发生不好的事情。

第一件不好的事情是,值50000被某个地方的另一个值使用,例如,某些州的税率发生变化的分类帐额...然后,当发生更改时,维护者没有知道时,当他在代码中找到这两个50000实例时,它们是指相同的50k还是完全不相关的50ks。并且如果有人也将它们用作常量,您还应该搜索49999和50001吗?这不是在其他服务的配置文件中对这些变量进行划分的调用:但是内联对其进行硬编码显然也是错误的。相反,它们应该是常量,在使用它们的类或文件中定义并限定范围。如果两个50k实例使用相同的常数,则它们可能代表相同的立法限制;如果没有,他们可能不会;无论哪种方式,它们都将具有一个名称,该名称的透明度比内联数字要少。

文件名将被传递给一个函数-AttachDocument()-该函数接受基本文件名作为字符串,没有路径或扩展名。从本质上来说,文件名是指向某些文件系统或数据库或attachDocument()从何处获取文件的外键。但是字符串没有告诉您任何信息-那里有多少个文件?它们是什么类型的文件?您如何知道在开拓新市场时是否需要更新此功能?它们可以附着在什么类型的东西上?维护人员完全不知所措,他所拥有的只是一个字符串,该字符串可能在代码中多次出现,并且每次出现时都具有不同的含义。在一个地方,“ SR008-04X”是一个作弊代码。在另一个命令中,这是命令订购四个SR008助推火箭的命令。这是文件名吗?这些有关系吗?有人刚刚更改了该功能,以提及另一个文件“客户端”。然后,您,糟糕的维护者,被告知“ CLIENT”文件需要重命名为“ CUSTOMER”。但是字符串“ CLIENT”在代码中出现了937次...您甚至从哪里开始看?码。不是“ 1”或“ 10”,而是“ 50,000”。不是“ client”或“ report”而是“ SR008-04X”。

稻草人的唯一解决不透明常量问题的方法是将它们隐藏在某些配置文件中无关的服务。

您可以一起使用这两个谬论来证明任何论点为真。

评论


不是玩具问题,不是稻草人。在这类业务应用程序中,您会一直看到这种情况。没有“向新市场开放”,也没有重复使用相同的数字(毕竟,无论如何这都会赋予它另一个含义),并且无论如何,该文章对DRY毫无用处-如果价值存在两个依赖关系,将被移到方法或常量中。显示这些常量(配置设置,实际上并不重要)应如何命名的示例,以及如何以一种可以证明未来且比代码更清晰的方式将它们存储在何处。

–罗安
16-4-10的6:31

该示例没有分解,因为它是一个玩具问题。周围的代码总是很恐怖的,因为软件必须执行的业务规则很恐怖。尝试通过规则引擎和DSL来回避这一基本挑战,而程序员通常会拖延时间,因为解决CS问题比解决复杂的税收形式更有趣。尝试实现“优雅”通常是傻瓜的事,因为该软件的最终任务是为复杂的灾难建模。

–whatsisname
16-4-10在6:58



业务规则可能很恐怖,但这本身并不是编写这种平庸的过程代码的借口。 (我倾向于Papadimoulis,认为在代码中建模和维护规则比在配置中更容易,我只是认为它应该是更好的代码。)我看到的DRY问题不是魔幻数字,是重复的if(。 ..){attachDocument(...); }。

– David Moles
16年4月11日在4:18



#7 楼

其中有几个问题。

一个问题是,应该构建规则引擎以使所有规则都可以在程序本身之外轻松配置。在类似情况下的答案通常不是。规则将以难以预测的奇怪方式变化,这意味着只要发生​​变化,就必须扩展规则引擎。

另一个问题是如何处理这些规则及其在您的更改中版本控制。最好的解决方案是将每个规则划分为一个类。

允许每个规则具有其自身的有效性,某些规则每年更改一次,某些更改取决于获得许可的时间给定或开具发票。规则本身包含必须应用哪个版本的检查。

由于常量是私有的,因此不能在代码中的其他任何地方滥用。

然后有一个列表所有规则并应用列表。

另一个问题是如何处理常量。 500000可能看起来不起眼,但是必须非常小心以确保正确转换。如果应用了任何浮点算法,则可能会将其转换为500,000.00001,因此与500,000.00000的比较可能会失败。甚至更糟的是500000总是按预期工作,但是以某种方式565000在转换时会失败。确保转换是显式的,并且不是由编译器猜测的,而是由您进行的。通常,这是通过在使用之前将其转换为一些BigInteger或BigDecimal来完成的。

#8 楼

尽管没有在问题中直接提及它,但我想指出重要的是不要将业务逻辑埋在代码中。

像上面的示例一样,对外部指定的业务需求进行编码的代码应确实驻留在源代码树的不同部分(可能名为businesslogic或类似名称)中,应注意确保仅以尽可能简单,可读和简洁的方式对业务需求进行编码,以最少的样板并提供清晰,有用的信息注释。

不应与实现业务逻辑所需功能的“基础结构”代码混合使用,例如示例中attachDocument()方法的实现,例如UI,日志记录或数据库代码。虽然执行这种分离的一种方法是在配置文件中“软编码”所有业务逻辑,但这远非唯一(或最佳)方法。写得足够清楚,如果您向没有编码技能的业务领域专家展示它,他们将能够理解它。至少,如果以及当业务需求发生变化时,对它们进行编码的代码应该足够清晰,以至于即使是一个事先不熟悉代码库的新程序员也应该能够轻松地定位,查看和更新​​业务逻辑,并假设理想情况下,此类代码也将以特定于域的语言编写,以实现业务逻辑与底层基础结构之间的分离,但是对于基本的而言,这可能不必要地复杂内部应用程序。也就是说,如果您是将软件出售给每个都需要自己的自定义业务规则集的多个客户,简单的特定于域的脚本语言(例如,可能基于Lua沙箱)就可以了。

评论


这正是我在想的!!!当逻辑深藏在代码中时,领域/主题专家或业务用户如何才能查看正在使用的值和逻辑,以确保它们正确并诊断系统的行为?配置文件所做的一件事是使设置可见。必须使用某种方法来提高业务规则的可见性,即使这会使编码变得“困难”。只要业务用户能够访问和理解它们,我就可以接受完成这些工作的一个或多个瘦类,而不会混淆其他问题。

– ErikE
16年4月11日在16:48