分类 软件开发 下的文章

上一篇文章 中,你学习了函数。在这篇文章中,我们将学习使用条件语句来管理 Rust 程序的控制流。

什么是条件语句?

在编写代码的时候,最常见的任务之一就是检查某些条件是否为 truefalse。“如果温度高于 35°C,打开空调。”

通过使用 ifelse 这样的关键字(有时候结合使用),程序员可以根据条件(例如提供的参数数量、从命令行传递的选项、文件名、错误发生等)改变程序的行为。

所以,对一个程序员来说,了解任何语言的控制流都是至关重要的,更不用说 Rust 了。

条件运算符

下表列出了所有常用的单个条件运算符:

运算符示例解释
>a > ba 大于 b
<a < ba 小于 b
==a == ba 等于 b
!=a != ba 不等于 b
>=a >= ba 大于等于 b
<=a <= ba 小于等于 b

以及下表是逻辑运算符,它们用于一个或多个条件之间:

运算符示例解释
` ` (逻辑或)`条件1 条件2`条件1条件2 中至少有一个为 true
&& (逻辑与)条件1 && 条件2所有 条件都为 true
! (逻辑非)!条件条件 的布尔值的相反值
? 与数学相似,你可以使用圆括号来指定操作的优先级。

使用 if else

要控制 Rust 代码的基本流程,使用两个关键字:ifelse。这可以根据提供的条件的状态创建两个“执行路径”。

一个简单的带有替代执行路径的 if 块的语法如下:

if 条件 {
    <语句>;
} else {
    <语句>;
}
? 当只有一个条件时,将其括在圆括号中并不是强制性的。根据语法,使用圆括号是可选的。你仍然应该使用它们来指定优先级并优化可读性。

来看看一个例子。

fn main() {
    let a = 36;
    let b = 25;

    if a > b {
        println!("a 大于 b");
    } else {
        println!("b 大于 a");
    }
}

这里,我声明了两个整数变量 ab,它们的值分别为 '36' 和 '25'。在第 5 行,我检查变量 a 中存储的值是否大于变量 b 中存储的值。如果条件计算结果为 true,则会执行第 6 行的代码。如果条件计算结果为 false,由于我们有一个 else 块(可选),第 8 行的代码将被执行。

来看看程序的输出。

a 大于 b

完美!

来修改一下变量 a 的值,使其小于变量 b 的值,看看会发生什么。我将把 a 的值改为 '10'。修改后的输出如下:

b 大于 a

但是,如果我将相同的值存储在变量 ab 中呢?为了看到这一点,我将两个变量的值都设置为 '40'。修改后的输出如下:

b 大于 a

嗯?从逻辑上讲,这没有任何意义... :frowning:

但是这可以改进!我们继续。

使用 else if 条件

与其他任何编程语言一样,你可以使用 else if 块来提供多于两个的执行路径。语法如下:

if 条件 {
    <语句>;
} else if 条件 {
    <语句>;
} else {
    <语句>;
}

现在,通过使用 else if 块,我可以改进程序的逻辑。下面是修改后的程序。

fn main() {
    let a = 40;
    let b = 40;

    if a == b {
        println!("a 与 b 是相等的");
    } else if a > b {
        println!("a 大于 b");
    } else {
        println!("b 大于 a");
    }
}

现在,我的程序的逻辑是正确的。它已经处理了所有的边缘情况(我能想到的)。第 5 行处理了 a 等于 b 的情况。第 7 行处理了 a 可能大于 b 的情况。而 a 小于 b 的情况则由第 9 行的 else 块隐式处理。

现在,当我运行这段代码时,我得到了以下输出:

a 与 b 是相等的

现在这就完美了!

示例:找到最大值

我知道使用 ifelse 很容易,但是让我们再看一个程序。这次,我们来比较三个数字。我还将在这个实例中使用逻辑运算符!

fn main() {
    let a = 73;
    let b = 56;
    let c = 15;

    if (a != b) && (a != c) && (b != c) {
        if (a > b) && (a > c) {
            println!("a 是最大的");
        } else if (b > a) && (b > c) {
            println!("b 是最大的");
        } else {
            println!("c 是最大的");
        }
    }
}

这个程序第一眼看上去可能很复杂,但是不要害怕,我会解释的!

最开始,我声明了三个变量 abc,并赋予了我能想到的随机值。然后,在第 6 行,我检查了没有变量的值与其他变量相同的条件。首先,我检查 ab 的值,然后是 ac,最后是 bc。这样我就可以确定没有变量中存储了重复的值。

