Pharo By Example - 第六章 制作一个小游戏

标签: pharo ; smalltalk ;


在本章中,我们将开发一个简单的游戏:熄灯游戏 (https://en.wikipedia.org/wiki/Lights_Out_%28game%29 ) 。通过这个过程,我们将提高对浏览器、检查器、调试器以及使用Iceberg进行代码版本控制的熟悉度。熟练掌握这些核心工具非常重要。同样地,我们将鼓励您采用测试优先的方法来开发这款游戏,即使用测试驱动开发。

一个警告:本章包含了一些故意制造的错误,以便我们演示如何处理错误和查找漏洞。如果您觉得这有点令人沮丧,还请见谅,但请尽量跟着操作,因为这些也是我们需要亲眼见证的重要技术。

图6-1 熄灯游戏的游戏板

6.1 熄灯游戏

游戏板由一个矩形的浅黄色单元格数组组成。当你点击其中一个单元格时,周围的四个单元格会变成蓝色。再次点击,它们会切换回浅黄色。游戏的目标是尽可能多地将单元格变成蓝色。

“熄灯游戏”由两种对象组成:游戏板本身和100个单独的单元格对象。实现这个游戏的Pharo代码将包含两个类:一个用于游戏,另一个用于单元格。

6.2 创建一个新的包

我们需要定义一个包,这一步我们将在浏览器中完成。如果你需要回顾如何操作,可以参考Pharo快速导览 章节和 开发一个简单的计数器 章节。

我们将这个包命名为 PBE-LightsOut。创建新包后,过滤包列表可能是个好主意:只需在过滤器中输入 PBE,你应该就只能看到我们的包了。

图6-3 创建一个新包以及类模板

6.3 定义LOCell类

此时,新包中当然还没有任何类。然而,主编辑面板会显示一个模板,便于我们创建新类(参见图6-3)。让我们用所需的信息填充这个模板。

6.4 创建一个新类

下面的代码包含了我们将要使用的新的类定义:

SimpleSwitchMorph subclass: #LOCell
    instanceVariableNames: 'mouseAction'
    classVariableNames: ''
    package: 'PBE-LightsOut'

让我们花一分钟时间思考一下你看到的这个模板。这是一个为了创建类而填写的特殊表单吗?是一种新的语法吗?不是的,这只是发送给另一个对象的消息而已!这条消息是 subclass:instanceVariableNames:classVariableNames:package,确实有些长,而且我们是将其发送给 SimpleSwitchMorph 类的。除了我们要创建的子类的名字是一个符号 #LOCell 外,其余的参数都是字符串。这里的包是我们新建的包,而 mouseAction 实例变量将用于定义当单元格被点击时应采取的动作。

那么,为什么我们要继承 SimpleSwitchMorph 而不是 Object 呢?我们很快就会看到继承已经专门化的类的好处。同时,我们也会了解到那个 mouseAction 实例变量到底是怎么回事。

要发送这条消息,可以通过菜单或者使用 Cmd-S 快捷键来接受代码。消息被发送后,新的类会被编译,我们就会得到类似图6-5的结果。

图6-5 刚创建的LOCell类

新类出现在浏览器的类面板中,编辑面板现在显示了类的定义。在窗口底部,你会收到质量助手的反馈:它会自动对你的代码进行一些质量检查并报告结果。目前不必太担心这些反馈。

6.5 关于注释

Pharo 开发者们非常重视代码的可读性,以帮助解释代码的功能,同时也注重良好的注释。

方法注释

最近有一种倾向认为,编写得当、表达清晰的方法不需要注释;意图和含义仅通过阅读代码就应该一目了然。这种观点是完全错误的,并且会助长敷衍了事的态度。当然,粗糙和难以理解的代码应该被改进、重命名和重构。

好的注释并不能弥补难读的代码。显然,为琐碎的方法编写注释是没有意义的;注释不应该只是用英语重写一遍代码。你的注释应当旨在解释方法的作用、其上下文,以及可能的实现背后的理由。读者应当能从你的注释中获得安慰,注释应该表明他们对代码的理解是正确的,并消除他们可能存在的困惑。

类注释

对于类注释,Pharo 提供了一个模板,并给出了一些建议,说明了什么是好的类注释。所以,请阅读它!该格式基于类(或候选)职责协作卡(CRC 卡),这是 Kent Beck 和 Ward Cunningham 在20世纪80年代使用 Smalltalk 工作时,受到许多影响而发展出来的(欲了解更多信息,请参阅他们的论文面向对象的思维教学实验室 )。简而言之,这个想法是在几句话中陈述类的职责,以及它是如何与其他类协作来实现这些职责的。此外,我们还可以陈述类的 API(对象理解的主要消息)、给出类的使用示例(通常在 Pharo 中,我们会将示例定义为类方法),以及关于内部表示或实现背后原理的一些细节。

6.6 向类添加方法

现在,让我们向我们的类添加一些方法。首先,让我们添加清单6-6作为实例端方法:

清单 6-6 LOCell实例初始化

LOCell >> initialize
    super initialize.
    self label: ''.
    self borderWidth: 2.
    bounds := 0 @ 0 corner: 16 @ 16.
    offColor := Color paleYellow.
    onColor := Color paleBlue darker.
    self useSquareCorners.
    self turnOff

回想一下前面的章节,我们使用语法ClassName >> methodName来显式地指明方法是在哪个类中定义的。

请注意,第3行的字符是两个单独的单引号,它们之间没有任何东西,而不是双引号!表示空字符串。另一种创建空字符串的方法是String new。别忘了编译这个方法!

这个方法中涉及的内容有点多,让我们逐一解析。

initialize 方法

首先,这是一个初始化方法,就像我们在上一章中看到的计数器那样。作为提醒,这些方法很特殊,因为按照惯例,它们会在实例创建后立即执行。因此,当我们执行 LOCell new 时,会自动向新创建的对象发送 initialize 消息。初始化方法用于设置对象的状态,通常是设置它们的实例变量,而这正是我们在这里所做的。

调用超类的初始化

但是,这个方法做的第一件事就是执行其超类 SimpleSwitchMorphinitialize 方法。这里的想法是,超类的任何继承状态都将通过超类的 initialize 方法被正确初始化。在做任何其他事情之前,最好总是通过发送 super initialize 来初始化继承的状态。当我们调用 SimpleSwitchMorphinitialize 方法时,我们可能不知道它具体会做什么,也不必关心,但可以合理推测它会设置一些实例变量来保存 SimpleSwitchMorph 需要的合理默认值。所以我们最好调用它,否则我们的新对象可能会处于无效状态。

该方法的其余部分设置该对象的状态。例如,发送self label: ''会将此对象的标签设置为空字符串。

关于点和矩形的创建

表达式 0@0 corner: 16@16 需要一些解释。0@0 构建了一个 x 和 y 坐标都设置为 0 的 Point 对象。实际上,0@0 向数字 0 发送了带有参数 0 的消息 @。其效果是数字 0 会请求 Point 类创建一个坐标为 (0,0) 的新实例。接着,我们向这个新创建的点发送消息 corner: 16@16,这会导致它创建一个顶点为 0@016@16Rectangle 对象。这个新创建的矩形将被赋值给从超类继承的 bounds 变量。这个 bounds 变量决定了我们的 Morph 将有多大——实际上,我们只是在说“成为一个 16x16 像素的正方形”。

请注意,Pharo屏幕的原点在左上角,y坐标自上而下递增。

关于剩下的部分

方法的其余部分应该是不言而喻的。编写好的Pharo代码的部分技巧是选择好的方法名称,这样代码就可以读起来像(非常基本的)英语。你应该能够想象这个对象在自言自语地说:“我,用正方角!”,“我,关掉!”

注意,你的方法旁边有一个小绿箭头(参见图6-7)。这表示该方法存在于超类中,并且在你的类中被重写了。

图6-7 新创建的initialize方法

6.7 检查一个对象

你可以立即通过创建一个新的 LOCell 对象并检查它来测试你编写代码的效果:打开一个 Playground,输入表达式 LOCell new,然后检查它。(“Inspect it”, 或者Ctrl-i)。

在 Inspector 的 Raw 标签页中,左边一列显示实例变量的列表,右边一列显示每个实例变量的值(参见图6-8)。其他标签页展示了 LOCell 的其他方面,你可以查看这些标签页并进行实验。

图6-8 用于检视LOCell对象的检查器

如果您点击一个实例变量,检查器将会打开一个新的窗格,其中包含了实例变量的详细信息(参见图6-9)。

图6-9 当我们单击一个实例变量时,我们检查它的值(另一个对象)

执行表达式

Inspector 的底部窗格充当一个迷你 Playground。它非常有用,因为在这一迷你 Playground 中,伪变量 self 被绑定到选定的对象上。

转到窗格底部的那个 Playground,输入文本 self bounds: (200 @ 200 corner: 250 @ 250),然后执行(“Do it”)。为了刷新值,点击窗格右上角的'update'按钮(蓝色小圆圈)。在 Inspector 中,bounds 变量的值应该会发生变化。现在在迷你 Playground 中输入文本 self openInWorld,然后执行(“Do it”)。

Morphic光环

单元格应该出现在屏幕的左上角附近(如图6-10所示),并且正好出现在其 bounds 所指定的位置——即从顶部向下200像素,从左侧向内200像素处。

图6-10 在世界中打开一个LOCell单元格

Meta-click(Option-Shift-Click)单元格以显示 Morphic 光环。Morphic 光环提供了一种视觉方式来与 Morph 交互,使用现在围绕 Morph 的“手柄”。使用蓝色手柄(靠近左下角)旋转单元格,使用黄色手柄(右下角)调整其大小。注意 Inspector 报告的 bounds 值也会随之变化(你可能需要点击刷新按钮才能看到新的 bounds 值)。通过点击粉红色手柄上的 “x” 来删除单元格。

[译注] 在我的机器上,Meta-click 是点击鼠标中键,不需要键盘修饰键。

6.8 定义LOGame类

现在,我们来创建游戏所需要的另一个类LOGame

使类定义模板在浏览器主窗口中可见。这可以通过点击包名或者右击Class面板来完成。输入清单6-11所示的代码,并Accept。

清单6-11 定义LOGame类

BorderedMorph subclass: #LOGame
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'PBE-LightsOut'

这里,我们创建了BorderedMorph的子类,Morph是所有图形形状的超类。我们已经看到了SimpleSwitchMorph,这是一种有开/关状态的Morph, 面BorderedMorph是带有边框的Morph。我们还可以在第二行的引号之间插入实例变量的名字,但是现在,我们暂时留空。

6.9 初始化游戏

我们来为LOGame定义一个initialize方法,在浏览器中输入清单6-12的内容作为LOGame的方法,然后Accept。

清单6-12 初始化游戏

LOGame >> initialize
    | sampleCell width height n |
    super initialize.
    n := self cellsPerSide.
    sampleCell := LOCell new.
    width := sampleCell width.
    height := sampleCell height.
    self bounds: (5 @ 5 extent: (width * n) @ (height * n) + (2 * self
        borderWidth)).
    cells := Array2D
        new: n
        tabulate: [ :i :j | self newCellAt: i at: j ]

代码有点多,不过不用担心。我们会在接下来的过程中补充细节。

Pharo 会抱怨它不知道 cells 的含义(参见图6-13)。它会提供几种解决方法。选择“声明新的实例变量”,因为我们希望 cells 成为一个实例变量。

图6-13 声明 cells 为一个新的实例变量

6.10 利用调试器

此时,如果你打开一个Playground,输入LOGame new,然后 Do it, Pharo会抱怨它不知道方法中某些术语的含义(参见图6-14)。它会告诉你LOGame不理解cellsPerSide这个消息,它会打开一个调试器。但是cellsPerSide并不是一个错误;它只是一个我们还没有定义的方法。我们将会定义它。

图6-14 Pharo 检测到一个未知的选择器。

不要关闭调试器。单击调试器中的Create按钮创建cellsPerSide方法,当出现提示时,选择LOGame,该类将包含该方法。单击Ok,然后在提示输入方法协议时输入accessing。调试器将动态地创建方法cellsPerSide并立即调用它。以这种方式生成的方法的默认实现是使用self shouldBeImplemented。然后求值,这会引发一个异常并再次打开调试器。

图6-15 系统创建了一个新方法,但方法体需要定义。

在这里,你可以编写新方法的定义。这个方法非常简单:它始终返回数字 10。将常量表示为方法的一个优点是,如果程序演进到常量依赖于其他特性,可以修改方法来计算这个值。

LOGame >> cellsPerSide
    "The number of cells along each side of the game"
    ^ 10

不要忘记在编写完方法定义后使用“Accept”来编译方法。你应该会得到类似于图6-16的结果。如果你按下“Proceed”按钮,程序将继续执行,但由于我们还没有定义 newCellAt: 方法,程序会再次停止。

我们可以使用相同的过程,但目前我们先停下来解释一下到目前为止所做的工作。关闭调试器,然后再次查看类定义(可以通过点击系统浏览器第二个面板中的 LOGame 来实现)。你会发现浏览器已经修改了类定义,增加了实例变量 cells

图6-16 在调试器中定义 cellsPerSide。

6.11 学习初始化方法

清单6-17 初始化游戏

LOGame >> initialize
    | sampleCell width height n |
    super initialize.
    n := self cellsPerSide.
    sampleCell := LOCell new.
    width := sampleCell width.
    height := sampleCell height.
    self bounds: (50 @ 50 extent: (width * n) @ (height * n) + (2 * self borderWidth)).
    cells := Array2D
        new: n
        tabulate: [ :i :j | self newCellAt: i at: j ]

让我们仔细看看LOGameinitialize方法。

第2行

在第2行,表达式 | sampleCell width height n | 声明了四个临时变量。它们被称为临时变量,因为它们的作用域和生命周期完全限定在方法内部。命名良好的临时变量有助于提高代码的可读性。第4-7行设置了这些变量的值。

第4行

我们的游戏板应该有多大呢?要足够大以容纳一定数量的单元格,并且还要有足够的空间在它们周围画出边界。而合适的单元格数量是多少呢?5个?10个?还是100个?我们现在还不清楚,即使我们认为自己知道了,将来也可能想要改变主意。因此,我们将确定这个数字的责任委托给一个方法——cellsPerSide,我们稍后会编写这个方法。不要因为这一点而感到困扰:在编写代码时引用尚未定义的方法是非常好的实践。为什么这么说呢?因为在我们开始编写初始化方法时才意识到我们需要它。所以当我们意识到将需要它的时候,就可以给它一个有意义的名字,然后继续定义其余的方法而不打断自己的思路。推迟这些决策和实现是一种超级能力。

因此,第4行 n := self cellsPerSide.self 发送了 cellsPerSide 消息。响应结果,即游戏板每边的单元格数量,被赋值给了临时变量 n

接下来的三行创建了一个新的 LOCell 对象,然后我们将它的宽度和高度设置为我们之前定义的宽度和高度临时变量。为什么这样做呢?因为我们使用这个 LOCell 实例来确定单元格的尺寸,因为持有这些信息最合适的地方就是在 LOCell 本身中。

第8行

第8行设置了我们新创建的 LOGame 的边界。先不用太担心具体的细节,相信我们,括号中的表达式创建了一个正方形,其原点(即左上角)位于点 (50,50),而其右下角则足够远,以留出适当的空间容纳正确的单元格数量。

最后一行

最后一行将 LOGame 对象的实例变量 cells 设置为一个新创建的 Array2D,该数组具有正确数量的行和列。我们通过向 Array2D 类发送 new:tabulate: 消息来实现这一点。我们知道 new:tabulate: 需要两个参数,因为它的名称中有两个冒号(:)。每个冒号后面跟着一个参数。如果你习惯于将所有参数放在圆括号内一起传递的语言,这可能会让你一开始觉得有些奇怪。不要慌张,这只是语法而已!实际上,这种语法非常出色,因为方法的名称可以用来解释参数的作用。例如,Array2D rows: 5 columns: 2 很明显是指5行2列,而不是2行5列。

Array2D new: n tabulate: [ :i :j | self newCellAt: i at: j ] 创建了一个新的 n×n 的二维数组(矩阵),并初始化其元素。每个元素的初始值将取决于其坐标。第 (i,j) 个元素将被初始化为 self newCellAt: i at: j 表达式的计算结果。这意味着对于数组中的每一个位置 (i,j),都会调用 newCellAt: 方法来创建一个新的单元格对象,并将其放置在该位置上。这种方法允许根据单元格的位置动态地初始化每个单元格。

6.12 将方法组织到协议中

在我们定义更多方法之前,让我们快速看一下浏览器顶部的第三个面板。就像浏览器的第一个面板让我们将类分类到不同的包中一样,协议面板让我们将方法分类,这样就不会在方法面板中被长长的方法名称列表所淹没。这些方法组被称为“协议”。

默认情况下,你会有一个实例侧虚拟协议,其中包含类中的所有方法。

如果你一直跟随这个例子,协议面板可能已经包含了初始化(initialization)和重写(overrides)协议。这些协议在你重写 initialize 方法时会被自动添加。Pharo 浏览器会自动组织方法,并尽可能将它们添加到相应的协议中。

浏览器是如何知道这是正确的协议的呢?好吧,一般来说,Pharo 并不能确切地知道。但是,例如,如果超类中也有一个 initialize 方法,它会假设我们的 initialize 方法应该与它重写的那个方法放在同一个协议中。

协议面板也可能包含“尚未分类”(as yet unclassified)的协议。未被组织到任何协议中的方法可以在这里找到。你可以右键点击协议面板并选择“分类所有未分类的方法”(categorize all uncategorized)来解决这个问题,或者你也可以手动整理这些方法。

6.13 完成游戏

现在让我们定义 LOGame >> initialize 中使用到的其他方法。你可以通过浏览器或调试器来完成这项工作,但无论如何,让我们从 LOGame >> newCellAt:at: 开始,将其放在初始化(initialization)协议中:

清单6-18 回调方法

LOGame >> toggleNeighboursOfCellAt: i at: j

    i > 1
        ifTrue: [ (cells at: i - 1 at: j) toggleState ].
    i < self cellsPerSide
        ifTrue: [ (cells at: i + 1 at: j) toggleState ].
    j > 1
        ifTrue: [ (cells at: i at: j - 1) toggleState ].
    j < self cellsPerSide
        ifTrue: [ (cells at: i at: j + 1) toggleState ]
        
LOGame >> newCellAt: i at: j

    "Create a cell for position (i,j) and add it to my on-screen
      representation at the appropriate screen position. Anser the new cell"
    
    | c origin |
    c := LOCell new.
    origin := self innerBounds origin.
    self addMorph: c.
    c position: ((i - 1) * c width) @ ((j - 1) * c height) + origin.
    c mouseAction: [ self toggleNeighboursOfCellAt: i at: j ].

注:前面的代码不正确。它会产生一个错误--这是有意为之。

格式化

正如你所见,我们的方法定义中有一些缩进和空行。Pharo 可以为你处理这种格式化:你可以在方法编辑区域右键点击并选择 Format(或者使用 Cmd-Shift-F 快捷键)。这将会把你的方法格式化得非常整齐。

相邻切换

上面定义的方法创建了一个新的 LOCell,并将其初始化为 Array2D 单元格数组中的位置 (i, j)。最后一行将新单元格的 mouseAction 定义为块 [ self toggleNeighboursOfCellAt: i at: j ]。实际上,这定义了鼠标点击时的回调行为。因此,相应的 toggleNeighboursOfCellAt:at: 方法也需要被定义:

toggleNeighboursOfCellAt:at: 方法会切换位于单元格 (i, j) 北方、南方、西方和东方的四个相邻单元格的状态。唯一的复杂之处在于游戏板是有限的,所以我们必须确保在切换状态之前相邻的单元格确实存在。

将这个方法放在一个名为“game logic”的新协议中。(右键点击协议面板以添加一个新的协议)。要移动(重新分类)一个方法,你可以简单地点击它的名称并拖动到新创建的协议中(参见图 6-19)。

将一个方法拖动到一个协议中

6.14 最终的LOCell方法

为了完成“Lights Out”游戏,我们需要在 LOCell 类中定义两个额外的方法来处理鼠标事件。

首先是mouseAction:在清单6-20中是一个简单的访问器方法:

清单6-20 一个典型的setter方法

LOCell >> mouseAction: aBlock
  mouseAction := aBlock

我们会把它放在accessing协议中。

最后,我们需要重写mouseUp:方法。如果在光标位于屏幕上的单元格上方时松开鼠标按钮,则此函数将由图形用户界面框架自动调用:

LOCell >> mouseUp: anEvent
    self toggleState.
    mouseAction value

首先,这个方法会切换当前单元格的状态。然后它会向存储在实例变量 mouseAction 中的对象发送 value 消息。在 LOGame >> newCellAt: i at: j 方法中,我们创建了一个块 [self toggleNeighboursOfCellAt: i at: j],当这个块被执行时,它会切换指定单元格的所有邻近单元格的状态,并且我们将这个块赋值给了单元格的 mouseAction 实例变量。因此,发送 value 消息会导致这个块被执行,从而切换邻近单元格的状态。

6.15 使用调试器

就是这样:“Lights Out”游戏完成了!如果你按照所有的步骤进行了操作,你应该能够玩游戏了。这个游戏只包含了两个类和七个方法。在 Playground 中,输入 LOGame new openInHand 然后执行(Do it)。这样就会创建一个新的 LOGame 实例并将其打开,你就可以开始玩游戏了。

游戏将会打开,你应该可以点击单元格,看看它是如何工作的。好吧,理论就说这么多……单击单元格时,将出现调试器。在调试器窗口的上部,您可以看到执行堆栈,显示所有活动的方法。选择其中任何一个将在中间窗格中显示在该方法中执行的代码,并突出显示触发错误的部分。

点击标有 LOGame >> toggleNeighboursOfCellAt: at: 的那一行(靠近顶部)。调试器会显示发生错误时该方法中的执行上下文(参见图 6-22)。

调试器,选中了 toggleNeighboursOfCellAt:at: 方法。

在调试器的底部是一个显示所有作用域内变量的区域。你可以检查导致选中方法执行的消息接收对象,因此这里可以看到实例变量的值。你还可以看到方法参数的值,以及在执行过程中计算出的中间值。

使用调试器,你可以逐步步进评估代码,检查参数和局部变量中的对象,就像在 Playground 中一样评估代码,并且最令人惊讶的是,你可以在调试过程中实际修改代码。一些 Pharo 用户几乎总是使用调试器而不是浏览器来编程。这样做的优势在于,你可以看到你正在编写的方法将在实际执行上下文中如何运行,带有真实的参数。

在这种情况下,我们可以在顶部面板的第一行看到 toggleState 消息被发送给了一个 LOGame 的实例,而显然应该是一个 LOCell 的实例。问题很可能出在单元格矩阵的初始化上。浏览 LOGame >> initialize 的代码会发现,cells 被填充了 newCellAt:at: 方法的返回值,但当我们查看该方法时,发现那里根本没有返回语句!默认情况下,一个方法返回 self,而在 newCellAt:at: 方法中,self 确实是一个 LOGame 的实例。在 Pharo 中从方法返回值的语法是 ^

关闭调试器窗口,并在 LOGame >> newCellAt:at: 方法的末尾添加表达式 ^ c,以便该方法返回 c

通常,你可以在调试器窗口中直接修复代码,然后点击 Proceed 继续运行应用程序。在我们的情况下,因为错误出现在对象的初始化过程中,而不是在失败的方法中,最简单的方法是关闭调试器窗口,销毁正在运行的游戏实例(通过按下 Alt-Shift-Click 打开光环并选择粉红色的 x),然后再创建一个新的实例。

再次执行 LOGame new openInHand,因为如果你使用旧的游戏实例,它仍然会包含带有旧逻辑的块。

现在游戏应该能正常工作了……或者几乎如此。如果我们恰好在点击和释放鼠标之间移动了鼠标,那么鼠标所在的单元格也会被切换。这实际上是继承自 SimpleSwitchMorph 的行为。我们可以通过重写 mouseMove: 方法使其什么都不做来修复这个问题,如清单 6-24 所示。

清单6-24 重写鼠标移动动作

LOCell >> mouseMove: anEvent

现在我们终于完成了!

关于调试器

默认情况下,当 Pharo 中发生错误时,系统会显示一个调试器。然而,我们可以完全控制这种行为,使其执行其他操作。例如,我们可以将错误写入文件,甚至可以将执行堆栈序列化到文件中,压缩它,然后在另一个 Pharo 镜像中重新打开。在开发软件时,调试器可以帮助我们尽可能快地解决问题。但在生产系统中,开发人员通常希望控制调试器,以防止他们的错误过多地干扰用户的工作。

6.16 如果一切都失败了……

首先,不要紧张!编程时出现混乱是完全正常的。事实上,这几乎是常态而非例外。当你刚开始在 Pharo 中实验图形元素时,最令人烦恼的事情之一可能是屏幕上充斥着看似无法删除的小部件。不要惊慌。试着通过使用元点击(Option-Shift-Click 或等效方式)打开 Morphic 光环,选择菜单手柄中的“debug > inspect”来打开一个检查器。一旦打开了检查器,你就自由了:

  • 如果你正在检视游戏自身: self delete.

  • 如果你正在检视一个游戏单元格:self owner delete.

6.17 保存并分享Pharo代码

现在你已经让“Lights Out”游戏运行起来了,你可能想把它保存在某个地方,以便存档并与朋友分享。当然,你可以保存整个 Pharo 镜像,并通过运行它来展示你的第一个程序,但你的朋友们可能在他们的镜像中已经有自己的代码,不愿意为了使用你的镜像而放弃自己的代码。你需要一种方法,将源代码从你的 Pharo 镜像中提取出来,这样其他程序员就可以将其导入到他们自己的环境中。

我们向你展示了使用 Iceberg 和 Git 来保存、分享和版本控制项目的 Basics。你应该自由地再次使用这些工具来处理“Lights Out”游戏。如果你希望了解更多关于 Iceberg 的内容,并且感到迫不及待,那么你可以自由地跳到第 7 章。

如果您还在阅读本章,我们现在来看看如何将Pharo代码导出为文件。

6.18 将代码保存在文件中

你还可以通过将代码写入文件来保存你的包,这种操作在所有 Pharo 用户、Squeak 用户及相关社区中通常被称为“导出”(filing out)。在包面板中右键点击菜单,你会看到一个选项,选择 Extra > File Out the whole of the PBE-LightsOut package。生成的文件虽然或多或少是人类可读的,但实际上是为计算机准备的,而不是给人阅读的。你可以将这个文件通过电子邮件发送给你的朋友,他们可以将其“导入”(file it in)到他们自己的 Pharo 镜像中。

导出PBE-LightsOut包,如图6-25所示。您现在应该找到一个名为PBE-LightsOut.st的文件。与映像处在同一个文件夹中。用文本编辑器查看一下这个文件,感受一下代码在文件中的样子。如果您不想离开Pharo查看文件,请尝试在Playground中检视'PBE-LightsOut' asFileReference

图6-25 导出 PBE-LightsOut 包

打开一个新的 Pharo 镜像,并使用文件浏览器工具(System > File Browser)将 PBE-LightsOut.st 文件导入(参见图 6-26)。右键点击该文件并选择 Install into the image。验证游戏是否在新的镜像中正常工作。

图6-26 使用文件浏览器导入你的代码。

6.19 访问器的约定

如果你习惯于其他编程语言中的 getter 和 setter 方法,你可能会期望这些方法被命名为 setMouseActiongetMouseAction。Pharo 的约定不同。getter 方法的名称与它获取的变量名称相同,而 setter 方法的名称类似,但以一个冒号(:)结尾,以便它可以接受一个参数,因此这些方法分别命名为 mouseActionmouseAction:。这些 setter 和 getter 方法统称为访问器方法(accessor methods),按照惯例,它们应该被放在 accessing 协议中。在 Pharo 中,所有实例变量都是私有的,只有拥有这些变量的对象才能读取或写入这些变量,因此其他对象只能通过访问器方法来读取或写入这些变量。实例变量当然也可以在子类中访问,但你永远不能访问另一个对象的实例变量——即使是同一类的另一个实例或类对象本身的实例变量也不例外。

6.20 本章小结

在本章中,我们有机会巩固迄今为止学到的 Pharo 知识,并将其应用于使用 Morphic 框架编写一个简单的游戏。我们学习了如何在屏幕上的图形元素周围打开 Morphic 光环,如何操作这些元素,以及(重要的是)如何删除它们。我们还学习了如何“导出”和“导入”Pharo 包以快速共享代码。