前几天在给上小学六年级的小孩进行了一次Swift的编程教学,感觉有点意思,在这里记录一下。
教学环境是iPad上的Swift playgrounds,课程是程序设计2中的一个关卡。这个关卡的要求是控制两个角色来完成捡宝石和踩开关的任务,稍麻烦的是如何设计角色的行走路径。具体关卡如下图所示:
好了,现在开始来编程解决这个问题吧。第一个最简单直接的程序如下:
let expert = Expert()
let character = Character()
expert.turnLeft()
expert.moveForward()
expert.moveForward()
expert.moveForward()
expert.turnRight()
expert.moveForward()
expert.moveForward()
expert.turnLeft()
expert.turnLockDown()
expert.turnLockDown()
expert.turnRight()
expert.moveForward()
expert.moveForward()
expert.trunRight()
expert.moveForward()
expert.moveForward()
expert.moveForward()
expert.moveForward()
expert.moveForward()
expert.moveForward()
expert.turnRight()
expert.moveForward()
expert.moveForward()
expert.turnLeft()
expert.turnLockUp()
character.moveForward()
character.moveForward()
character.collectGem()
character.moveForward()
character.moveForward()
character.toogleSwitch()
上面这个程序很直接,但是有很多重复的moveForward()函数调用,如何消除这些重复的函数调用呢?最直接的解决方法就是使用for循环来代替这些重复的调用,于是我们有了如下的使用了for循环的第二个程序,如下所示:
let expert = Expert()
let character = Character()
expert.turnLeft()
for i in 1..3 {
expert.moveForward()
}
expert.turnRight()
for i in 1..2 {
expert.moveForward()
}
expert.turnLeft()
expert.turnLockDown()
expert.turnLockDown()
expert.turnRight()
for i in 1..2 {
expert.moveForward()
}
expert.turnRight()
for i in 1..6 {
expert.moveForward()
}
expert.turnRight()
for i in 1..2 {
expert.moveForward()
}
expert.turnLeft()
expert.turnLockUp()
for i in 1..2 {
character.moveForward()
}
character.collectGem()
for i in 1..2 {
character.moveForward()
}
character.toggleSwitch()
这回这些重复的moveForward函数调用都没有了,不过我们仔细看看这些for循环,可以发现除了1..2,1..3,1..6中的2,3,6不同外,其他都是一样的。如果我们将2,3,6使用一个变量distance来代替,那这些for循环就完全一样了。于是我们将for循环放到一个函数中,把distance变量作为这个函数的参数,其值在调用这个函数时设置为2,3,6这几个不同的值就可以了。嗯,看起来不错,于是有了下面的第三个程序:
let expert = Expert()
let character = Character()
func moveExpert(distance: Int)
{
for i in 1..distance {
expert.moveForward()
}
}
func moveCharacter(distance: Int)
{
for i in 1..distance {
character.moveForward()
}
}
expert.turnLeft()
moveExpert(distance: 3)
expert.turnRight()
moveExpert(distance: 2)
expert.turnLeft()
expert.turnLockDown()
expert.turnLockDown()
expert.turnRight()
moveExpert(distance: 2)
expert.turnRight()
moveExpert(distance: 6)
expert.turnRight()
moveExpert(distance: 2)
expert.turnLeft()
expert.turnLockUp()
moveCharacter(distance: 2)
character.collectGem()
moveCharacter(distance: 2)
character.toggleSwitch()
将for循环的行为抽象为函数后,我们的程序的主流程变短了很多,更清晰,意思更明确,也更容易读了。不过还有不足的是这两个move函数moveExpert和moveCharacter的实现几乎是完全一样的,除了expert和character的区别。能否统一两个函数呢,首先想到的是将expert和character也使用一个变量来替代,作为参数传入进来。但expert和characer的类型是不一样的,分别是Expert和Charater,似乎不能使用一个变量来替代。不过注意到Expert是Character的子类型,根据子类型替换原则,我们可以使用Character类型的变量来表示Expert类型的变量。于是我们就可以使用一个变量来替代expert和character了,就可以将函数moveExpert和moveCharacter统一为一个函数move了。代码如下:
func move(sprite: Character, distance: Int)
{
for i in 1..distance {
sprite.moveForward()
}
}
我们再来仔细看看expert角色的行走路径,可以发现是存在一个模式的,即先往前,拐弯,然后再往前。我们可以把这个共性的行为抽象出来,将行走模式抽象为一个函数moveTurnMove,我们就得到了第四个程序。
let expert = Expert()
let character = Character()
func move(sprite: Character, distance: Int)
{
for i in 1..distance {
sprite.moveForward()
}
}
func moveTurnMove(distance1: Int, isTurnLeft: Bool, distance2: Int)
{
move(sprite: expert, distance: distance1)
if isTurnLeft {
expert.turnLeft()
} else {
expert.turnRight()
}
move(sprite: expert, distance: distance2)
}
expert.turnLeft()
moveTurnMove(distance1: 3, isTurnLeft: false, distance2: 2)
expert.turnLeft()
expert.turnLockDown()
expert.turnLockDown()
expert.turnRight()
moveTurnMove(distance1: 2, isTurnLeft: false, distance2: 3)
moveTurnMove(distance1: 3, isTurnLeft: false, distance2: 2)
expert.turnLeft()
expert.turnLockUp()
move(sprite: character, distance: 2)
character.collectGem()
move(sprite: character, distance: 2)
character.toggleSwitch()
最终,我们得到了一个主流程更短的程序,更清晰明确了,读起来更容易理解了。当地图更大更复杂,角色的行走路径更多步骤时,我们就可以节约很多代码,而且更不容易出错。良好的抽象层次可以减少大量重复的代码,而且使程序的行为更加清晰明确,不容易出错,使得读代码的人也更轻松了,不会一下子就陷入到细节中去。
这次教学到这里就结束了。通过多次修改通关的程序,我们从一个简单直接,但抽象度低,冗长啰嗦,不好阅读的程序开始,一步一步的提升抽象层次,得到了明确清晰,容易阅读的程序。这也是我们在工作实践中设计复杂程序的一种良好的方法,从一开始让小孩熟悉这种设计程序的方法是很有益处的。
这个程序还有重复的代码没有去除,moveTurnMove函数也还可以写的更通用些。这些就留给有兴趣的读者来思考吧。