要开始使用 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中:
一切都是对象;
实例变量是完全私有的;
与对象交互的唯一方式是向其发送消息。
除了向对象发送消息之外,没有其他机制可以从计数器的外部访问我们的实例变量。必须做的是定义一个返回实例变量的值的方法。这样的方法称为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
实例的其它方法一样(increment
和decrement
)。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-I
在 Counter 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”视频,以更好地理解这一工作流程。