失控

2021-01-23 15:43:34

关于范式,重构,控制流,数据,代码,二元性以及罗马数字曾经为我们做什么的文章

从不同的角度看事物可以揭示隐藏的一面。对于物理对象,此隐藏面可以是字面的,因此,为什么通常使用多视图投影(例如平面图和立面图)制作三维对象的技术图纸。对于抽象概念,隐藏的一面更具象征意义。例如,在软件架构中,可以使用视图模型(例如4 + 1或ODP视点)来关注系统的不同问题,例如用户交互,治理,行为,代码结构,信息模型,物理分布,基础设施等

数字系统也具有这种质量。改变数字的表示方式不会改变数字的表示方式,但是会改变我们对数字的看法。例如,考虑二进制中的整数,会发现与十进制中的整数(和失败)不同的模式(和失败)。二进制也更容易将其分块为十六进制。考虑到RGB三元组,十六进制比十进制更有意义。等等。不断变化的基础可以揭示新的可能性。

从位置系统(例如常见的印度-阿拉伯小数系统)到基于不同幅度值缩写的符号系统(符号在符号的符号意义上更确切地说是符号)而不是作为正/负指示符),例如罗马数字。在这两个数字系统之间的某个位置(乃至更广泛的范围内),我们发现算盘是一个双五进制编码的十进制系统(例如罗马数字),建立在一个更简单的符号系统上,但是在位置上进行组织(例如印度-阿拉伯系统)以实现快速算术。

在印度-阿拉伯系统广泛采用之前,罗马数字系统已在整个欧洲使用。与西罗马帝国统治时期相比,西方帝国陷落后的情况可能更加统一。

如果不是为了在文艺复兴之后到现代很长时间内坚持数字系统,那将是历史课的结束。这些天,您最可能会遇到罗马数字的地方包括老建筑,模拟钟面,日di,音乐和弦,BBC程序的版权年和程序员的编码katas。

标准格式可以舒适地表示1到3999(I到MMMCMXCIX),尽管有时可以表示4000或更高(MMMM为4000)。超出此范围的数字通常会被写为单词,并且通常会被近似。没有表示零的符号,甚至没有表示零的数字的概念,因此在中世纪的文字中,该数字将被写为nulla或缩写为N。而且,如果没有零,则肯定没有负数。

以下Python代码显示了一种将数字转换为相应的罗马数字字符串的方法:

