gnu smalltalk 教程-创建新的类

通过前几章的学习,我们已经掌握了一些基础知识,现在让我们开始作真正的Smalltalk程序。在本章我们将构建3个新类型对象,并使用继承使之联系到一起,创建的新对象都属于类(类的实例),并且发送消息给类。

我们将通过创建一个家庭财务会计系统来练习这些。我们将跟踪你的全部现金,将会有一个特殊的处理来检查和存储。我们将定义一些将在以后用到的类。既然你可能将在以后运行这整个教程,他就应该可以漂亮的存储,恢复Smalltalk的状态,而不需要重新输入之前的例子。存储当期Smalltalk状态需键入:

ObjectMemory snapshot: 'myimage.im'

从shell回复者个快照:

gst -I myimage.im

这样一个不大于几兆字节的快照包含了所有的变量,对象,类和你添加的一切

创建一个新类

猜猜如何创建一个新类?应该是简单的向一个对象发送一个消息。通过这种方式我们创建第一个自己的对象:

Object subclass: #Account.
Account instanceVariableNames: 'balance'.

相当拗口,不是么?gnu Smalltalk提供一种简单的方式去实现这一功能,但是目前让我们先忍受这些。从概念上说,这不是很差。Smalltalk的变量Object绑定在系统中所有类的鼻祖上。我们在这做的是告诉Object类,我门想要添加一个子类作为Account。之后instanceVariableNames: 'balance' 告诉新的类的所有的对象有一个隐藏的balance变量。

对象文档
下一步是为类关联一个描述。你同说发送下列消息给新类:

Account comment:
       'I represent a place to deposit and withdraw money'

描述被关联到每一个Smalltalk的类,你需要为每个类添加一个恰当的描述。你可以通过如下方法取得类描述:

Account comment

你的描述将会被打印给你。试着在Integer上打印描述:

Integer comment
'I am the abstract integer class of the GNU Smalltalk system.  My
subclasses'' instances can represent signed integers of various
sizes (a subclass is picked according to the size), with varying
efficiency.'

然而,这里另一种方式去定义一个类。这同样是发送消息给对象,但是看起来更像是传统编程语言:

Object subclass: Account [
         | balance |
         <comment:
             'I represent a place to deposit and withdraw money'>
     ]

这里创建了一个类,如果我们想在通过一次,比如修改描述信息,我么可以这样做

Account extend [
         <comment:
             'I represent a place to withdraw money that has been deposited'>
     ]

这个命令让Smalltalk去取出存在的类,而不是创建一个新的子对象

定义类的方法
我们已经定义了一个类,但是它依旧没有准备好处理消息。我们开始定义方法来创建实例:

Account class extend [
            new [
                | r |
                <category: 'instance creation'>
                r := super new.
                r init.
                ^r
           ]
        ]

关于这里的重点是:
Account 类意味着我们正在定义的消息将发送消息给自己。
是多文档支持; 它说明我们定义的方法支持创建Account对象。
以new [开头知道]结尾的文本定义了对于消息new的响应。当你进入这段定义,除非你的方法已经被编译使用,否则gnu Smalltalk将会给你其他的提示。
gnu Smalltalk成功定义方法的时候非常安静,如果除了问题,你会得到一大堆错误消息。
如果你非常熟悉其他Smalltalk,请注意method的主体总在方括号内

最好的描述方法如何工作就是运行他,想想我们发送消息给新的类Account用如下命令:

Account new

Account 接收到消息new,然后查找处理这个消息的代码。他发现我们的new定义,然后开始运行它。第一行| r |创建了一个名为r的将被用作我们创建对象的占位符的本地变量。r将会抛弃放当整个过程处理结束。注意以下与之相同的balance,当对象不再使用。同时注意,你必须在这里使用本地变量,并不像你在之前例子所作的。