然后,在第 7 行,我检查了变量 a 中存储的值是否是最大的。如果这个条件计算结果为 true,则会执行第 8 行的代码。否则,将检查第 9 行的执行路径。

在第 9 行,我检查了变量 b 中存储的值是否是最大的。如果这个条件计算结果为 true,则会执行第 10 行的代码。如果这个条件也是 false,那么只有一种可能。3 个变量中的最大值既不是 a 也不是 b

所以,自然地,在 else 块中,我打印出变量 c 拥有最大值。

来看看程序的输出:

a 是最大的

这是预期的结果。尝试修改分配给每个变量的值,并自己测试一下! :smiley:

总结

你学习到了如何使用 ifelse 语句。在你继续使用大量 if else if 语句制作自己的 AI 之前(哈哈),让我们在本系列的下一篇文章中学习 Rust 中的循环。

持续关注。

(题图:MJ/3eea3bbb-b630-4470-ae21-391ab86cd5bf)


via: https://itsfoss.com/rust-if-else/

作者:Pratham Patel 选题:lkxed 译者:Cubik65536 校对: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 方法的前六个原则(LCTT译注:原文为前五个,应为笔误)。

第一篇中 实现了最简场景,它为代码提供了最简可行路径。第二篇文章中执行了 单元素场景和多元素场景上的测试。第三篇中介绍了 边界和接口。 第四篇中处理 特殊行为。在本文中,我将介绍最后一项:简单场景用简单的解决方案。

简单场景用简单的解决方案

回顾这个网购 API 的实现过程,你会发现总是有目的性地坚持考虑最简单的场景。在这个过程中,最终你会得到最简单的解决方案。

ZOMBIES 方法通过坚持简洁性来帮助你交付健壮优雅的解决方案。

胜利了吗?

似乎一切工作都结束了,一个不那么认真负责的工程师很可能会宣布胜利。但开明的工程师总是会探索得更深一点。

我一直推荐做 变异测试 mutation testing 。在圆满结束这个练习项目,开始新的征程之前,用变异测试来敲打敲打你的解决方案是明智的。况且你也不得不承认,变异很适合对付丧尸的。

你可以在开源网站 Stryker.NET 上进行变异测试。

Mutation testing

看起来有一个存活的变异体。这可不是好兆头。

这意味着什么呢?当你自认为解决方案无懈可击的时候,Stryker.NET 却表示在你的地盘上并非一切安好。

让我们来看看这个存活下来的烦人变异体:

Surviving mutant

变异测试工具将

