2023年5月

Rust 项目再次陷入领导力危机

继两年前 Rust 审核团队宣布立即辞职后,Rust 项目的管理就一直处于跛脚运行之中。最近,Rust 项目又卷入了 RustConf 主题演讲争议。RustConf 2023 主动邀请了 JeanHeyd Meneide 发表主题演讲,他是 C 语言官方标准的两位编辑之一,管理着许多重要的大型开源项目。但在他花费了大量时间准备了演讲《编译时编程(可能)的未来》后,Rustconf 以其演讲“不被 Rust 项目认可”,担心该演讲被认为是官方方向的理由,而通知他将主题演讲降为普通演讲。但实际原因被推测为他曾经分享过一些认为 Rust 应该反思的内容,Rust 管理层内有人反感他。他认为这是一次侮辱,随即 宣布 拒绝出席 RustConf。此事还导致推荐他发表演讲的 Rust 核心成员愤而辞职。在此事冲上热搜后,Rust 项目发表 声明 对此事公开道歉,称它的决策和沟通流程出现了问题,开始启动两年来迟迟未决的新的治理委员会。

消息来源:The PHD
老王点评:一个成功的开源项目,需要广泛的社区决策,也需要一个定海神针般的领袖人物,Linux 如此、Python 如此。而 Rust 社区却没有一个灵魂人物,我们只能看到各种内部斗争和自行其是,这样的风格,即便成立治理委员会,我认为也很难说有未来。

英伟达成为第一家万亿芯片公司

英伟达周二成为第一家市值突破 1 万亿美元的芯片公司。此前进入万亿美元俱乐部的公司包括苹果、微软、Alphabet/谷歌、亚马逊和 Meta。AI 的繁荣催动了英伟达的股价上周一度飙升 24%,接近万亿美元,本周二开盘短时突破这一数字。

消息来源:The Verge
老王点评:热点常有,而芯片永存。无论是区块链、游戏还是 AI,这都让芯片公司赚得盆满钵满。

ChatGPT 为律师编写的法庭文件杜撰了六个不存在的案例

尽管我们知道人工智能会煞有介事的杜撰一些“事实”,但是一位律师在使用 ChatGPT 帮助编写法庭文件时,AI 居然杜撰了六个不存在的案例,并且还信誓旦旦地给出了法律来源,保证了其可靠性。律师表示,没有确认 ChatGPT 提供的来源是他的错。法官称这是“前所未有的”,并考虑对该律师进行惩罚。

消息来源:Ars Technica
老王点评:在 AI 出错时,你对它提出的批评它往往会听从。但如果你指出它的“事实性”错误时,它往往是嘴硬的,所以你还是得依靠“古典的”搜索引擎来确认事实。

人工智能有“灭绝(人类)的危险”

非营利组织“ 人工智能安全中心 Center for AI Safety ”预计将发布一份只有一句话的声明:“减轻人工智能带来的灭绝风险应与其他社会规模的风险,如大流行病和核战争一样,成为全球优先事项。”这封公开信由 350 多名从事人工智能工作的高管、研究人员和工程师签署,签署者包括三家领先的人工智能公司 OpenAI、谷歌 DeepMind 和Anthropic 的 CEO,也包括除了 Yann LeCun 之外的人工智能领域另外两位图灵奖得主。

消息来源:纽约时报
老王点评:这个时候,我们应该相信他们真的放出来一只不得了的巨兽。但是我觉得,倡议是徒劳的,或许能引起全社会的关注,但是如果没有切实的利益机制制约,早晚会无可遏制。

人工智能意味着每个人现在都可以成为程序员

在台北举行的 Computex 论坛上,英伟达 CEO 黄仁勋对数千人说,人工智能正在引领一场计算革命。他在演讲中说:“毫无疑问,我们正处于一个新的计算时代”。他表示,人工智能意味着每个人现在都可以成为计算机程序员,因为他们需要做的只是与计算机对话,并为 “数字鸿沟” 的结束欢呼。

消息来源:路透社
老王点评:程序员可能就会像以前的手工业者们一样,绝大部分都会消失,只在偏僻的角度零星留下几个来修复一些遗留系统。

英特尔将在其最新 CPU 中加入人工智能处理芯片