在创建对象过程中第一个真正处理的步骤。在行 r:= super new 用了一个有趣的手段。super代表了相同的消息new被发送的对象(记得吗?是Account),此外当Smalltalk开始寻找方法,其开始于一个比当前更高的层次。对于Account的方法是Object类(应为类Account继承自其,你可以回头看看我们如何创建的它),Object类的方法之后响应#new消息运行一些代码。随着其结束,Object将会在发送#new消息时创建创建对象。
再一次慢动作:Account的方法#new在object被创建的时候做了一些无足轻重的事,但是,他也想运行其父类的同命方法。通过 r:=super new令他的父类创建对象,之后绑定到变量r。所以当这行运行之后,我们开始有了Account的r绑定的新对象。随着时间的流逝,你将会更好的理解这一点,但是现在先郁闷吧,把它当作是秘籍后继续。
我们有了新的对象,但是我们没有设置正确。记得我们第一章看到的隐藏变量balance吗?super new给了我们什么都没有的balance的空间,但我们想要balance从0开始

所以我们需要的是要求对象设置自己。当我们执行r init时,我们发送init消息给我们的新Account对象。我们将在下面定义这个方法-目前,仅仅假设发送init消息将会使Account设置。

最后,我们执行 ^r。在英语中,这是返回属于r的。这意味着无论谁发送new消息给Acount都将绑定新的account对象作为返回。同时,我们的缓存r终止退出。

定义一个实例方法
我们需要定义为Account对象定义init方法,so that我们的new方法在之前定义的那样工作
Smalltalk code:

     Account extend [
         init [
             <category: 'initialization'>
             balance := 0
         ]
     ]

看起来和之前定义的有一点像,除了最开始声明Account类扩展,代码所写的Account extend
区别在于第一个已直接被Account创建,而第二个需要在消息第一次被发送给Account对象时才被创建。

init方法仅仅一行,balance := 0。它初始化了隐藏变量balance(这一动作被叫做变量实例化)成为0,对于账户结余而言说得通。注意这个方法没有用^r或类似的方式作为结束,其没有给消息接收者没有返回值。当你没有特殊的返回值时,Smalltalk默认返回当前执行的对象。为编写更加清晰的代码,你需要使用其自生时,应当在其后返回自生。(最所以不返回nil,也许是他们不欣赏返回void值的函数。毕竟最终Smalltalk还是设计了,C甚至没有void数据类型)

看看我们的Account
让我门创建一个Account的实例

        a := Account new

你能猜到这些代码在干什么吗?Smalltalk在 at: #a put: 创建列一个Smalltalk变量。Account new 创建了一个新的Account然后返回其作为结果。所以这行代码创建了一个名为a的变量,然后链接到一个新的Account对象。当期被创建事会打印Account对象:
an Account
恩。。不是非常有用。问题是我们没有告诉我们的Account如何打印其自生,所以我们仅仅得到了系统默认的printNl方法输出的对象是什么。为了得到清晰的返回,我们必须添加一个方法

         Account extend [
             printOn: stream [
                 <category: 'printing'>
                 super printOn: stream.
                 stream nextPutAll: ' with balance: '.
                 balance printOn: stream
             ]
         ]

现在再试一遍

a
an Account with balance: 0

似乎变得好了点。我们添加了一个新的printOn:方法,改变了printNl消息的行为。原因是你一旦定义了printOn:就改变了打印函数。所有的其它打印方法最终调用它。他的参数是打印的地方--通常其是一个变量副本这个变量通常勾在你的终端上,正是这样你在你的屏幕上得到打印输出。
super printOn: stream让对象的父对象先运行,打印出我们的对象类型。一部分打印输出来自这里。stream nextPutAll: ' with balance: '创建with balance字符串并输出到stream。注意:我们没有在这里用printOn:应为会把两个引号也算进去。最后的balance printOn: stream要求把变量balance 打印自生到stream加入到对象。我们之前设置balance为0。

修改数据
我们可以创建记录,查看。随着它的建立,虽然我们的结余balance总是0,多么悲剧。我们最后的方法家会让我们存钱或者花钱。以下是代码

        Account extend [
            spend: amount [
                <category: 'moving money'>
                balance := balance - amount
            ]
            deposit: amount [
                <category: 'moving money'>
                balance := balance + amount
            ]
        ]

随着这个方法的建立,你现在可以存钱和花钱了,试着如下操作:

        a deposit: 125
        a deposit: 20
        a spend: 10

接下来该干什么?
现在,我们已经有了一般的的概念,一个Account类。我们可以创建他们,检查余额,把钱存入或移出。他们提供了一个不错的基础,但是省去了一些重要的账户需要的特殊信息,下一章,我们将着重用subclass修复这些问题。