Pharo By Example - 第五章 开发一个简单的计数器

标签: pharo ; smalltalk ;


要开始使用 Pharo,我们将按照以下步骤编写一个简单的计数器。在这个练习中,您将学习如何创建包、类、方法、实例、单元测试等。这个教程涵盖了在 Pharo 开发时您将执行的大部分重要操作。您还可以观看 Pharo MOOC 上提供的配套视频,网址为 http://mooc.pharo.org ,这些视频有助于说明本教程。

请注意,这个小教程所但倡导的开发流程是传统的,即您将定义一个包、一个类,然后定义它的实例变量,接着定义它的方法,最后再执行它。而在 Pharo 中,开发人员通常遵循一种不同的工作流程,称之为测试驱动开发(Test-Driven Development),就像我们在上一章中看到的那样:他们首先执行一个会引发错误的表达式。这些错误会被调试器捕获,开发人员可以直接在调试器中编码,从而允许系统即时定义实例变量和方法。

我们还将向您展示如何使用 Iceberg 将代码保存到 Git 托管服务,例如 GitHub。

完成本教程后,如果您对 Pharo 愈加自信,我们强烈建议您再次使用测试驱动开发(TDD)进行练习。同样,有另一个视频展示了这种强大的编码方法。

5.1 我们的用例

我们的用例如下:我们希望创建一个计数器,让它递增两次,递减一次,然后检查它的值是否符合预期。以下示例展示了这个过程,并将成为一个完美的单元测试——您稍后将定义一个。

| counter |
counter := Counter new.
counter increment; increment.
counter decrement.
counter count = 1

我们将编写所有必要的类和方法来支持这个示例。

5.2 创建包和类

在这一部分,您将创建您的第一个类。在 Pharo 中,类是在包中定义的,因此我们需要先创建一个包来放置该类。每次创建类的步骤都是相同的,所以请注意。

创建一个包

使用浏览器创建一个包(在Package窗格中右键点击,选择“New package”)。系统会要求您输入一个名称,请输入 MyCounter。这个新的包将被创建,添加到包列表中,并且默认被选中。图 5-1 显示了预期的结果。

图5-1 包已创建,创建类的模板

创建类

浏览器的下部窗格现在应该已经打开了,并且显示一个类定义模板的选项卡。要创建一个新类,您只需要编辑这个模板并编译这个类。这里有五个部分你可能需要改变:

  • 超类. 这描述了您正在创建的类的超类。它默认为Object,这是Pharo中所有类中最不具体化的类,这是我们想要的Counter类的超类。但情况并非总是如此:通常情况下,您可能希望将一个类建立在更具体的类上面。

  • 类名. 接下来,您应该填写类的名称,将#MyClass替换为#Counter。注意类的名称以大写字母开头,并且不要删除#Counter前面的#符号。这是因为我们使用Symbol来命名类,在Pharo中,以#开头的字符串表示该字符串是独一无二的。

  • 实例变量. 然后,您应该在instanceVariableNames旁边填写该类的实例变量名。我们只需要一个名为count的实例变量。注意要写在单引号里面!

  • 类变量. 它们被声明在classVariableNames:;确保它是一个空字符串,因为我们不需要任何类变量。

你应该得到如下的类定义:

Object subclass: #Counter
    instanceVariableNames: 'count'
    classVariableNames: ''
    package: 'MyCounter'

我们现在有了 Counter 类的定义。要在系统中定义它,我们仍需编译它——可以通过下半部分的上下文菜单"Accept"或快捷键 Cmd-S 来完成。Counter 类现在已编译并立即添加到系统中。

图5-2说明了浏览器应该显示的结果情况。

图5-2 类已创建:它继承了Object类,并有一个名为count的实例变量

Pharo的代码评估工具会自动运行并显示一些错误;现在不要担心它们,它们主要是关于我们的类还没有被使用。