英特尔称,将在其即将推出的第 14 代酷睿处理器 Meteor Lake CPU 的所有型号中加入 “视觉处理单元” VPU。如今,CPU 和 GPU 承担着繁重的工作,但它们将被人工智能工作负载的需求所淹没。将这些工作转移到云端是昂贵的,也是不切实际的,因为买家希望个人电脑有良好的性能,因此,VPU 将作为一种 SoC 出现在 CPU 中。CPU 仍将被要求做低延迟的简单推理工作,GPU 将被用来做涉及并行和吞吐量的工作,而其他与人工智能相关的工作将被卸载到 VPU 上。

消息来源:The Register
老王点评:整个世界仍像一辆没有刹车的卡车,奔向了人工智能的未来。

回音

我正在制作一份有关计算机以二进制表示事物的小册子,有人问我一个问题 - 为什么 x86 架构使用 8 位字节?为什么不能是其他大小呢?

对于类似这样的问题,我认为有两种可能性:

  • 这是历史原因造成的,其他尺寸(如 4、6 或 16 位)同样有效。
  • 8 位是客观上的最佳选择,即使历史发展不同,我们仍然会使用 8 位字节。
  • 一些混合 1 和 2 的因素。

我对计算机历史并不是非常着迷(与阅读计算机文献相比,我更喜欢使用计算机),但我总是很好奇计算机事物今天的方式是否存在本质原因,或者它们大多是历史偶然的结果。因此,我们将谈论一些计算机历史。

作为历史偶然性的一个例子:DNS 有一个 class 字段,它有 5 种可能的值(internetchaoshesiodnoneany)。 对我来说,这是一个明显的历史意外的例子 - 如果我们今天重新设计 DNS 而不必担心向后兼容性,我无法想象我们会以相同的方式定义类字段。我不确定我们是否会使用 class 字段!

这篇文章没有明确的答案,但我在 Mastodon 上提问,并找到了一些潜在的 8 位字节原因。我认为答案是这些原因的某种组合。

字节和字有什么区别?

首先,本文中经常提到 “ 字节 byte ” 和 “ word ”。它们有什么区别?我的理解是:

  • 字节的大小 是你可以寻址的最小单元。例如,在我的计算机上,程序中的 0x20aa87c68 可能是一个字节的地址,然后 0x20aa87c69 是下一个字节的地址。
  • 字的大小 是字节大小的某个倍数。我对此困惑了多年,维基百科的定义非常模糊(“字是特定处理器设计使用的自然数据单元”)。我最初认为字大小与寄存器大小相同(在 x86-64 上为 64 位)。但是根据 英特尔架构手册 的第 4.1 节(“基本数据类型”),在 x86 上,虽然寄存器是 64 位的,但一个字是 16 位的。因此我困惑了 —— 在 x86 上,一个字是 16 位还是 64 位?它可以根据上下文而有不同的含义吗?这是怎么回事?

现在让我们来讨论一些使用 8 位字节的可能原因!

原因 1:将英文字母适配到 1 字节中

维基百科文章 表示 IBM System/360 于 1964 年引入了 8 位字节。

在管理该项目的 Fred Brooks 的一段 视频采访 中,他讲述了原因。以下是我转录的一些内容:

…… 6 位字节在科学计算中确实更好,而 8 位字节则更适合商业计算,每个字节都可以针对另一个字节进行调整,以使两种字节互相使用。

因此,这变成了一个高管决策,我决定根据 Jerry 的建议采用 8 位字节。

……

我在我的 IBM 职业生涯中做出的最重要的技术决策是为 360 选择 8 位字节。

我相信字符处理将变得重要,而不是十进制数字。

使用 8 位字节处理文本很有道理:2 6 为 64,因此 6 位不足以表示小写字母、大写字母和符号。

为了使用 8 位字节,System/360 还引入了 EBCDIC 编码,这是一种 8 位字符编码。

接下来在 8 位字节历史上重要的机器似乎是 英特尔 8008,它设计用于计算机终端(Datapoint 2200)。终端需要能够表示字母以及终端控制代码,因此使用 8 位字节对其来说很有意义。计算机历史博物馆上的 Datapoint 2200 手册 在第 7 页上说 Datapoint 2200 支持 ASCII(7 位)和 EBCDIC(8 位)。

为什么 6 位字节在科学计算中更好?

我对这条 “6 位字节在科学计算中更好” 的评论很好奇。以下是 Gene Amdahl 的一段采访摘录