def roman(number):结果=""而数字> = 1000:结果+ =" M"如果数字&=; = 900,则数字-= 1000:结果+ =" CM"如果数字&=; = 500,则数字-= 900:结果+ =" D"如果数字&=; = 400,则数字-= 500:结果+ =" CD"数字-= 400,而数字> = 100:结果+ =" C"如果数字> = 90,则数字-= 100:结果+ =" XC"数字-= 90(如果数字> = 50:结果+ =" L"数字-= 50(如果数字> = 40:结果+ =" XL"数字-= 40,而数字> = 10:结果+ =" X"数字-= 10(如果数字> = 9:结果+ =" IX"数字-= 9(如果数字> = 5:结果+ =" V"数字-= 5(如果数字> = 4:结果+ =" IV"数字-= 4,而数字> = 1:结果+ =" I"数字-= 1返回结果

抛开类型和范围验证的问题,此代码有效。我们可以通过推理,检查并针对以下测试用例样本找到对此主张的支持:

case = [#小数位对应于数字[1," I"],[10," X"],[100," C"],[1000 ," M"],#五进制间隔对应于数字[5," V"],[50," L"],[500," D"],#个十进制数字的整数连接[2," II"],[30," XXX"],[200," CC"] ,[3000," MMM"],#非十进制小数以降序连接[6," VI"],[23," XXIII"], [273," CCLXXIII"],[1500," MD"],#数个前任是减法的[4," IV"],[9,&#34 ; IX"],[40," XL"],[90," XC"],[400," CD"],#个相减的前任串联在一起[14," XIV"],[42," XLII"],[97," XCVII"],[1999," MCMXCIX" ]] failures = [[数字,期望,罗马(数字)],如果期望的话,则期望!= roman(number)]断言失败== [],str(失败)

因此,可以肯定的是,该功能有效,但效果并不理想。这是只有企业程序员才能喜欢的代码。或者,也许是Pascal程序员:Kathleen Jensen和Niklaus Wirth在《 Pascal:用户手册和报告》中使用了这种繁琐的控制流方法。鉴于Pascal通常被视为一种学习良好实践的语言,因此可以认为这是具有讽刺意味的。

罗马函数的代码具有很高的过程性,因为它是无懈可击的命令,并且面向控制流。但是,即使作为一种程序解决方案,也不是一个特别好的解决方案。它是重复的,笨拙的并且缺乏抽象性。

但是,如果我们将此代码视为垫脚石,而不是最终状态,那么它将成为一个示例和机会(尤其是在存在测试的情况下),而不是一个反例和死胡同。这种宽容也许是我们应该扩展到大多数代码的慷慨之举。除非像罗马人在建筑物上凿凿单词和数字一样,将代码固定在石头上,否则最好将代码视为正在进行中的工作。

在渴望追求完美的人们的眼中,一项工作从未真正完成过-对他们来说这是没有道理的-而是被放弃了。而且,由于厌倦或需要将其交付出版而将书抛向火中或向公众公开,是一种偶然的事件,相当于松散了已经很累的想法或令人讨厌的是,人们对此失去了所有兴趣。

循环结构通常是了解可以抽象的内容的良好起点。乍一看,一段有节奏的节,然后是三个if语句,看起来像是一个很好的支点,可以用来重构:

而数字> = 1000:结果+ =" M"如果数字&=; = 900,则数字-= 1000:结果+ =" CM"如果数字&=; = 500,则数字-= 900:结果+ =" D"如果数字&=; = 400,则数字-= 500:结果+ =" CD"数-= 400

此片段在1000、900、500和400中的结构在100、90、50和40中重复,然后在10、9、5和4中重复。但是,基于这种重复进行的重构错过了更深的重复,因此无法统一。

首先考虑一下什么是while语句?这是一条由条件控制的语句,执行次数为零到很多次。那么,if语句是什么?这是一条由条件控制的语句,执行零次或一次。恰到好处地斜着看,一个if可以看作是一段时间的有限情况。

查看所涉及的特定数字和操作,我们看到以下仅while代码等效于while和if代码的先前混合:

而数字> = 1000:结果+ =" M"数字-= 1000,而数字> = 900:结果+ =" CM"数字-= 900,而数字> = 500:结果+ =" D"数字-= 500,而数字> = 400:结果+ =" CD"数-= 400

新创建的while语句将执行零次或一次,就像它们的前提一样。行为没有改变,但是我们对问题的理解以及我们要重构的事物的形状和性质发生了巨大变化。现在我们有十三个循环,看起来像

对解决方案最重要的是一系列阈值及其对应的字母。这不是控制流问题:它是数据问题。我们需要数据结构,而不是控制结构:

def roman(number):数字= [[1000," M"],[900," CM"],[500," D"],[400 ," CD"],[100," C"],[90," XC"],[50," L"], [40," XL"],[10," X"],[9," IX"],[5," V" ],[4," IV"],[1," I"]]结果=""对于除数,数字中的字母:result + =(数字//除数)*字母数字%=除数返回结果

此版本仍然符合程序性要求,但比起绝对必要的第一个版本更具声明性。

数据驱动方法将数据与由数据驱动的代码分开,从而使意图和结构都更加清晰。 Niklaus Wirth指出算法+数据结构=程序,但是算法和数据结构不一定是平等的伙伴。正如弗雷德·布鲁克斯(Fred Brooks)所指出的那样,在“神话人物月”中,在“表示形式”标题下是编程的本质:

有时,战略突破将是一种新算法[…]。通常,战略突破将来自重做数据或表的表示。这就是程序的核心所在。告诉我您的流程图并隐藏您的表格,我将继续感到困惑。告诉我您的表格,通常不需要您的流程图;他们很明显。

我们更可能直接对控制流进行编码,而不是对其进行图表处理,但这可以凸显出,尽管编程领域的某些事情发生了变化,但让数据进行交流并没有什么新意。从查找表到表驱动的代码,这一系列方法可以在许多范式和上下文中找到表达式-例如,已经显示了针对罗马的数据驱动测试-但常常被忽略。这种思维方式要么没有被完全明确地讲授,要么在诸如Pascal之类的能力较弱的语言中,总是不能方便地表达出来。

我们还可以做更多的工作来探索罗马数字问题的算法空间和范式形状,例如将循环转换为折叠操作(在Python中简化),或者消除算术并使用术语重写表示解决方案(转换为一元代码,然后使用replace),但现在我们将控制流保留在原处,以便我们可以从另一个方向出发探索代码元素的组织。

由于数字表不变,并且与number参数无关,因此我们可以实现不变的代码运动以将其提升到函数之外:

数字= [[1000,"],[900," CM"],[500," D"],[400," CD& #34;],[100," C"],[90," XC"],[50," L"],[40,&#34 ; XL"],[10," X"],[9," IX"],[5," V"],[4,& #34; IV"],[1," I"]] def roman(number):结果=""对于除数,数字中的字母:result + =(数字//除数)*字母数字%=除数返回结果

我们可以通过将数字的定义放入另一个源文件digits.py中来进一步增加这种距离:

数字= [[1000,"],[900," CM"],[500," D"],[400," CD& #34;],[100," C"],[90," XC"],[50," L"],[40,&#34 ; XL"],[10," X"],[9," IX"],[5," V"],[4,& #34; IV"],[1," I"]]

从数字导入数字def roman(数字):结果=""对于除数,数字中的字母:result + =(数字//除数)*字母数字%=除数返回结果

也许不是针对这个特定问题,但这是一个有趣的解耦,因为-只要数据的结构是一致的-它使我们能够独立于算法来更改实际数据。我们正在使用numerics.py中的Python作为数据语言,实际上,它使digits.py成为本机数据库。另一种看待这种分离的方式是roman.py中的代码实现了针对digits.py的高度特定于域的数据语言的解释器。

这导致我们要思考代码和数据之间的区别,而不是计算机科学历史上的第一次或最后一次。即使在最简单的情况下,我们也遇到了这个问题。您如何描述上面的重构转换?许多人将数据与代码分离来描述它们。那么,这是否意味着digits.py包含数据但不包含代码?这是一个有效的Python模块,可将变量初始化为字符串-整数对的列表。听起来像代码。没有什么说要被认为是代码的必要条件是控制流的存在。

我们自由地使用“代码”一词,既指用编程语言编写的任何内容,更具体地说,指的是主要关注算法和运算而不是数据结构和定义的代码(原文如此)。自然语言是如此混乱,充满了歧义,句法和上下文依赖。

如果我们想更加严格,可以说我们已经将代码分为抽象化操作的代码和抽象化数据的代码。换句话说,我们说的是程序=代码,并且假设算法+数据结构=程序,因此算法+数据结构=代码。这可能是方便而清晰的方式来构架我们的思想并描述我们所做的事情。但是,我们还需要认识到,仅仅是这样:它是一种思维工具,一种看待事物并对其进行推理的方式,而不必评论这些事物的内在本质。它是一种描述工具,是一种将抽象概念更具体地呈现到对话中的方法。

如果我们将事物的本质的观点弄混了,我们最终会感到二分法,就像笛卡尔二元论。正如笛卡尔声称存在两种不同的物质,即物理的和精神的,我们最终可以声称存在两种截然不同的代码,即数据代码和操作代码。

当我们关注硬件,编译器或计算机科学的基础(例如图灵机)时,我们不会为此类分离找到明确的支持或严格的界限。歧义深入。尽管我们在过程地址中有代码段和数据段,但这些段强制执行约定和保护的可商谈事项(例如,代码段或文本段通常是只读的)。代码段和数据段都包含数据,但是代码段中的数据旨在通过预定义期望和指令集的过滤器来理解。另一方面,也可以将数据段中的数据视为要执行的内容。

Lisp编程语言的基本思想是,所有内容都可以表示为列表,包括代码在内都可以作为列表进行操作。 Lisp的观点可以概括为“数据结构=程序”。对于某些语言,源代码是供编译器使用的数据,而编译器又会为物理或虚拟机生成数据以供执行。对于其他语言,例如Python,源代码是可以更直接地解释和执行的字符串。在示例中,我们可以明确地模糊代码-数据边界,如下所示:

来源=""" [[1000," M"],[900," CM"],[500," D&# 34;],[400," CD"],[100," C"],[90," XC"],[50," L"],[40," XL"],[10," X"],[9," IX"],[5,&# 34; V"],[4," IV"],[1," I"]]""数字= eval(源)

我们在这里要解决的二元论不是对本质上分离的事物的严格分类,而是对本来就捆绑在一起的事物的二元论。我们强迫竞争的阴阳互补的观点,但它们以猫的状态存在,并且容易以另一种方式解决。我们看到或选择的是观察问题,欲望和当前问题,而不是人工制品。

不用涉及中间变量和转换,我们可以简单地用一个仅包含数据表达式而不包含语句的文件替换digits.py:

[[1000,"],[900," CM"],[500," D"],[400," CD&#34 ;],[100," C"],[90," XC"],[50," L"],[40," XL& #34;],[10," X"],[9," IX"],[5," V"],[4,&#34 ; IV"],[1," I"]]

尽管是有效的Python表达式,但它不再是有用的Python模块。数据未绑定到可在其他位置引用的变量。但是,它确实与更广泛认可的数据语言JSON非常相似。

使用open(" numerals.json")作为源:数字= eval(source.read())def罗马(数字):result =""对于除数,数字中的字母:result + =(数字//除数)*字母数字%=除数返回结果

如果您想知道到目前为止为什么我在代码中避免使用元组和单引号字符串,那么您将得到答案:以正确的方式看,JSON几乎是Python的适当子集。 (如果没有,就不用担心。就像以前一样。)如果用以下内容替换以前的eval表达式,则关系会更紧密,观察更真实:

当然,这不仅使有效的JSON通过,还可以使您感到有理由感到不舒服。我们可以通过使用更受约束和更具体的内容来代替许可的和普遍的评估,从而更充分地认识和维护这种转变:

查看上面代码的进展,从第一个表格版本发展到最终的JSON版本,在转换的什么时候该表停止成为代码并开始成为配置?

从某种程度上讲,这是一个棘手的问题,但这也是重点。我们没有二元论,歧义和观点。该问题旨在突出一个常见的盲点和监督:配置就是代码。将配置与代码概念脱节,将我们引向错误的方向。将其视为其他事物通常会导致将其视为次要事物。

那么,配置是什么?它是一个正式的结构,用于指定软件应如何运行。听起来像代码。如果软件系统的运行不符合预期,则配置是以键值对还是图灵完备的语言定义,使用临时专有二进制格式还是使用广泛使用且公认的基于文本的格式都无所谓,我们认为这是一个问题。该问题是否源于JSON有效负载,注册表设置,数据库,环境变量或源代码均无关紧要:该软件被视为无法正常工作-有一个错误需要修复。

错误配置的后果包括因个人不便而导致的不便,即在应用程序更新时浪费您的设置,以及因火箭发射失败而造成的损失更大。配置不亚于软件系统的任何其他方面。但是,其常见的二等公民身份使其受到的尊重和可见度降低,从而导致大量潜在的配置错误。如果我们将其视为代码,则我们更有可能考虑版本控制,测试,审阅,设计,验证,可维护性以及我们通常在代码库其他部分授予的其他质量和实践,但可能会忽略配置。

无论您是发现自己使用罗马数字探索TDD,还是使用Gilded Rose重构套件,将复杂的约束条件转换为代码,还是试图破解传统逻辑缠结的代码,从而通过resp来解决问题

......