作为严谨的开发者,我们将在 Counter 类中添加注释,方法是点击“Comment”面板并点击"Toggle Edit / View comment"。您可以输入以下注释:

`Counter` is a simple concrete class which supports incrementing and
    decrementing.

Its API is
- `decrement` and `increment`
- `count`
Its creation message is `startAt:`

注释是用Microdown语法编写的,这是Markdown的一种方言,应该很直观。它们能够很好地呈现在浏览器中。同样,通过菜单Accept或点击Cmd-S来接受这些更改。

图5-3显示了带有注释的类。

图5-3 Counter类现在有注释了! 干得不错

5.3 定义协议和方法

在这部分,你将学习如何使用浏览器添加协议和方法。

我们定义的类有一个名为count的实例变量,我们将使用该变量来保持计数。我们将对它递增、递减,并显示它当前的值。但是我们需要记住三件事,在Pharo中:

  1. 一切都是对象;

  2. 实例变量是完全私有的;

  3. 与对象交互的唯一方式是向其发送消息。

除了向对象发送消息之外,没有其他机制可以从计数器的外部访问我们的实例变量。必须做的是定义一个返回实例变量的值的方法。这样的方法称为getter方法。因此,我们来为我们的实例变量count定义一个访问器方法。

通常需要将方法放入协议中。协议是对方法的分组-它们在Pharo中没有意义,但它们可以向您的类的读者传达了重要的信息。尽管协议可以起任意名字,但是Pharo程序员在命名协议时遵循某些约定。如果您定义了一个方法,但不确定它应该采用哪种协议,请首先查看现有代码,看看是否可以找到已经存在适当的协议。

5.4 创建一个方法

现在,让我们为实例变量count创建getter方法。首先在浏览器中选择Counter类,并确保通过选择'instance side`选项卡来编辑类的实例侧(即,我们在类的实例上定义方法)。然后定义你自己的方法。

图5-4显示了准备定义方法的方法编辑器。

图5-4 方法编辑器已选中,并准备好定义一个方法

提示:在文本的末尾或开头双击并开始输入您的方法,会自动替换掉模板。

输入下面的方法定义:

count
    ^ count

这里定义了一个名为count的方法,该方法不接受任何参数,返回实例变量count的值。然后在菜单中选择Accept以编译该方法。该方法被自动归类到accessing协议中。

图5-5显示了定义方法后系统的状态。

图5-5 在accessing协议中定义的count方法

现在,您可以在Playground中输入并求值下面的表达式来测试刚刚编写的方法:

Counter new count
>>> nil

该表达式首先创建一个新的Counter实例,然后将消息count发送给它。检索计数器当前的值。它应该会返回nil(未初始化实例变量的默认值)。之后,我们将创建具有合理的默认值的实例。

5.5 添加一个setter方法

与getter方法互补的是setter方法。setter方法用于从对象外部更改实例变量的值。例如,表达式Counter new count: 7首先创建了一个新的计数器实例,然后通过向其发送消息count: 7将它的值设置为7。getter方法和setter方法统称为访问器方法。

此示例显示了实际使用的setter方法:

| c |
c := Counter new count: 7.
c count
>>> 7

setter方法当前并不存在,作为练习,需要由你自己创建count:方法,当该方法被Counter的实例调用时,实例变量会被设置为消息的参数。在Playground 中求值上面的示例来测试您的方法。

5.6 定义一个测试类

编写测试--无论您是在编写代码之前还是之后--如今已经不再是可选项。一组编写良好的测试将支持您的应用程序的演进,并让您有信心程序能够按预期工作。为您的代码编写测试是一项很好的投资;测试代码一旦编写完成就可以被执行无数次。例如,如果我们把上面的示例转化为一个测试,我们就可以自动检查新的 setter 方法是否如预期那样工作。

我们的测试用例,以方法的形式编写,需要存在于继承自 TestCase 的测试类中。因此,我们定义一个名为 CounterTest 的类,如下所示:

TestCase subclass: #CounterTest
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'MyCounter'

现在我们可以通过定义一个方法来编写我们的第一个测试。测试方法的名称应该以 test 开头,这样它们才能被 Test Runner 自动执行,或者在方法名旁边出现一个小的可点击圆圈,让您能够运行测试。

图5-6显示了CounterTest类中的方法testCountIsSetAndRead的定义。

图5-6 第一个测试被定义并通过

为我们的测试用例定义以下方法。它首先创建了一个计数器实例,设置它的值,然后验证值是否已设置。消息assert:equals:是在我们的测试类中实现的消息。它验证一个事实(在这种情况下,即两个对象是否相等),如果事实不成立,则测试失败。

CounterTest >> testCountIsSetAndRead
    | c |
    c := Counter new.
    c count: 7.
    self assert: c count equals: 7

排版惯例

Pharo 用户经常使用 ClassName >> methodName 这种记法来标识一个方法所属的类。例如,我们在 Counter 类中编写的 count 方法会被表示为 Counter >> count。只需记住,这并不是严格的 Pharo 语法,而是一种方便的记法,用来表示“属于 Counter 类的实例方法 count”。

从现在起,当我们在本书中展示方法时,我们将会以这种形式书写方法的名称。当然,当您在浏览器中实际输入代码时,不需要入类名或>>;相反,您需要确保在Class窗格中选中了合适的类。

通过点击方法前面的圆圈图标(如图5-6所示)或使用工具菜单中提供的 Test Runner 程序来验证测试是否通过。

既然你现在有了第一个通过的测试,现在是保存工作的良好时机。

5.7 通过Iceberg将你的代码保存到git存储库中

在 Pharo 镜像中保存你的工作是好的,但这对于分享你的工作或与他人协作并不理想。现代软件开发很大程度上是通过 Git 进行的,Git 是一个开源的版本控制系统。像 GitHub 这样的服务建立在 Git 之上,为开发者提供了共同构建开源项目(比如 Pharo)的地方。

Pharo 通过工具 Iceberg 与 Git 配合工作。本节将向您展示如何为您的代码创建一个本地 Git 仓库,提交更改到该仓库,以及将这些更改推送到如 GitHub 这样的远程仓库。

打开Iceberg

通过Sources菜单或使用快捷键 Cmd-O, I打开Iceberg

现在你应该能看到类似于图 5-7 所示的内容,显示的是顶级的 Iceberg 窗格。它展示了 Pharo 项目以及其他几个随你的镜像一起提供的项目,并通过显示“本地仓库缺失”来表明未能找到这些项目的本地仓库。如果你不想为 Pharo 贡献代码,你不必担心 Pharo 项目或是否有本地仓库。

图5-7 全新映像上的Iceberg Repositories浏览器会提示,
如果您想对Pharo本身进行版本控制,你需要告诉Iceberg Pharo克隆的位置。
不过现在你不必在意。

我们将创建一个属于我们自己的新项目。

添加并配置一个工程

按下“添加”按钮来创建一个新项目。从左侧选择“New Repository”,你应该会看到一个类似于图 5-8 所示的配置窗格。在这里,我们需要命名我们的项目,声明一个本地磁盘上的目录来保存项目的源代码,以及项目中的一个子目录,该子目录将用于存放 Pharo 代码——按照惯例,这个子目录通常是 src

图5-8 添加并创建一个名为MyCounter的项目,该项目包含src子目录。

将你的包添加到工程中

添加工程后,Iceberg Working Copy 浏览器应该会显示一个空的窗格,因为我们还没有向项目中添加任何的包。点击Add Package按钮,选择MyCounter包,如图5-9所示。

图5-9 选择 Add packages 图标按钮,将您的包 MyCounter添加到项目中。

提交你的更改

一旦你的包被添加,Iceberg 会显示你的项目管理的包中有未提交的代码,如图 5-10 所示。按下“提交”按钮。Iceberg 将显示所有即将保存的更改(如图 5-11 所示)。输入一个提交信息并提交你的更改。