我原本希望采用 24 和 48 而非 32 和 64,因为这将为我提供一个更合理的浮点系统。因为在浮点运算中,使用 32 位字大小时,你必须将指数保持在 8 位中用于指数符号,并且要使其在数字范围上合理,你必须每次调整 4 个位而不是单个位。因此,这将导致你比使用二进制移位更快地失去一些信息。

我完全不理解这条评论 - 如果你使用 32 位字大小,为什么指数必须是 8 位?如果你想要,为什么不能使用 9 位或 10 位?但这是我在快速搜索中找到的全部内容。

为什么大型机使用 36 位?

与 6 位字节相关的问题是:许多大型机使用 36 位字大小。为什么?在维基百科的 36 位计算 文章中有一个很好的解释:

在计算机问世之前,即需要高精度科学和工程运算的领域,使用的是十位数码电动机械计算器……这些计算器每位数码均有一个专用按键,操作人员在输入数字时需要用到所有手指,因此,虽然有些专业计算器有更多位数码,但这种情况是个实际的限制。

因此,早期针对相同市场的二进制计算机通常使用 36 位字长度。这足以表示正负整数最高精度到十位数字(最小应为 35 位)。

因此,这种 36 位大小似乎是基于

的,它等于 34.2。嗯。

我猜这个原因是在 50 年代,计算机非常昂贵。因此,如果您想要你的计算机支持十位十进制数字,你将设计它恰好具有足够的位来执行此操作,而不会更多。

现在计算机更快更便宜,因此,如果您想要出于某种原因表示十位十进制数字,你只需使用 64 位即可 - 浪费一点空间通常并不会有太大问题。

还有人提到,一些具有 36 位字大小的计算机可以让你选择字节大小 - 根据上下文,你可以使用 5 或 6 或 7 或 8 位字节。

原因 2:与二进制编码的十进制一起工作

20 世纪 60 年代,有一种流行的整数编码叫做 二进制编码的十进制 binary-coded decimal (缩写为 BCD),它将每个十进制数字编码为 4 位。

例如,如果你想要编码数字 1234,在 BCD 中,它会是这样的:

0001 0010 0011 0100

因此,如果你想要能够轻松地与二进制编码的十进制一起工作,你的字节大小应该是 4 位的倍数,比如 8 位!

为什么 BCD 很流行?

这个整数表示方法对我来说真的很奇怪 —— 为什么不用更有效率的二进制来存储整数呢?在早期的计算机中,效率非常重要!

我最好的猜测是,早期的计算机没有像我们现在这样的显示器,所以一个字节的内容被直接映射到开关灯上。

这是来自维基百科一个带有一些亮灯的 IBM 650 显示器的图片(CC BY-SA 3.0 许可):

因此,如果你想让人们能够相对容易地从二进制表示中读取十进制数,这样做就更有意义了。我认为,今天 BCD 已经过时了,因为我们拥有显示器,并且我们的计算机可以将用二进制表示的数字转换为十进制,并显示它们。

此外,我想知道,“ 四位 nibble ”(意为 “4 位”)这个词是不是来自 BCD 的。在 BCD 的上下文中,你经常会引用半个字节(因为每个十进制数字是 4 位)。所以有一个 “4 位” 的词语是有意义的,人们称 4 个位为 “ 四位 nibble ”。今天,“四位” 对我来说感觉像是一个古老的词汇,除了作为一个趣闻我肯定从未使用过它(它是一个很有趣的词!)。维基百科关于 “四位” 的文章支持了这个理论:

“四位” 用来描述存储在 IBM 大型计算机中打包的十进制格式(BCD)中数字的位数。

还有一个人提到 BCD 的另一个原因是 金融计算。今天,如果你想存储美元金额,你通常只需使用整数的分数,然后在需要美元部分时除以 100。这没什么大不了的,除法很快。但显然,在 70 年代,将一个用二进制表示的整数除以一个 100 是非常慢的,所以重新设计如何表示整数,以避免除以 100 是值得的。

好了,关于 BCD 就说这么多。

原因 3:8 是 2 的幂?

许多人说,CPU 的字节大小是 2 的幂次方很重要。我无法确定这是真的还是假的,而且我对 “计算机使用二进制,所以 2 的幂次方很好” 这种解释感到不满意。这似乎非常合理,但我想深入探讨一下。而且从历史上看,肯定有很多使用字节大小不是 2 的幂次方的机器,例如(来自这个来自 Stack Exchange 上复古计算版块的 帖子):

  • Cyber 180 大型机使用 6 位字节
  • Univac 1100/2200 系列使用 36 位字长
  • PDP-8 是一台 12 位计算机