if(total > 500.00) {

变异为:

if(total >= 500.00) {

然后运行测试,结果对于这个变化没有一个测试失败。如果业务处理代码中发生了一处变动却没有任何一个测试失败,这就意味着你遇到一个存活的变异体。

为什么要在意变异体

为什么存活的变异体是麻烦的征兆呢?因为你写的业务处理逻辑代码控制着整个系统的行为。如果业务处理逻辑改变,系统的行为也应该随之改变。而系统行为的改变应该会导致测试表示的期望被违反。如果没有期望被违反,那就说明这些期望对系统行为的描述还不够准确。这也意味着你的业务处理逻辑中存在漏洞。

要解决这个问题,你需要干掉这个存活下来的变异体。要怎么做呢?一般来说,有存活的变异体意味着有期望被遗漏了。

仔细检查代码,梳理已定义的期望,看看漏掉了什么:

  • 期望 0:新建购物框里有零个商品(这隐含了总价为 ¥0)。
  • 期望 1:向购物框添加一件商品的结果是购物框里有一件商品,如果这件商品的价格是 ¥10,那么总价为 ¥10。
  • 期望 2:向购物框添里加入一件价值 ¥10 的商品,再加入一件价值 ¥20 的商品,总价是 ¥30 。
  • 期望 3:关于从购物框移除商品的期望。
  • 期望 4:总价大于 ¥500 时打,享受九折优惠。

缺了什么呢?根据变异测试报告,你没有定义订单总价刚好为 ¥500 的销售策略。你已经定义订单总额大于 ¥500 和小于 ¥500 时的情况。

定义边界情况的期望:

[Fact]
public void Add2ItemsTotal500GrandTotal500() {
    var expectedGrandTotal = 500.00;
    var actualGrandTotal = 450;
    Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

第一步先写一个假的实现让测试失败。现在共有 9 个微测试。其中 8 个通过,第 9 个失败了:

[xUnit.net 00:00:00.57] tests.UnitTest1.Add2ItemsTotal500GrandTotal500 [FAIL]
  X tests.UnitTest1.Add2ItemsTotal500GrandTotal500 [2ms]
  Error Message:
   Assert.Equal() Failure
Expected: 500
Actual: 450
[...]
Test Run Failed.
Total tests: 9
     Passed: 8
     Failed: 1
 Total time: 1.5920 Seconds

将硬编码值替换成正样例的预期代码:

[Fact]
public void Add2ItemsTotal500GrandTotal500() {
    var expectedGrandTotal = 500.00;
    Hashtable item1 = new Hashtable();
    item1.Add("0001", 400.00);
    shoppingAPI.AddItem(item1);
    Hashtable item2 = new Hashtable();
    item2.Add("0002", 100.00);
    shoppingAPI.AddItem(item2);
    var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); }

共添加了两件商品,一件价值 ¥400,另一件价值 ¥100,总价是 ¥500。调用计算总价的函数,期望的总价是 ¥500。

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

Total tests: 9
     Passed: 9
     Failed: 0
 Total time: 1.0440 Seconds

现在是揭晓真相的时刻。这个新增的期望能够清理掉所有的变异体吗?运行变异测试来看看结果:

Mutation testing success

成功了!10 个变异体全都被干掉了。太棒了!现在你可以放心地发布这个 API 了。

结语

如果从这次练习中有什么收获的话,那就是 技术性拖延 skillful procrastination 这一概念的提出。这个是一个重要的概念,因为我们中的许多人往往会在客户描述完他们的问题之前,就盲目地去设想解决方案。

主动拖延

拖延对于软件工程师来说并不是一件容易的事情。我们总是急于动手写代码。我们熟悉各种设计模式、反模式、编程原则和现成的解决方案。我们总是迫不及待想将它们应用到可执行的代码中,并且倾向于一次性做大量的工作。所以在行动之前仔细考虑每个步骤是一种美德。

这个练习说明 ZOMBIES 方法能够通过一系列深思熟虑的小步骤来帮你实现解决方案。但是有一点需要特别注意,根据 Yagni 原则,这些深思熟虑常常会飞得太远,以至于最终形成一个大而全的系统。这会产生臃肿、紧密耦合的系统。

迭代式开发与增量式开发

在这次练习给我们的另一个重要收获是让我们意识到保持系统持续可用的唯一方法是采用迭代式的开发方法。你通过改造已有代码开发出整个购物 API。这种改造工作是在迭代优化解决方案的过程中不可避免的。

很多团队混淆了迭代和增量。这是两个完全不同的概念。

增量式方法是假设是你有完整清晰的需求,然后通过增量累加的方式来构建解方案。大体上来说,你一点点地构建各个部分,然后将所有的部分组装在一起。大功告成!解决方案已经准备好交付了!

相比之下,迭代式方法中,你并不很确定自己需要交付给客户的是什么。因为这个原因,你更加小心谨慎。你会小心翼翼地避免破坏能够运行的系统(也就是说系统处于稳态)。如果不得不扰动已有的平衡,你也会采取最小侵入性的方式。你专注于用尽可能小的工作量来快速完成每次得改动工作。你倾向于让系统尽快回到稳态。

这就是为什么迭代式方法在真正实现一个功能之前总是先提供一个假实现。你硬编码一系列的期望,以验证小的修改不会导致系统无法运行。然后进行必要的修改,用实际处理代码替换硬编码的值。

作为经验法则,在迭代方法中,你的目标是通过设计期望(微测试),对代码进行不断改进。每进行一次改进,你都要检验系统,以确保它处于工作状态。以这种方式不断前进,最终会达到满足所有期望的程度,并且此时代码已经被重构,没有任何存活的变异体了。

一旦达到了这种状态,你就可以相当自信地交付解决方案了。

由衷感谢 Kent BeckRon JeffriesGeePaw Hill 在我的软件工程学习道路的启发。

愿 ZOMBIES 方法在软件开发的征程上帮助到你。

(题图:MJ/ca463fc6-021b-4818-ba3d-9cd3c8736577)


via: https://opensource.com/article/21/2/simplicity

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

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

以下是如何使用 OpenAI ChatGPT AI 创建聊天助手的 Python 程序的方法。

易于使用的 AI “ChatGPT” 已经以 API 提供。ChatGPT 的创造者 OpenAI 宣布,模型('gpt-3.5-turbo')现在适用于自定义产品和解决方案。而且成本也非常实惠。目前的价格为每 1000 个令牌 0.002 美元。

该模型目前与 Whisper API 一起提供,后者也用于文本到语音解决方案。该 API 目前具备以下功能:

  • 创建自定义的对话代理和机器人
  • 为你编写 Python 代码
  • 起草电子邮件或任何你想要的文档
  • 你可以将自然语言界面集成到你当前的产品/应用/服务或软件中,为你的消费者提供服务
  • 语言翻译服务
  • 成为许多科目的导师
  • 模拟视频游戏角色

正如你所见,机会无限。

如果你计划尝试该 API 并开始使用它,这里有一个简单的指南,为你提供逐步指导。

OpenAI ChatGPT API: 入门指南

先决条件

确保你拥有一个 OpenAI 账户。如果你没有,访问此页面 并创建一个账户。你也可以使用你的谷歌或微软账号。

创建一个账户后,生成一个专属于你的 API 密钥。访问 此页面 并创建一个新的秘密密钥。

创建 OpenAI API 密钥的位置

记录该密钥或在安全的地方保存它。基于安全原因,它将不会从 OpenAI 账户部分再次可见。而且不要与任何人分享此密钥。如果你计划使用企业解决方案,请向你的组织查询 API 密钥。由于该密钥与你的付费 OpenAI 计划相关,因此请谨慎使用。

设置环境

安装 Python 和 pip

本指南使用 Python 编程语言来调用 OpenAI API 密钥。你可以使用 Java 或其他任何语言来调用它。

首先,请确保你在 Linux 或 Windows 中已经安装了 Python。如果没有,请按照以下指南安装 Python。如果你使用现代 Linux 发行版(例如 Ubuntu),Python 应该已经安装好了。

在安装 Python 后,确保 pip 在 Linux 发行版中可用。运行以下命令进行安装。对于 Windows,你应该已经在 Python 安装的一部分中安装了它。

Ubuntu、Debian 和其他基于 Debian 的发行版:

sudo apt install python3-pip

Fedora、RHEL、CentOS 等:

sudo dnf install python3-pip

Arch Linux:

sudo pacman -S python-pip

将 OpenAI API 密钥设置为环境变量

上述步骤中创建的 API 密钥,你可以直接在程序中使用。但这并不是最佳实践。

最佳实践是从文件或你系统的环境变量中调用它。

对于 Windows,请设置一个任何名字的环境变量,例如 API-KEY。并添加密钥值。

对于 Linux,请使用超级用户权限打开 /etc/environment 文件并添加密钥。例如:

API-KEY="<你的密钥>"

对于基于文件的密钥访问,请在你的代码中使用以下语句:

openai.api_key_path = <你的 API 密钥路径>

对于直接在代码中访问(不建议),你可以在你的代码中使用以下语句:

openai.api_key = "你的密钥"

注意:如果验证失败,OpenAI API 将抛出以下错误。你需要验证你的密钥值、路径和其他参数以进行更正:openai.error.AuthenticationError: No API key provided

安装 OpenAI API

最后一步是安装 OpenAI 的 Python 库。打开终端或命令窗口,使用以下命令安装 OpenAI API。

pip install openai

在此阶段,你已经准备好撰写你的第一个程序了!

编写助手程序(逐步)

OpenAI API 提供了各种接口模式。例如“聊天补完”、“代码补完”、“图像生成”等。在本指南中,我将使用 API 的“聊天补完”功能。使用此功能,我们可以创建一个简单的对话聊天机器人。

首先,你需要导入 OpenAI 库。你可以使用以下语句在你的 Python 程序中完成:

import openai

在这个语句之后,你应该确保启用你的 API 密钥。你可以使用上面解释的任何方法来完成。

openai.api_key="your key here"
openai.api_key="your environment variable"
openai.api_key_path = <your path to API key>

OpenAI 聊天 API 的基本功能如下所示。openai.ChatCompletion.create 函数以 JSON 格式接受多个参数。这些参数的形式是 “角色”(role) 和 “内容”(content):

openai.ChatCompletion.create(   
  model = "gpt-3.5-turbo",   
  messages = [         
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Who won the world series in 2020?"},
    {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
    {"role": "user", "content": "Where was it played?"}     
  ]
)

说明:

role: 有效的值为 systemuserassistant

  • system: 指示 API 如何行动。基本上,它是 OpenAI 的主提示。
  • user: 你要问的问题。这是单个或多个会话中的用户输入。它可以是多行文本。
  • assistant: 当你编写对话时,你需要使用此角色来添加响应。这样,API 就会记住讨论的内容。

注意:在一个单一的消息中,你可以发送多个角色。如上述代码片段所示的行为、你的问题和历史记录。

让我们定义一个数组来保存 OpenAI 的完整消息。然后向用户展示提示并接受 system 指令。

messages = []
system_message = input("What type of chatbot you want me to be?")
messages.append({"role":"system","content":system_message})

一旦设置好了,再次提示用户进行关于对话的进一步提问。你可以使用 Python 的 input 函数(或任何其他文件输入方法),并为角色 user 设置 content

print("Alright! I am ready to be your friendly chatbot" + "\n" + "You can now type your messages.")
message = input("")
messages.append({"role":"user","content": message})

现在,你已经准备好了具有基本 JSON 输入的数组,用于调用“聊天补完”服务的 create 函数。

response=openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=messages)

现在,你可以对其进行适当的格式化,要么打印响应,要么解析响应。响应是以 JSON 格式提供的。输出响应提供了 choices 数组。响应在 message JSON 对象下提供,其中包括 content 值。

对于此示例,我们可以读取 choices 数组中的第一个对象并读取其 content

reply = response["choices"][0]["message"]["content"]
print(reply)

最后,它将为你提供来自 API 的输出。

运行代码

你可以从你的 喜好的 Python IDE 或直接从命令行运行代码。

python OpenAIDemo2.py

未格式化的 JSON 输出

以下是使用未格式化的 JSON 输出运行上述程序供你参考。正如你所看到的,响应在 choices 数组下具有 content

[debugpoint@fedora python]$ python OpenAIDemo2.py
What type of chatbot you want me to be?a friendly friend
Alright! I am ready to be your friendly chatbot
You can now type your messages.
what do you think about kindness?
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "As an AI language model, I don't have personal opinions, but I can tell you that kindness is a very positive and essential trait to have. Kindness is about being considerate and compassionate towards others, which creates positive emotions and reduces negativity. People who are kind towards others are more likely to inspire kindness and compassion in return. It is an important quality that helps to build positive relationships, promote cooperation, and create a more peaceful world.",
        "role": "assistant"
      }
    }
  ],
  "created": <removed>,
  "id": "chatcmpl-<removed>",
  "model": "gpt-3.5-turbo-0301",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 91,
    "prompt_tokens": 22,
    "total_tokens": 113
  }
}