图5-10 现在Iceberg显示您还没提交代码

图5-11 Icegerg 向您展示即将进行的更改

代码已保存

一旦提交,Iceberg就会提示您的系统和本地存储库是同步的。

干得好!稍后,我们将介绍如何将这些更改推送到远程存储库。但是现在先让我们回到Counter上。

图5-12 一旦你保存了更改,Iceberg就会向你展示。

5.8 添加更多的消息

我们将以测试驱动的方式为Counter类开发下面的消息。首先,这是一个测试increment的消息:

CounterTest >> testIncrement
    | c |
    c := Counter new.
    c count: 0 ; increment; increment.
    self assert: c count equals: 2

现在轮到你了!编写 increment 方法的定义,使测试通过。

当你完成之后,尝试为 decrement 消息编写一个测试,然后通过在 Counter 类中实现该方法使其通过测试。

解答

Counter >> increment
    count := count + 1
    
Counter >> decrement
    count := count - 1

你的测试都应该通过(如图 5-13 所示)。再次强调,这是一个保存工作的良好时机。在测试通过的情况下保存是一个很好的习惯。要保存你的更改,只需使用 Iceberg 提交它们。

图5-13 多个测试为绿色的类

5.9 实例的初始化方法

目前,我们的计数器的初始值尚未设置,如下表达式所示:

Counter new count
>>> nil

我们来编写一个测试,断言新创建的计数器实例的计数为0

CounterTest >> testInitialize
    self assert: Counter new count equals: 0

这次测试会变为黄色,表示测试失败——测试本身运行正常,但断言未通过。这与我们迄今为止见到的红色测试不同,红色测试是因为发生错误(例如,某个方法尚未实现)而导致测试失败。

5.10 定义 initialize 方法

现在,我们必须编写一个初始化方法来设置Counter实例变量的默认值。然而,正如我们所提到的,initialize消息被发送到新创建的实例。这意味着initialize方法应该在实例端定义,就像发送到Counter实例的其它方法一样(incrementdecrement)。initialize方法负责设置实例变量的默认值。

因此,在Counter的实例端,在initialization协议中,编写以下方法(此方法的主体为空。请填写!)。

Counter >> initialize
    "set the initial value of the value to 0"
    
    "Your code here"

如果你没做错,我们的testInitialize测试现在应该通过了。

像往常一样,在进入下一步之前保存您的工作。

5.11 定义一个新的创建实例的方法

我们刚刚讨论了如何在类的实例侧定义initialize方法,因为它负责修改Counter的实例。现在,让我们来看看如何在类侧定义方法。类方法将在向类本身而不是类的实例发送消息时执行。为了在类上定义方法,我们需要通过选择“Class side”来切换代码浏览器。

定义一个名为startingAt:的新的创建实例的方法。这个方法接收一个整数作为参数,并返回一个Counter的新实例,该实例的count设置为指定的值。

我们首先要做什么?当然是定义一个测试:

TestCounter >> testCounterStartingAt5
    self assert: (Counter startingAt: 5) count equals: 5

这里,消息startingAt:被发送给了Counter自身。

你的实现看起来应该像下面这样:

Counter class >> startingAt: anInteger
    ^ self new count: anInteger.

这里我们看到了用于标识类侧方法的记法:ClassName class >> methodName 只是意味着“在 Counter 类上的类侧方法 startingAt:”。

self在这里指的是什么?像往常一样,self指的是定义方法的对象本身,因此,这里的self指的就是Counter类本身。

我们来再编写一个测试,以确保一切正常:

CounterTest >> testAlternateCreationMethod
    self assert: ((Counter startingAt: 19) increment; count) equals: 20

5.12 更好的对象描述

当你检查一个 Counter 实例时,无论是通过调试器(Debugger)、使用 Cmd-ICounter new 表达式上打开检查器(Inspector),还是直接运行 Print it 操作,你都会看到一个非常简单的表示形式;它只会显示为 'a Counter'