一些我听到的关于 2 的幂次方很好的原因我还没有理解:

  • 一个单词中的每个位都需要一个总线,而你希望总线数量是 2 的幂次方(为什么?)
  • 很多电路逻辑容易针对分而治之的技术(我需要一个例子来理解这个)

对我更有意义的原因是:

  • 它使设计“时钟分频器”更容易,这些分频器可以测量“在这条线路上发送了 8 位”,分别基于减半进行操作 - 你可以将 3 个减半时钟分频器串联起来。Graham Sutherland 告诉我这个,他制作了这个非常酷的 分频器模拟器,展示了这些分频器的工作原理。该网站(Falstad)还有很多其他示例电路,似乎是制作电路模拟器的一个非常酷的方式。
  • 如果你有一个指令可以将字节中的特定位清零,则如果你的字节大小为 8(2 的 3 次方),你可以只使用 3 位指令来指示哪一位。x86 似乎没有这样做,但 Z80 的位测试指令 是这样做的。
  • 有人提到一些处理器使用 进位前瞻加法器,它们按 4 位分组。经过一些快速的谷歌搜索,似乎有各种各样的加法器电路。
  • 位图:你计算机的内存被组织成页(通常大小为 2 的 n 次方)。它需要跟踪每一页是否空闲。操作系统使用位图来完成这项工作,其中每个位对应一页,并且根据页面是空闲还是占用,值为 0 或 1。如果你有一个 9 位的字节,你需要除以 9 来在位图中找到你要查找的页面。除以 9 的速度比除以 8 慢,因为除以 2 的幂次方总是最快的。

我可能很糟糕地扭曲了其中一些解释:在这里,我非常超出了自己的知识领域。我们继续前进吧。

原因 4:小字节大小很好

你可能会想:好吧,如果 8 位字节比 4 位字节更好,为什么不继续增加字节大小呢?我们可以有 16 位字节啊!

有几个保持字节大小较小的理由:

  • 它是一种空间浪费 —— 字节是你可以寻址的最小单位,如果你的计算机存储了大量的 ASCII 文本(只需要 7 位),那么每个字符分配 12 或 16 个位相当浪费,而你可以使用 8 个位代替。
  • 随着字节变得越来越大,你的 CPU 需要变得更复杂。例如,你需要每个位线路一条总线线路。因此,我想简单总是更好。

我对 CPU 架构的理解非常薄弱,所以就说到这里吧。对我来说,“这是一种空间浪费” 的理由似乎相当有说服力。

原因 5:兼容性

英特尔 8008(1972 年)是 8080(1974 年)的前身,8080 是第一款 x86 处理器 8086(1976 年)的前身。似乎 8080 和 8086 很受欢迎,这就是我们现代 x86 计算机的来源。

我认为这里有一个 “如果它好好的就不要动它” 的问题 - 我假设 8 位字节功能良好,因此英特尔看不到需要更改设计的必要性。如果你保持相同的 8 位字节,那么你可以重复使用更多指令集。

此外,80 年代左右我们开始出现像 TCP 这样的网络协议,它们使用 8 位字节(通常称为“ 八位组 octet ”),如果你要实现网络协议,你可能希望使用 8 位字节。

就这些!

在我看来,8 位字节的主要原因是:

  • 很多早期的电脑公司都是美国的,美国使用最广泛的语言是英语
  • 这些人希望计算机擅长文本处理
  • 较小的字节大小通常更好
  • 7 位是你可以用来容纳所有英文字母和标点符号的最小尺寸
  • 8 比 7 更好(因为它是 2 的幂次方)
  • 一旦有得到成功应用的受欢迎的 8 位计算机,你希望保持相同的设计以实现兼容性。

有人指出 这本 1962 年的书 第 65 页谈到了 IBM 选择 8 位字节的原因,基本上说了相同的内容:

  1. 其完整的 256 个字符的容量被认为足以满足绝大多数应用程序的需要。
  2. 在该容量范围内,单个字符由单个字节表示,因此任何特定记录的长度并不因该记录中字符而异。
  3. 8 位字节在存储空间上是相当经济的。
  4. 对于纯数字工作,一个十进制数字只需要 4 个比特表示,两个这样的 4 位字节可以打包成一个 8 位字节。尽管这种数字数据包装不是必需的,但为了提高速度和存储效率,它是一种常见做法。严格来说,4 位字节属于不同的代码,但与 4 位及 8 位方案相比,它们的简单性导致了更简单的机器设计和更清晰的寻址逻辑。
  5. 4 位和 8 位的字节大小,作为 2 的幂次方,允许计算机设计师利用二进制寻址和位级索引的强大功能(见第 4 章和第 5 章)。

