了解在Flash中的编程工作
作者:
本文节选自《Flash MX professional 2004 第一步》(陈冰著)
第十八章 了解在Flash中的编程工作
妈妈,这扇大门好雄伟好辉煌啊,它通向哪里?
宝贝,它通向编程的世界,那是充满魔法的世界,进入那里,你将成为伟大的魔法师。
经历了此前整整十七章的学习,我们终于来到了编程世界的大门,大门敞开着,显然,它一直在等待我们的到来。
18.1 像软件设计师那样思考问题
我相信你们中有很多人只有很少或完全没有编程基础,否则,你就不会选择我写的这本《第一步》了。我们已经知道,在Flash中使用ActionScript编写脚本,与使用其他语言编程没有什么不同。而要最终锻炼成一名出色的软件设计师,你要做的第一件事就是要时刻提醒自己要像一个软件设计师那样来思考问题。
18.1.1 不要总想着逃避编程
对于在Flash中的编程而言,要做到像软件设计师那样来思考问题,最重要的一点就是不要总想着通过逃避编程来实现你的想法,很多人总是想方设法试图用非编程的手段来完成他所面对的一切问题,为此,他可以不惜代价,可以花费大量的时间,他会想出数不清的绕过编程的变通方法,如果这些方法也可以被称为算法的话,绝对会令许多软件设计师吃惊。
实际上,在很多时候,使用编程的方法可以被非编程的方法节省大量的时间,而且会产生更为真实的效果。很典型的一个例子就是表现各种随机运动,例如,大量雪花的飘落、鱼的游动。
18.1.2 认真的编写设计说明书
要像软件设计师那样来思考问题,你要努力地用逻辑清晰的语言来描述你想要实现的那个想法。这个过程就是软件开发中非常重要的编写设计说明书的过程。
听起来有些傲慢,但在你完成了一个详细的设计说明书后,你的工作中的95%就已经完成了。每当有人问我某个想法是否能实现时,我总是回答说只要他能够详细的描述出它,我就能够编出它来。对于Flash中的开发而言,一个设计说明书的全部就是一个对于Flash电影将如何执行和表现的详细而严谨的描述。一个好的说明书将花费大量的时间和工作,但这是值得的。当它完成时,它将承担起工作蓝图的职责。
一个人的关于资料要详细到何种程度的想法可能与另一个人不同,但越详细越好。当你在工作的前期投入了额外的精力后,它将不仅仅在沿着这条路的行进中为你节省下时间,它也将减少返工的机会。
编写一个极为详细的说明书所存在的一个问题是你很难做到充分的描述最终的程序,因为一个书面的说明书与最终的Flash电影差别巨大。简单点说吧,仅仅用语言来描述一个绘图中的颜色或在没有某些音乐设备的情况下描述一个歌曲的声音是不可能,这里存在着解释错误。但这并不意味着你应该草率的放弃编写说明书的整个过程。相反,你只需将它编写的足够详细使你有把握依靠它展开工作就可以了。另外,你一定要知道先前你所做的工作将对以后的工作产生杠杆作用。失之毫厘,谬以千里的事情经常是由不负责的设计说明书导致的。
18.1.3 要想到还有其他的可能
有些时候,一个实现似乎有一个清晰的逻辑,似乎很明显就该按照你设计的算法和逻辑来编写程序,但当开发进展到某一步时,你却好像遭遇到了一堵墙,无法再前进一步,很多初学编程的人会在这里投入大量的时间,不断的反复检查自己设计的算法的逻辑和程序代码的编写,对其进行无数次的小修小改,最终陷入泥潭。
然而,一个有经验的软件设计师在这种情况下,会很快意识到自己最初的判断可能存在根本性的错误。很多时候,复杂事物的背后是简单的逻辑,而简单事物的背后却潜藏着复杂的逻辑。彻底抛弃自己原先的算法,换一种思维,尽力去猜测另一种可能,你会发现,那堵墙开始消失了。
人们总是情愿用简单的方法去解决事情,在编程上也是一样,这是正确的思路,但有的时候,当简单的方法行不通时,你要意识到这件事情或许要用复杂的方法才能实现。
不要总把事情想的那么简单,总要想到还有其他的可能。
18.2 面向对象的软件开发中的重要概念
在第四章中我已经简单的介绍了什么是面向对象的软件开发,你应该还记得那个以“人”作为对象的例子,以及当路遇恶狗而被恶狗撵这一事件发生时,他可以调用“撒腿就跑”这个方法来作为对这个事件的响应。
但不管怎么说,我不想因为这些例子而使你轻视了面向对象的软件开发。事实上,面向对象含义深远,涉及许多重要的概念,只有充分理解了这些概念的含义,你才算真正理解了什么叫面向对象。很多概念你都应该知道,但限于篇幅,本节中我将讲解其中最重要的九个。
面向对象的软件开发中九个最基本的概念:类、对象、属性、方法、抽象、封装、继承、多态,以及事件。
18.2.1 类和对象
类是对象的软件抽象,是创建对象的模板。例如,如果一个人被看作一个对象的话,则人这个物种就可以被看作一个类。类定义描述了包括数据和功能在内的结构,对象由其创建。一个类表示一组相似的对象。对象是类的实例。
为什么需要类,想象这样一个情况,假如我们要编写一个Flash游戏,里面要有一些能自主活动的小人,这些小人都有相同的体貌特征和活动能力,那么,若我们要为每个小人编写程序的话,则工作量毫无疑问是巨大和重复的。在这种情况下,我们就应该创建一个“小人”类,以后每当需要一个新的小人时就从“小人”类生成一个小人即可。
话说到这里,你一定想到了元件和实例的关系。没错,类和对象的关系与元件和实例的关系一样,因为元件实际上就是Flash的内建类。
18.2.2 属性和方法
属性是数据,而方法是函数。属性是类知道的事情,而方法是类完成的事情。属性和方法都是类的职责。面向对象的软件开发是基于这样的概念:系统应由对象来创建,对象拥有数据和功能。属性定义数据,而方法定义功能。
显然,在面向对象的开发中,最重要的工作就是定义类。而定义类时,就必须定义它的属性和方法。属性的定义应该是直接明了的,需要定义它的名称和数据类型。方法的定义就是创建一个函数的过程,根据需要,你可以创建出能够接受参数且能够返回值的方法。
18.2.3 抽象
世界非常复杂,为了处理它的复杂性,在软件设计中,当需要对事物建立数学模型时,我们有必要对事物进行泛化或抽象。还是以人作为对象,从招聘的角度看,我们需要知道这个人的姓名、性别、年龄、教育背景、工作经历,联系方式,以及性格特点;而从相亲的角度看,则需要知道这个人的年龄、身高、外貌、教育背景、家庭背景,甚至生辰八字。还是同一个人,只是对他(或她)进行了不同的抽象而已,对一个事物进行怎样的抽象将依据你要设计的程序而定。
抽象是个分析的过程,是在事物周围绘制出一个清晰的框架的过程。抽象应该包含应用程序感兴趣的功能、属性、方法,而将其他因素忽略。这就是为什么招聘的抽象包含教育背景和工作经历,但不会包含生辰八字的原因。抽象的过程,就是定义类知道和要完成的事情的过程。
18.2.4 封装
尽管抽象告诉我们,对于招聘,我们应该存储应聘者的教育背景和工作经历,但它没有告诉我们该怎样完成这些事情。封装解决了如何为系统功能建模的问题。在面向对象的世界中,我们要把系统建模成类,类也要建模为属性和方法。设计类将如何完成这些事情的过程就被称为封装。
封装描述了如何在系统中划分功能的问题。我们并不需要知道对象内部是怎样实现的。封装暗示着我们能够以任何方式构建系统,如果需要的话,还可以在日后再次修改其内部的结构,只要系统中不同功能组件之间的接口没有发生变化,那么对系统中一个功能部分的改变不会对系统的其他功能部分产生影响。
封装是把事物周围的那个框架涂黑的过程。你可以定义任何想要定义的东西,但你并不需要告知外界你的做法。例如,当你到银行存取现金的时候,你并不需要知道银行的数据中心是如何使用大型机、小型机和PC机来记录你的账户信息的,它们使用着怎样的数据库和操作系统,也根本无关紧要,因为它们已经把账户服务的功能封装起来了。你只需走到柜台前,把存折递给银行的工作人员既可。通过隐藏起实现账户功能的细节,银行能够在任何时候自由改变功能实现,而不用费事得更换每一个用户的存折。
为了让应用程序容易维护和增加安全性及健壮性,需要限制对类的属性和方法的访问。基本思想如下:如果一个类想获取另一个类的信息,必需先征得同意,而不是直接拿来就用。考虑一下,在现实世界中其实也是按照这种方法工作的。如果你想了解其他人的姓名,你该怎么做?是直接询问这个人他的姓名呢,还是偷走他的钱包看看他的身份证呢?
18.2.5 继承
不同的类之间经常会存在相似性。两个以上的类也会经常共享相同的属性和/或相同的方法。因为我们并不想重复编写代码,因此我们就要利用这种相似机制。继承就是这种机制,它使你可以很容易的重用现有的数据和代码。
例如,学生都有姓名、地址、手机,也都能快跑。同时,老师也都有这些东西。毫无疑问,我们可以开发有关学生和老师的类,让它们一起运行。实际上,仅需要先开发Student类,一旦它运行起来了,制作一份拷贝,就叫Teacher类,并对其进行一些适当的修改即可。这样做很容易,但并不完美。如果Student类中的源代码出现错误将会怎样?你将不得不在两个地方改正错误。这将是索然无趣的工作。如果仅有一份代码拷贝用于开发和维护,这样不是更好吗?
这就是继承的思想。使用继承,就可以定义一个类来封装学生和老师之间的相似性。新的类将有属性name、address、mobileTelephone,以及方法run。我们可以把这个类命名为Person。
一旦定义了Person类,就可以从它继承出Student和Teacher类。我们称Person是Student和Teacher类的超类,而Student和Teacher类则是Person类的子类。任何超类知道和完成的事情,子类也知道。因此,既然Person类遇到恶狗时能撒腿就跑,Student和Teacher类也能。
18.2.6 多态
让我们考虑一下这个例子。你很爱你的女友,你对她说“亲爱的,我爱你”,你的女友热情的拥抱你,你还得到了一个吻,你觉得这很不错。然后,你来到了大街上,看到一位很有气质的美女,你也用跟女友打招呼的方式对这位美女说“亲爱的,我爱你”,你将立刻得到有关你的神经和品德方面的一些建议。随后,你又来到医院,对一老中医说“亲爱的,我爱你”,老中医将毫不犹豫的拉过你的手腕,开始为你把脉。你心想,无论何时当我遭遇这些对象并说“亲爱的,我爱你”时,这种情况肯定就会发生—这就是多态。
多态使得对象可以在事先不知道其他对象的类型时就与其他对象协作。
从这次经历中你可以学到几个有意思的经验:
第一,多态阻碍了你与其他对象之间的交互。你并不区别其他对象的类型,你以同样的方式对待他们。你的想法是其他对象都是人。多态是允许这种情况发生的一个概念。
第二,不同的对象以他们自己的方式作出不同的响应。你的女友拥抱你并给你吻;陌生的美女给你有关神经和品德方面的建议;而老中医则给你把脉。同一消息到达不同的对象,每一个完成的事情都是不同的。实际上,从各个对象的角度看,他们做了应该做的事情。
第三,多态可以实现一致而恰当的方法名。尽管每种对象都以适当的方式响应了“亲爱的,我爱你”,但还是要实现方法的不同版本。
多态性主要包括两个方面:
运算符的重载:同一运算符可以作用在多种对象类型上。
函数名重载:相同的函数名可以作用在不同的对象类型上,并产生不同的效果。
18.2.7 事件
面向对象的软件的运行是随着一个个事件的发生来进行的,这被称为“事件驱动”。例如,当你欣赏Flash动画时,实际上是正在发生一个个“进入帧”事件,在这些事件中,Flash Player会呈递每帧中的内容,所以你就能看到画面。
同样的,当你点击按钮时,一个按钮事件就发生了,该按钮事件中的脚本就被执行,这些脚本的作用可能是把你带到特定的场景、特定的帧,或是链接到某个URL。
在面向对象的程序设计中,整个软件都是以事件驱动的,这意味着假如没有任何事件发生,你的程序将寸步难行,不过好在程序中总是有某些事情发生的,即使所有其他的事件都不发生,也会有时间流逝这一事件在时刻发生着。
在本书后面章中的实际开发中,你将看到上述所有这些面向对象的概念的实际运用。
18.3 好的编程风格
尽管你将进行先进的面向对象的软件开发了,但一些在面向过程的年代就已经总结出的好的编程风格在任何时候(至少在可预见的未来)都不会过时。本节将教给你这些放之四海皆准的规则,这些规则不是强制性的,但遵守它们毫无疑问会使得你的生活变得容易。
好的风格意味着以一种易维护的方式进行编程。你的代码应该容易到足以使任何人都能够理解它。这倒不是说其他人需要查看你已经编写的这些代码(当然这种情况可能会发生),而是说当你需要进行调试或修改错误时,你能够快速的理解你已经开发出的到底是些什么东西。人们总是很容易失去自制力的试图去建立某些东西而忽视了有益的整理工作。草率的行事将引起无限的烦恼,因此你应该总是尽力遵循好的编程风格。
当然,“好”不能轻易得到,“好”通常就意味着大量的工作。在这里,好就意味着:好的名字、减少重复、总是注释,以及分离代码和数据。
18.3.1 好的名字
什么是好的名字?在编程世界中的好名字的概念和我们日常生活中好名字的概念有很大的不同。在我们的日常生活中,一个好名字往往意味着非常值得推敲:或者是表达父母的良好愿望,或者是字典中绝妙的解释。但一个日常生活中的好名字用在程序中却是很糟糕的,一个这样的名字无法提供除名字之外的其他信息。
在编程世界中,判断一个好名字的标准是是否能够以最少的字符提供更多的信息。在Flash中,我们可以且需要命名的东西是非常多的,每一个按钮或电影剪辑的实例,每一个文本实例,都有可能需要命名;每一个变量,每一个函数,每一个类,都必须命名。
在Flash中,当你命名一个事物的时候,你应该尽量让这个名字反映出这个事物的所有重要的信息。例如:fishCounterMC作为一个统计鱼缸中鱼的数量的电影剪辑的实例名就很不错。很多天后,当你再次见到这个名字时,你能够在瞬间获得许多重要的信息。首先,一打眼,我们就能知道这个名字指代的是一个电影剪辑(MC表示MovieClip),因此,这个名字是一个电影剪辑实例的名字,其次,我们能够看出这是一个用来计数的电影剪辑(根据Counter),最后,我们可以推断这个计数器应该是来统计鱼的(根据fish)。
当为变量命名时,能够在变量名中体现出这个变量的数据类型将是很有益的。childAge_Num作为一个用来保存孩子年龄的变量的名字会是不错的,从Num这个后缀我们可以意识到这个变量应该保存的是数字数据类型。
有的时候,为了给事物赋予一个更有意义的名字,传递更多的信息,你会发现名字正在变得越来越长,这不是好事情,太长的名字同样会造成阅读的困难。因为太多长名字堆积在一起会使得你看不清程序的逻辑,因此,对待任何事情,你都应该保持适可而止的态度,很多时候,你需要在传递更多的信息和防止太长的名字之间进行妥协。
18.3.2 减少重复
要使事情变得简单,每一个编程工作应该在你的电影中只出现一次。如果你的同一段代码出现在两处,那么你的更新和修改Bug的工作也将加倍。你将学到一些方法来实现这点,譬如说将脚本保存在库中、保存在函数中或从外部引入电影。任何时候,每当你打算拷贝和粘贴代码时,一个不大的声音应该在你的头脑中响起—“住手!”。总会有一个更简练的方法在等待着你。
减少重复还意味着精炼代码,尽量用更少的代码来完成同样的工作。细想一下,当你为调试Bug而回头检查你的程序时,你所检查的每一行代码都必须在你的头脑中进行翻译,更少的行你必定读起来会感觉更好。通常,任何时候你都可以以较少的步骤或更少的代码来做某些事情。比较在代码1和代码2中的两个代码段,它们实现的是同样的效果,但代码2中的代码要精炼的多。
代码一:
on (release){
setProperty ("highlight", _x, getProperty ("highlight", _x)+10);
tellTarget ("highlight"){
gotoAndStop(getProperty("",_currentframe)+1);
}
}
代码二:
on (release){
highlight._x+=10;
highlight.nextFrame();
}
那些在代码一中的脚本实现的是与代码二中的脚本同样的效果,只是添了没有必要的复杂性而已。这除了能引起比你水平低很多的选手的崇拜外,没有更多的意义。
诚然,没有什么方法是法定的最好的方法,但减少重复和精炼代码毫无疑问是有益的。当然,你也没有必要把减少重复,精炼代码这点贯彻得太彻头彻底。精简代码的要求不应重于易读性。彻底失去自制力并最终终结于一堆连你自己都无法阅读的代码是很容易的事情。我永远都不会在一个能够工作的已完成的代码段中挑刺儿—因为,说真的,这才是最优先要考虑的。其次,你的代码通常都是由你来维护的,因此,编写出你能够阅读和理解的代码是最重要的。尽一切可能使用你能够理解的代码。如果这有时意味着你的代码罗嗦一点的话,那也只能由它去了。随着时间的推移,慢慢地你将看到你的代码正在逐渐缩短。
有时,当我审视那些仅仅是几个月前才编好的程序时,我也会质疑我当时所采用的方法—但这仅仅是因为我总是在进步。如果你打算等到你的技术变得完美时再做的话,你将等待太长的时间。因此就这样投入进去吧,时间自会证明你能够进步。
18.3.3 总是注释
在Flash中,注释是以//开始的文本。注释在Flash中是被忽略的代码行。注释绝不是Flash的特点,翻开任何一本涉及编程的计算机书,你都会发现里面有有关注释的重要性的论述。的确,注释是非常重要的,怎么强调都不会为过。注释能使你在数月甚至数年后仍能知道每段代码的作用,仍能够继续对程序进行后续开发和维护;能够在必要时候,让别人看懂你的代码,他(她)会一边看一边体味你的仁慈,并心怀感激,发誓也要做像你这样的好男孩(或好女孩)。
我认为我是一个坏男孩,因为我经常做不到对我的代码进行充分的注释,直到我让它运行起来为止。但不管怎么说,没有将这一步继续拖延下去对我来说很重要,因为在我写出它的几天之后,我会将有关这段代码的一切忘的一干二净。没有注释,代码的理解将变得困难的多。因此,花上一些时间去注释你的代码吧,即使你已经完成了你的代码且热情也日渐下降。比较一下代码三中没有注释的代码和代码四中同样的一些但做了注释的代码。尽管你目前可能不明白这些代码的细节,但如果这里存在问题的话,你将能够轻松的识别出包含问题的部分。
代码三:
OnClipEvent (keyUp) {
if (Key.getAscii()==13 | Key.getAscii()==0){
return;
}
if (Key.getAscii()==8){
if (cur.charAt(cur.length-2)==" "){
_root.wordThisTime--;
}
cur=cur.slice(0, cur.length-2)+mbchr(8);
if (_root.wrongPlace[_root.place-1]=="x"){
_root.wrongPlace.pop();
_root.wrongs--;
}
_root.place>0 && _root.place--;
return;
}
}
代码四:
OnClipEvent (keyUp) {
//忽略这些字符
if (Key.getAscii()==13 | Key.getAscii()==0){
return;
}
//假如他们点击退格键
if (Key.getAscii()==8){
//删除一个空格?
if (cur.charAt(cur.length-2)==" "){
_root.wordThisTime--;
}
//删除最后的字符
cur=cur.slice(0, cur.length-2)+mbchr(8);
//他们修改了一个错误吗?
if (_root.wrongPlace[_root.place-1]=="x"){
_root.wrongPlace.pop();
_root.wrongs--;
}
//后退一格
_root.place>0 && _root.place--;
//离开
return;
}
}
代码三在被注释前,其中的代码理解起来很困难。代码四说明了不多的一点注释就使得事情变得明朗,即使你还不明白这些代码的潜在含义。
18.3.4 分离代码和数据
所有的程序设计师都应该力争使代码(简单的说就是编程脚本)和数据(或说特定工程的内容,例如:文本和图形)保持分离。通过保持代码和数据的分离,你能够使你的编程成果轻松的移植到其他的工程。同样的,当你想要对内容作重大的改变时—比如说,以不同的语言重做整个工程—你只需要替换数据而不必去碰(或破坏)那些代码。这是伟大的思想但有时难以实现。
假定你的Flash站点中有一些图形按钮,当用户将他的鼠标指针放置到按钮之上时这些图形按钮将显示出一个浮动的工具提示。如果你保持代码(使工具提示出现的脚本)和数据(实际将出现在工具提示中的文本)分离。你能够轻松的将这个功能移植到另一种语言,而你所做的只是用另一种语言的文本来替换工具提示。理想的情况是,你将所有工具提示的所有文本保存在同一个位置,这将使得移植工作变得更加的轻松。这一思想的主旨是你应该尽量做到对代码或数据中任何一个所做的重大改变都不会对另一个产生影响。
你可以将代码数据分离视为一种模块化形式。还有一些其他的模块化形式—包括Flash的loadMovie(),它使你能够在一个更大的电影的内部播放独立的.swf文件。除了所提及的代码数据分离之外,模块化还有许多其他的优点。首先,通过模块化你的Flash电影,用户们不需要等待整个电影的下载。他们可以有选择的下载那些那他们感兴趣的部分。此外,模块化也使得一个工程可以被干净的剥离成几个独立的部分,从而使得这些部分可以同时进行开发。思考这样一种情况,如果你的整个电影仅由一个基本文件构成,则同时只能有一个人可以工作。因此,可以看出,代码数据分离及其它的一些模块化形式有着许多的优势,你应该尽量保持以模块化的思想来考虑问题。
18.4 ActionScript术语
我们已经知道了不少ActionScript的术语了,比如类、对象、属性、方法、事件、脚本等等。本节中将再介绍一些ActionScript中常用的术语,以便我在后面提及时,你不会感觉陌生。
标识符—是一个符合特定规则要求的字符串。具体的说,这个字符串的第一个字符必须是字母、下划线(_)或美元符号($),其后的字符必须是字母、数字、下划线或美元符号。程序中所有需要命名的东西(例如:变量或实例)的名称都应该是一个标识符。
实例—就是指对象。类的实例就是对象。很多人总是分不清类、对象和实例三者的关系。记住,对象就是从它所属的那个类产生的。例如,当我们创建学生对象时,我们就说从Student类中把它实例化了。
函数—是可重用的代码段。一个函数完成一个特定的任务。你可以向函数传递参数,也可以从函数中返回值。
参数—是用于向函数传递值的占位符。在下面的area函数中,x和y是该函数的两个参数。
function area(x,y){
return x*y;
}
当你像下面这样调用area函数时,就通过其参数向该函数传递了所需的两个值,这两个值在函数中进行运算,并将其运算结果返回到调用位置。
rectangleArea= area(2,3);
ActionScript中还有很多术语,我将在书中涉及它们时再进行讲解