Counter new
>>> a Counter

我们想要一个更丰富的表示,例如,一个显示计数值的表示。在printing协议下实现如下方法:

Counter >> printOn: aStream
    super printOn: aStream.
    aStream nextPutAll: ' with value: ', count printString.

注意,当使用 Print it(见图 5-14)或在检查器(Inspector)中检查对象时,会向该对象发送 printOn: 消息。通过在 Counter 的实例上实现 printOn: 方法,我们可以控制它们的显示方式,并覆盖 Object 类中定义的默认实现,后者迄今为止一直在处理所有这些工作。我们将在本书后面更详细地探讨这些概念,同时也会更多地了解流和超类调用。

图5-14 计数器实例的描述更好了

在这种情况下,我们将让你为这个方法定义一个测试用例。一个小提示:向 Counter new 发送 printString 消息以获取其字符串表示形式,该字符串是由 printOn: 方法生成的。

Counter new printString
>>> a Counter with value: 0

现在,让我们再次保存代码,不过,这一次是在远程 git 服务器上

5.13 将你的代码保存到远程服务器

到目前为止,你已经将代码保存在本地磁盘上。我们现在将展示如何将代码保存到远程 Git 仓库,比如你可以在 GitHub (http://github.com) 或 GitLab 上创建的仓库。

在远程仓库中创建一个工程

首先,你应该在远程 Git 服务器上创建一个项目。不要往里面放任何东西!否则可能会引起混淆。给项目起一个简单明了的名字,比如“Counter”或“Pharo-Counter”。这就是我们的 Iceberg 项目将要发送到的地方。

添加一个通过HTTPS访问的远程仓库

在 Iceberg 中,通过双击你的 Counter 仓库来进入工作副本浏览器。然后点击看起来像一个盒子、标有“Repository”的图标。这将打开 Counter 项目的仓库浏览器,如图 5-15 所示。

图5-15 在项目上打开Repository浏览器

然后你只需要为项目添加一个远程仓库,这一步非常简单,只需点击标记为“Add remote”的大加号图标即可。系统会要求你为远程仓库提供一个名称,这是 Git 用于本地标识远程仓库的标签,以及远程仓库的 URL。你可以使用 HTTPS 访问(URL 以 https://github.com 开头,适用于 GitHub),或 SSH 访问(URL 以 git@github.com 开头)。使用 SSH 需要在你的机器上设置 SSH 代理并提供正确的凭据(请咨询你的 Git 远程仓库提供商以获取具体操作步骤),而使用 HTTPS 则需要你输入用户名和密码;Pharo 可以为你存储这些信息。有关使用 HTTPS 的详情,请参见图 5-16 和图 5-17。

图5-16 我们的项目的GitHub HTTPS地址

图5-17 使用GitHub HTTPS地址

Push

一旦你添加了一个有效的服务器地址,Iceberg 将会在“Push”按钮上显示一个小红点指示器。这表明你在本地仓库中有尚未推送到远程仓库的更改。你所需要做的就是按下“Push”按钮;Iceberg 会显示将要推送到服务器的提交记录,如图 5-18 所示。

图5-18 提交已经发送给远程存储库

现在你真正保存了代码,可以从另一台机器或位置重新加载。这项技能将使你能够远程工作,并与他人共享和协作。

5.14 总结

在本教程中,你学会了如何定义包、类、方法和测试。我们为第一个教程选择的编程工作流程与其他大多数编程语言相似。然而,在 Pharo 中,聪明且敏捷的开发者使用不同的工作流程:测试驱动开发(TDD)。我们建议你通过首先定义测试、执行测试、在调试器中定义方法,然后再重复这一过程,来重新做整个练习。观看 Pharo MOOC 网站( http://mooc.pharo.org )上提供的第二部“Counter”视频,以更好地理解这一工作流程。