总的来说,如果你在英语国家设计二进制计算机,选择 8 位字节似乎是一个非常自然的选择。

(题图:MJ/3526a0d5-bee5-4678-8637-e96e9843b53c)


via: https://jvns.ca/blog/2023/03/06/possible-reasons-8-bit-bytes/

作者:Julia Evans 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

完善你的电商应用,使它能够正确处理业务规则。

在前面的文章中,我已经解释了为什么将编程问题看作一整群丧尸来处理是错误的。我用 ZOMBIES 方法来解释为什么循序渐进地处理问题更好。

ZOMBIES 表示以下首字母缩写:

  • Z – 最简场景(Zero)
  • O – 单元素场景(One)
  • M – 多元素场景(Many or more complex)
  • B – 边界行为(Boundary behaviors)
  • I – 接口定义(Interface definition)
  • E – 处理特殊行为(Exercise exceptional behavior)
  • S – 简单场景用简单的解决方案(Simple scenarios, simple solutions)

在系列的前三篇文章中,我展示了 ZOMBIES 方法的前五项。第一篇中 实现了最简场景,它为代码提供了最简可行路径。第二篇文章中执行了 单元素场景和多元素场景上的测试。第三篇中介绍了 边界和接口。在本文中,我将带你了解倒数第二个方法:处理特殊行为。

处理特殊行为

在开发一个电子购物应用时,你需要从产品负责人或赞助商那里了解需要采用什么销售策略。

毫无疑问,与任何电子商业活动一样,你需要通过制定销售策略来诱导顾客进行消费。假设有如下的销售策略:订单金额超过 ¥500 时可以享受一定的折扣优惠。

现在将这个销售策略转换为可运行期望:

