什么是“软编码”?


87

在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");
  }
}

我真的不明白这篇文章。

我引用:

如果每个业务规则不断被存储在一些配置文件,生活会多[更多(原文如此)难以为大家维护软件:有会是大量的代码文件共享的一个大文件(或者,反过来,大量微型配置文件);部署对业务规则的更改不需要新代码,而是手动更改配置文件;而调试则要困难得多。

这是反对在配置文件中包含“ 500000”常量整数或“ AUTHCNS-1A”和其他字符串常量的说法。

这怎么可能是个坏习惯?

在此代码段中,“ 500000”不是数字。例如,它与以下内容不同:

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

其中2是不需要抽象的数字。它的用法很明显,并且不代表以后可以重用的内容。

相反,“ 500000”不仅仅是数字。这是一个重要的价值,代表了功能断点的想法。该号码可以在多个地方使用,但不是您正在使用的号码。这是限制/边界线的概念,低于此规则将适用一个规则,高于该规则将适用另一条规则。

从配置文件甚至是#defineconst或您的语言提供的任何内容中引用它,比包含它的值还差吗?如果后来上节目,或者一些其他的程序员,还要求边缘,使该软件使得另一种选择,你就完蛋了(因为它改变,没有什么可以保证你将在这两个文件改变)。对于调试而言,这显然更糟。

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

让我澄清一下。我并不是说这篇文章是错误的;我就是不明白。也许解释得不太好(至少出于我的想法)。

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


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

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

对于适当的高级语言,配置采用实际子例程的形式,而不是字符串。
托尔比约恩Ravn的安徒生

Answers:


100

作者警告不要过早抽象。

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

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

(请注意,在这种情况下,业务分析师/专家对需求的“所有权”通常发生在专业领域的开发人员没有足够的领域专业知识的情况下;尽管我仍然希望开发人员与领域专家之间能够进行充分的沟通/合作,以防止受到攻击含糊不清或书写不当的要求。)

当维护其需求充满边缘情况和高度复杂的逻辑的系统时,通常没有办法有效地抽象该逻辑或使其更具可维护性。尝试构建抽象的尝试很容易适得其反-不仅浪费时间,而且导致可维护性降低。

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

此类代码倾向于受到以下事实的保护:代码本身可能具有与需求的一对一映射。即,当开发人员知道500000数字在需求中出现两次时,该开发人员也知道其在代码中出现两次。

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

这种情况在定制的法律/财务软件(例如,保险报价逻辑)中经常发生-编写此类文档的人不是工程师,他们可以毫无问题地复制+粘贴规范的全部内容,只需修改几个单词/数字,但是保持大多数不变。

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

此类需求的实际情况是,它们通常不会长时间保持复制+粘贴,并且值有时会定期更改,但它们通常不会一前一后更改,因此请尝试对这些需求进行合理化或抽象化或简化无论如何,它们最终都带来了更多的维护难题,而不仅仅是将需求逐字翻译为代码。


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

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

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

8
Those requirements are typically owned and maintained by business analysts and subject matter experts, rather than by engineers这并不总是一个好主意。有时,将这些需求转换为代码的行为会揭示出一些极端的情况,这些需求要么没有很好地定义,要么定义得与业务利益背道而驰。如果业务分析师和开发人员可以合作实现共同的目标,那么可以避免很多问题。
kasperd '16

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

44

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

此外,明天政府将发布“从5/3/2050起,您需要添加AUTHLDG-122B而不是AUTHLDG-1A”。

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

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

您怎么知道以后将不需要它?还是其他人呢?

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

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

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


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

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

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

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

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

26

本文继续讨论“企业规则引擎”,这可能是他所反对的一个更好的例子。

逻辑是,您可以概括到您的配置变得如此复杂以至于它包含自己的编程语言。

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

<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

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


4
也许您应该定义“ DI”。也许再解释一下。
罗勒·布尔克

9
为什么该文件不在源控制系统中?
JDługosz

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

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

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

17

相反,“ 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。任何其他业务规则更改将需要更多的工作–配置,文档,代码等

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


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

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

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

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

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

8

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

  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,纯文本等)中进行重复扫描以查找文件修改日期-时间更改。


1
我喜欢您的回答,但我认为您还应该考虑它在实施时是否会发生变化。这主要是相关的,如果事情是,在许多组织可能,例如,有超过监事是否需要通过X批准退款不同的规则中使用的产品,等等等等
小子下来酒吧

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

7

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

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

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

看,如果周围有代码,那么显然会发生坏事。

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

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

玩具的问题是,值都是不寻常的,能够合理保证在代码中是独一无二的。不是“ 1”或“ 10”,而是“ 50,000”。不是“客户端”或“报告”,而是“ SR008-04X”。

稻草人是唯一的其他方式来解决impenetrably不透明常数的问题是蜂巢不送到一些不相关的服务的配置文件。

您可以一起使用这两个谬论来证明任何论点都是正确的。


2
不是玩具问题,不是稻草人。这是你会看到所有的时间在这些类型的业务应用。没有“开拓新市场”,也没有重复使用相同的数字(毕竟,无论如何这都会给它带来另一种含义),并且无论如何,该文章对DRY毫无用处-如果价值存在两个依赖关系,将被移到方法或常量中。显示这些常量(配置设置,实际上并不重要)应如何命名的示例,以及应在哪些方面存储这些常量,这些常量应以将来可以证明并且比代码更清楚的方式进行存储。
a安

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

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

2

这有几个问题。

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

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

这使得每个规则都有其自己的有效性,某些规则每年都会更改,某些更改将取决于已颁发许可或开具发票的时间。规则本身包含必须应用哪个版本的检查。

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

然后,列出所有规则并应用该列表。

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


2

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

像上面的示例一样,对外部指定的业务需求进行编码的代码应该确实存在于源树的不同部分(可能是命名的businesslogic或类似的名称),并且应注意确保其仅对业务需求进行简单,易读且可编码的编码。尽可能简明扼要,尽量减少样板,并提供清晰,有益的评论。

不应其与实现业务逻辑所需功能的“基础结构”代码混合,例如,attachDocument()示例中方法的实现,或者通常是UI,日志记录或数据库代码。虽然执行这种分离的一种方法是在配置文件中“软编码”所有业务逻辑,但这远非唯一(或最佳)方法。

这样的业务逻辑代码还应该足够清晰地编写,如果您向没有编码技能的业务领域专家展示了该代码,他们将能够理解它。至少,如果以及当业务需求发生变化时,对它们进行编码的代码应该足够清晰,以至于即使是一个事先不熟悉代码库的新程序员也应该能够轻松地定位,查看和更新​​业务逻辑,并假设从本质上讲,不需要任何新功能。

理想情况下,此类代码也将以特定于域的语言编写,以实现业务逻辑与底层基础结构之间的分隔,但是对于基本的内部应用程序而言,这可能不必要地变得复杂。就是说,如果您要将软件出售给多个需要各自定制的业务规则的客户,那么简单的特定于域的脚本语言(例如,可能基于Lua沙箱)就可以了。


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