格式化的输出

这是一个适当的对话式输出。

[debugpoint@fedora python]$ python OpenAIDemo2.py
What type of chatbot you want me to be?a friendly friend
Alright! I am ready to be your friendly chatbot
You can now type your messages.
what do you think about artificial general intelligence?
As an AI language model, I am programmed to be neutral and not have personal opinions. However, artificial general intelligence (AGI) is a fascinating field of study. AGI refers to the development of machines and algorithms that can perform any intellectual task that a human being can. The potential benefits and risks of AGI are still widely debated, with some experts worried about the implications of machines reaching human-like intelligence. However, many believe that AGI has the potential to revolutionize fields such as healthcare, education, and transportation. The key is to ensure that AGI is developed in a responsible and ethical manner.

完整代码

这是上面演示中使用的完整代码。

import openai

openai.api_key = "<your key>"
messages = []
system_message = input("What type of chatbot you want me to be?")
messages.append({"role":"system","content":system_message})

print("Alright! I am ready to be your friendly chatbot" + "\n" + "You can now type your messages.")
message = input("")
messages.append({"role":"user","content": message})

response = openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=messages
)

reply = response["choices"][0]["message"]["content"]
print(reply)

总结

希望这篇简单的指南能让你开始尝试 OpenAI CharGPT API。你可以将上述步骤扩展到更复杂的会话式聊天机器人。此外,你还可以使用 OpenAI 的其他产品。

请不要错过我后续的教程,我将会实验和分享给大家。最后,请不要忘记关注我们,以便及时获取我们的文章。

如果上述步骤对你有帮助,请在评论框中告诉我。

干杯!

参考资料

(题图:MJ/b206dd48-f698-4800-bccc-19ea11a17ea6)


via: https://www.debugpoint.com/openai-chatgpt-api-python

作者:Arindam 选题: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中国 荣誉推出