[Fact]
public void Add2ItemsTotal600GrandTotal540() {
        var expectedGrandTotal = 540.00;
        var actualGrandTotal = 0.00;
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

这个正面样例表示的销售策略是,如果订单总额为 ¥600.00,那么 shoppingAPI 会将其减价为 ¥540.00。上面的代码伪造了一个失败验证用例。现在修改它,让它能够通过测试:

[Fact]
public void Add2ItemsTotal600GrandTotal540() {
        var expectedGrandTotal = 540.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 200.00);
        shoppingAPI.AddItem(item);
        Hashtable item2 = new Hashtable();
        item2.Add("00000002", 400.00);
        shoppingAPI.AddItem(item2);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

在这个正样例中,你向购物框加入一件价值 ¥200 的商品和一件价值 ¥400 的商品,使总价达到 ¥600 。当调用 CalculateGrandTotal() 方法时,你期望总价是 ¥540。

这个微测试能够通过吗?

[xUnit.net 00:00:00.57] tests.UnitTest1.Add2ItemsTotal600GrandTotal540 [FAIL]
  X tests.UnitTest1.Add2ItemsTotal600GrandTotal540 [2ms]
  Error Message:
   Assert.Equal() Failure
Expected: 540
Actual: 600
[...]

很可惜,它失败了。你期望的结果是 ¥540,但计算结果为 ¥600。为什么会这样呢?那是因为你还没有告诉系统在订单总价大于 ¥500 时怎么进行折扣计算。

现在来实现折扣计算逻辑。根据上面的正样例可知,当订单总价为 ¥600(超过了营销策略的阈值 ¥500)时,期望的最终总价为 ¥540。也就是说系统需要从订单总额中减去 ¥60。刚好是是原订单总价的 10%。因此该销售规则就是当订单总额超过 ¥500 时享受九折优惠。

ShippingAPI 类中实现该处理逻辑:

private double Calculate10PercentDiscount(double total) {
        double discount = 0.00;
        if(total > 500.00) {
                discount = (total/100) * 10;
        }
        return discount;
}

首先,检查订单总额是否大于 ¥500 。如果是,则计算出总额的 10%。

你还需要告诉系统怎么从订单总额中减去 10%。改动非常直接:

return grandTotal - Calculate10PercentDiscount(grandTotal);

到此,所有测试都能够通过。你又一次享受到系统处于稳态的欢愉。你的代码通过处理特殊行为实现了需要的销售策略。

最后一步

现在我已经介绍完 ZOMBIE 了,只剩下 S 了。我将会在最后一篇中介绍它。

(题图:MJ/7f8bf5d2-54ce-4d6e-9dbf-13abf6df966a)


via: https://opensource.com/article/21/2/exceptional-behavior

作者:Alex Bunardzic 选题:lujun9972 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

丧尸是没有边界感的,需要为你的软件设定限制和期望。

丧尸没有边界感。它们踩倒栅栏,推倒围墙,进入不属于它们的地盘。在前面的文章中,我已经解释了为什么把所有编程问题当作一群丧尸一次性处理是错误的。

ZOMBIES 代表首字母缩写:

  • Z – 最简场景(Zero)
  • O – 单个元素场景(One)
  • M – 多个元素场景(Many or more complex)
  • B – 边界行为(Boundary behaviors)
  • I – 接口定义(Interface definition)
  • E – 处理特殊行为(Exercise exceptional behavior)
  • S – 简单场景用简单的解决方案(Simple scenarios, simple solutions)

在本系列的前面两篇文章中,我演示了 ZOMBIES 方法的前三部分:最简场景、单元素场景和多元素场景。第一篇文章 实现了最简场景,它提供了代码中的最简可行路径。第二篇文章中针对单元素场景和多元素场景 运行测试。在这篇文章中,我将带你了解边界和接口。

回到单元素场景

要想处理边界,你需要绕回来(迭代)。

首先思考下面的问题:电子商务的边界是什么?我需要限制购物框的大小吗?(事实上,我不认为这有任何意义。)

目前唯一合理的边界条件是确保购物框里的商品数量不能为负数。将这个限制表示成可运行的期望:

[Fact]
public void Add1ItemRemoveItemRemoveAgainHas0Items() {
        var expectedNoOfItems = 0;
        var actualNoOfItems = -1;
        Assert.Equal(expectedNoOfItems, actualNoOfItems);
}

这就是说,如果你向购物框里添加一件商品,然后将这个商品移除两次,shoppingAPI 的实例应该告诉你购物框里有零个商品。

当然这个可运行期望(微测试)不出意料地会失败。想要这个微测试能够通过,最小改动是什么呢?

[Fact]
public void Add1ItemRemoveItemRemoveAgainHas0Items() {
        var expectedNoOfItems = 0;
        Hashtable item = new Hashtable();
        shoppingAPI.AddItem(item);
        shoppingAPI.RemoveItem(item);
        var actualNoOfItems = shoppingAPI.RemoveItem(item);
        Assert.Equal(expectedNoOfItems, actualNoOfItems);
}

这个期望测试依赖于 RemoveItem(item) 功能。目前的 shippingAPI 还不具备该功能,你需要增加该功能。

回到 app 文件夹,打开 IShippingAPI.cs 文件,新增以下声明:

int RemoveItem(Hashtable item);

ShippingAPI.cs 中实现该功能:

public int RemoveItem(Hashtable item) {
        basket.RemoveAt(basket.IndexOf(item));
        return basket.Count;
}

运行,然后你会得到如下错误:

 title=

系统在移除一个不在购物框的商品,这导致了系统崩溃。加一点点 防御式编程defensive programming

public int RemoveItem(Hashtable item) {
        if(basket.IndexOf(item) >= 0) {
                basket.RemoveAt(basket.IndexOf(item));
        }
        return basket.Count;
}

在移除商品之前先检查它是否在购物框中。(你可能试过用捕获异常的方式来处理,但是我认为上面的处理方式更具可读性。)

更多具体的期望

在讲更多具体的期望之前,让我们先探讨一下什么是接口。在软件工程中,接口表示一种规范,或者对能力的描述。从某种程度上来说,接口类似于菜谱。它罗列出了制作蛋糕的原材料,但它本身并不能吃。我们只是按照菜谱上的说明来烤蛋糕。

与此类似,我们首先通过说明这个服务能做什么的方式来定义我们的服务。这个描述说明就是所谓的接口。但是接口本身并不能向我们提供任何功能。它只是指导我们实现指定功能的蓝图而已。

到目前为止,我们已经实现了接口(只是某部分实现了,稍后还会增加新功能)和业务处理边界(也就是购物框里的商品不能是负数)。你指导了 shoppingAPI 怎么向购物框添加商品,并通过 Add2ItemsBasketHas2Items 测试验证了该功能的有效性。

然而仅仅具备向购物框添加商品的功能还不足以使其成为一个网购应用程序。它还需要能够计算购物框里的商品的总价。现在需要增加另一个期望。

按照惯例,从最直接明了的期望开始。当你向购物框里加入一件价值 ¥10 的商品时,你希望这个购物 API 能正确地计算出总价为 ¥10。

第五个测试(伪造版)如下:

[Fact]
public void Add1ItemPrice10GrandTotal10() {
        var expectedTotal = 10.00;
        var actualTotal = 0.00;
        Assert.Equal(expectedTotal, actualTotal);
}

还是一样的老把戏,通过硬编码一个错误的值让 Add1ItemPrice10GrandTotal10 测试失败。当然前三个测试成功通过,但第四个新增的测试失败了:

A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.57] tests.UnitTest1.Add1ItemPrice10GrandTotal10 [FAIL]
  X tests.UnitTest1.Add1ItemPrice10GrandTotal10 [4ms]
  Error Message:
   Assert.Equal() Failure
Expected: 10
Actual: 0

Test Run Failed.
Total tests: 4
     Passed: 3
         Failed: 1
 Total time: 1.0320 Seconds

将硬编码值换成实际的处理代码。首先,检查接口是否具备计算订单总价的功能。根本没有这种东西。目前为止接口中只声明了三个功能:

  1. int NoOfItems();
  2. int AddItem(Hashtable item);
  3. int RemoveItem(Hashtable item);

它们都不具备计算总价的能力。所以需要声明一个新功能:

double CalculateGrandTotal();

这个新功能应该让 shoppingAPI 具备计算总价的能力。这是通过遍历购物框中的商品并把它们的价格累加起来实现的。

修改第五个测试:

[Fact]
public void Add1ItemPrice10GrandTotal10() {
        var expectedGrandTotal = 10.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 10.00);
        shoppingAPI.AddItem(item);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

这个测试表明了这样的期望:如果向购物框里加入一件价格 ¥10 的商品,然后调用 CalculateGrandTotal() 方法,它会返回商品总价 ¥10。这是一个完全合理的期望,它完全符合商品总价计算的逻辑。

那么怎么实现这个功能呢?就像以前一样,先写一个假的实现。回到 ShippingAPI 类中,实现在接口中声明的 CalculateGrandTotal() 方法:

public double CalculateGrandTotal() {
                return 0.00;
}

现在先将返回值硬编码为 0.00,只是为了检验这个测试能否正常运行,并确认它是能够失败的。事实上,它能够运行,并且如预期一样失败。接下来的工作就是正确实现计算商品总价的处理逻辑:

public double CalculateGrandTotal() {
        double grandTotal = 0.00;
        foreach(var product in basket) {
                Hashtable item = product as Hashtable;
                foreach(var value in item.Values) {
                        grandTotal += Double.Parse(value.ToString());
                }
        }
        return grandTotal;
}

运行,五个测试全部通过!

从单元素场景到多元素场景

现在是时候进入下一轮迭代了。你已经通过处理最简场景、单元素场景和边界场景迭代地构建了系统,现在需要处理稍复杂的多元素场景了。

快捷提示:由于我们一直在针对单个元素场景、多元素场景和边界行为这三点上对软件进行迭代改进,一些读者可能会认为我们同样应该对接口进行改进。我们稍后就会发现,接口已经完全满足需要了,目前没有新增功能的必要。请记住,应该保持接口的简洁。(盲目地)扩增接口不会带来任何好处,只会引入噪音。我们要遵循 奥卡姆剃刀 Occam's Razor 原则:如无必要,勿增实体。 现在我们已经基本完成了接口功能描述的工作,是时候改进实现了。

通过上一轮的迭代,系统已经能够处理购物框里有超过一件商品的情况了。现在我么来让系统具备购物框里有超过一件商品时计算总价的能力。首先写可执行期望:

[Fact]
public void Add2ItemsGrandTotal30() {
        var expectedGrandTotal = 30.00;
        var actualGrandTotal = 0.00;
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

硬编码所有值,尽量让期望测试失败。

测试确实失败了,现在得想办法让它通过。向购物框添加两件商品,然后调用 CalculateGrandTotal() 方法:

[Fact]
public void Add2ItemsGrandTotal30() {
          var expectedGrandTotal = 30.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 10.00);
        shoppingAPI.AddItem(item);
        Hashtable item2 = new Hashtable();
        item2.Add("00000002", 20.00);
        shoppingAPI.AddItem(item2);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

测试通过。现在共有六个可以通过的微测试,系统回到了稳态。

设定期望

作为一个认真负责的工程师,你希望确保当用户向购物框添加一些商品然后又移除一些商品后系统仍然能够计算出正确出总价。下面是这个新的期望:

[Fact]
public void Add2ItemsRemoveFirstItemGrandTotal200() {
        var expectedGrandTotal = 200.00;
        var actualGrandTotal = 0.00;
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

这个期望表示将两件商品加入到购物框,然后移除第一件后期望的总价是 ¥200。硬编码行为失败了。现在设计更具体的正面测试样例,然后运行代码:

[Fact]
public void Add2ItemsRemoveFirstItemGrandTotal200() {
        var expectedGrandTotal = 200.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 100.00);
        shoppingAPI.AddItem(item);
        Hashtable item2 = new Hashtable();
        item2.Add("00000002", 200.00);
        shoppingAPI.AddItem(item2);
        shoppingAPI.RemoveItem(item);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

在这个正面测试样例中,先向购物框加入第一件商品(编号为 00000001,价格为 ¥100),再加入第二件商品(编号为 00000002,价格为 ¥200)。然后将第一件商品移除,计算总价,比较计算值与期望值是否相等。

运行期望测试,系统正确地计算出了总价,满足这个期望测试。现在有七个能顺利通过的测试了。系统运行良好,无异常!

Test Run Successful.
Total tests: 7
     Passed: 7
 Total time: 0.9544 Seconds

敬请期待

现在你已经学习了 ZOMBIES 方法中的 ZOMBI 部分,下一篇文章将介绍处理特殊行为。到那个时候,你可以试试自己的测试!

(题图:MJ/c4eb23b5-84aa-4477-a6b9-7d2a6d1aeee4)


via: https://opensource.com/article/21/2/boundaries-interfaces

作者:Alex Bunardzic 选题:lujun9972 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在遇到合并冲突时,请不要惊慌。通过一些娴熟的技巧协商,你可以解决任何冲突。

假设你和我正在共同编辑同一个名称为 index.html 的文件。我对文件进行了修改,进行了提交,并将更改推送到 Git 远程仓库。你也对同一个文件进行了修改,进行了提交,并开始将更改推送到同一个 Git 仓库。然而,Git 检测到一个冲突,因为你所做的更改与我所做的更改冲突。

以下是你可以解决冲突的方法:

1、从远程仓库获取并合并最新更改:

$ git pull

2、识别一个或多个有冲突的文件:

$ git status

3、使用文本编辑器打开冲突文件:

$ vim index.html

4、解决冲突。冲突的修改会被标记为 <<<<<<< HEAD>>>>>>>。你需要选择要保留和放弃哪些修改,并手动编辑文件以合并冲突的修改。

以下是一个示例:

<<<<<<< HEAD
<div class="header">
<h1>Sample text 1</h1>
</div>
=======
<div class="header">
<h1>Sample text 2</h1>
</div>
>>>>>>> feature-branch

在这个例子中,我将网站标题更改为 Sample text 1,而你将标题更改为 Sample text 2。两种更改都已添加到文件中。现在你可以决定保留哪一个标题,或者编辑文件以合并更改。在任一情况下,删除指示更改开始和结束的标记,只留下你想要的代码:

<div class="header">
<h1>Sample text 2</h1>
</div>

5、保存所有更改,并关闭编辑器。

6、将文件添加到暂存区:

$ git add index.html

7、提交更改:

$ git commit -m "Updated h1 in index.html"

此命令使用消息 Resolved merge conflict 提交更改。

8、将更改推送到远程仓库:

$ git push

结论

合并冲突是将注意力集中于代码的好理由。你在文件中进行的更改越多,就越容易产生冲突。你应该进行更多的提交,每个提交更改应该更少。你应该避免进行包含多个特性增强或错误修复的单片巨大更改。你的项目经理也会感谢你,因为具有清晰意图的提交更容易追踪。在最初遇到 Git 合并冲突时可能会感到吓人,但是现在你知道如何解决它,你会发现它很容易解决。

(题图:MJ/f432c41a-eb4f-4de0-b2da-f3d8d7a86e26)


via: https://opensource.com/article/23/4/resolve-git-merge-conflicts

作者:Agil Antony 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出