【用代码说话】#2.史莱姆巢穴

上一篇教程中,我曾举了一个史莱姆的例子来说明为什么要使用面向对象。不过,略微有些遗憾的是,上一篇的知识只是面向对象思想的冰山一角,所以,这一篇可以看作是对上一篇内容的一个延续和补充。通过这一篇的内容,我希望能够让你真正开始了解面向对象为了什么、做了什么、抽象了什么。当然,我承认,即使搞不懂面向对象也不代表你就无法写出一个完整的脚本了,但是,如果你我都能从这个简短而又啰嗦的小教程中学到什么的话,我将倍感欣慰。
出于让代码易于阅读的目的,在这次的示例中,各种命名我使用的是英文(而非拼音或汉字),不过,请不用担心,相应单词的中文释义我都将通过注释的方式和代码一并给出

那么,没有意义的废话就此打住,下面,让我们一起进入史莱姆的巢穴——面向对象的世界吧

【用代码说话】#2.史莱姆巢穴

class Slime
  def appear; @hp = 10; end
  def die; @hp = 0; end
  def bark
    if @hp == 0
      p 'Uuuu...'
    else
      p 'GaO!!'
    end
  end
end
enemy1 = Slime.new
enemy2 = Slime.new
enemy1.appear
enemy2.appear
enemy1.bark
enemy2.bark
enemy1.die
enemy1.bark
enemy2.bark
enemy3 = Slime.new
enemy3.appear
enemy3.bark
# Slime - 史莱姆
# appear - 出现
# die - 死亡
# bark - 吠叫
# enemy - 敌人

请不要借助任何代码执行工具,阅读上面的代码并思考,输出的结果是什么?

这次的教程我决定从一个传统的日式 RPG 故事作为开始:
一个新人冒险者接到了他冒险生涯中的第一个任务——讨伐三只史莱姆,从公会的接待那里领到了史莱姆的图鉴后,他兴致勃勃的踏上了旅途,没走多远,两个敌人嘎哦嘎哦的叫着从草丛中出现了……
“嘿,这就是史莱姆,他们的行为举止果然和图鉴上写的一模一样!”
一边注意着周围的环境,冒险者打倒了了其中一只史莱姆
“真是绝情啊,明明自己的同伴已经死亡了,另外这只史莱姆却仿佛什么都没发生一样的”
冒险者一边看着发出呜呜的声音瘫软在路边的史莱姆,一边开始将集中精力应对起剩下的那只史莱姆
这时候,从冒险者的身后,第三只史莱姆精力充沛的叫着嘎哦出现了!

从1967年第一种面向对象的语言 Simula67 发明到你阅读这篇教程的这个时间节点,编程语言已经发生了太多的改变,不论是内存控制亦或是垃圾收集,多亏了编程语言和硬件性能的发展,我们得以从各种乏味单调的任务中解脱从而将精力放到运行逻辑本身。鉴于继续顺着这个话题说下去估计这篇教程的主题就回不来了,我们就此打住,用代码说话
回到我们最开始的故事,冒险者公会将史莱姆写成图鉴,史莱姆的行动和图鉴一模一样,史莱姆1死了以后,史莱姆2仿佛什么都没变,史莱姆3在史莱姆1死了以后仍然精力充沛的出现,这就是面向对象的特征
下面,我们可以从头开始完整的分析一遍代码了,我们一起从上往下看一下

class Slime
  def appear; @hp = 10; end
  def die; @hp = 0; end
  def bark
    if @hp == 0
      p 'Uuuu...'
    else
      p 'GaO!!'
    end
  end
end
  • 冒险者公会写了一本怪物图鉴,定义了一个叫 Slime 的类
  • Slime 有一个名叫出现的行为,当他执行这个行为的时候,他的 @hp 会变为 10
  • Slime 有一个名叫死亡的行为,当他执行这个行为的时候,他的 @hp 会变为 0
  • Slime 有一个名叫吠叫的行为,当他的 @hp 为 0 时,他的叫声是呜呜呜,当他的 @hp 为 10 时,他的叫声是嘎哦
enemy1 = Slime.new
enemy2 = Slime.new
enemy1.appear
enemy2.appear
enemy1.bark
enemy2.bark
enemy1.die
enemy1.bark
enemy2.bark
enemy3 = Slime.new
enemy3.appear
enemy3.bark
  • 敌人1是一只史莱姆
  • 敌人2也是一只史莱姆
  • 敌人1出现了!
  • 敌人2出现了!
  • 敌人1开始吠叫
  • 敌人2开始吠叫
  • 敌人1死了
  • 敌人1开始吠叫
  • 敌人2开始吠叫
  • 敌人3是一只史莱姆
  • 敌人3出现了!
  • 敌人3开始吠叫

这段代码运行后的结果是

"GaO!!"
"GaO!!"
"Uuuu..."
"GaO!!"
"GaO!!"

在上一篇中,我们曾经说到,对于面向编程来说,每一个实例都是不同的,就好比这里的敌人1、敌人2、敌人3是三只不同的史莱姆一样,那么,这三只史莱姆明明行为都是一样的,为什么却需要让他们不同呢,这里就引出了各个实例“状态”的作用,在这次的例子里,就是三只史莱姆的HP,也就是代码中出现的 @hp 的功能。正因为像这种以 @ 开头的变量是用来表明实例状态的,所以我们称呼他为“实例变量”
实例的变量的存在让一个个实例的状态不再相同,在 class Slime 中,我们定义了史莱姆的行为,在实例 enemy1、enemy2、enemy3 中,我们储存了史莱姆的状态,从冒险者的角度来看,史莱姆在做的事情仅仅只是吠叫而已,冒险者无法关注到史莱姆吠叫时的 if else 逻辑,冒险者也没有必要去关注史莱姆吠叫时的 if else 逻辑。将不确定因素进行封装来让代码易于理解,这正是面向对象的编程所期望的
当然,虽然吹了半天面向对象,但是没有面向对象并不代表你就写不出代码了,例如,上面那段代码完全可以用下面这段面向过程的代码来等效替代

def appear; return 10; end
def die; return 0; end
def bark(hp)
  if hp == 0
    p 'Uuuu...'
  else
    p 'GaO!!'
  end
end
enemy1 = 'Slime'
enemy2 = 'Slime'
enemy1_hp = appear
enemy2_hp = appear
bark(enemy1_hp)
bark(enemy2_hp)
enemy1_hp = die
bark(enemy1_hp)
bark(enemy2_hp)
enemy3 = 'Slime'
enemy3_hp = appear
bark(enemy3_hp)

如果场上同时出现三只史莱姆时,两种编程方式只从行数上来看似乎相差也不大……

enemy1 = Slime.new
enemy1.appear
enemy2 = Slime.new
enemy2.appear
enemy3 = Slime.new
enemy3.appear
enemy1 = 'Slime'
enemy1_hp = appear
enemy2 = 'Slime'
enemy2_hp = appear
enemy3 = 'Slime'
enemy3_hp = appear

但是,当每只史莱姆除了拥有HP这个状态,还拥有 MP、TP、EXP、LV、ATK、DEF 这些状态的时候……

class Slime
  def appear
    @hp = 10
    @mp = 10
    @tp = rand(5)
    @exp = rand(5)
    @lv = rand(5)
    @atk = rand(5)
    @def = rand(5)
  end
  def die; @hp = 0; end
  def bark
    if @hp == 0
      p 'Uuuu...'
    else
      p 'GaO!!'
    end
  end
end
enemy1 = Slime.new
enemy1.appear
enemy2 = Slime.new
enemy2.appear
enemy3 = Slime.new
enemy3.appear
def appear_hp; return 10; end
def appear_mp; return 10; end
def appear_tp; return rand(5); end
def appear_exp; return rand(5); end
def appear_lv; return rand(5); end
def appear_atk; return rand(5); end
def appear_def; return rand(5); end
def die; return 0; end
def bark(hp)
  if hp == 0
    p 'Uuuu...'
  else
    p 'GaO!!'
  end
end
enemy1 = 'Slime'
enemy1_hp = appear_hp
enemy1_mp = appear_mp
enemy1_tp = appear_tp
enempy1_exp = appear_exp
………………$%$^%$@#$@

当然,你可能会利用 hash 把你的代码写成这样

def appear
  return {
    :hp => 10,
    :mp => 10,
    :tp => rand(5),
    :exp => rand(5),
    :lv => rand(5),
    :atk => rand(5),
    :def => rand(5),
  }
end
def die; return 0; end
def bark(hp)
  if hp == 0
    p 'Uuuu...'
  else
    p 'GaO!!'
  end
end
enemy1 = 'Slime'
enemy1_state = appear
enemy2 = 'Slime'
enemy2_state = appear
enemy2 = 'Slime'
enemy2_state = appear

那么,恭喜你,你已经在很大程度上理解了面向对象编程所做的事情是什么了哦,在你的代码里 enemy1_state[:hp] 对应的就是面向编程中 enemy1 实例的实例变量 @hp 所承担的作用
如果你对 Proc 略有了解,更进一步,把你的代码改写成这样

def appear
  die = Proc.new{ 0 }
  bark = Proc.new{|hp|
    if hp == 0
      p 'Uuuu...'
    else
      p 'GaO!!'
    end
  }
  return {
    :hp => 10,
    :mp => 10,
    :tp => rand(5),
    :exp => rand(5),
    :lv => rand(5),
    :atk => rand(5),
    :def => rand(5),
    :die => die,
    :bark => bark
  }
end
enemy1 = 'Slime'
enemy1_state = appear
enemy2 = 'Slime'
enemy2_state = appear
enemy2 = 'Slime'
enemy2_state = appear

那么,你的代码已经越来越接近面向对象的思维了呢。实际上,对于类似 lua 或者 javascript 这样的语言,这就是他们面向对象的实现方式。当然,这里要特别说明的是,虽然现阶段面向对象在你眼中可能只是一个语法糖一样的东西,实际上,他却和面向过程存在编程思想上的区别。

不知不觉已经写了这么多字了,是时候结束这篇的内容了,和前一篇一样,在这一篇的最后,会有一个小小的恶作剧代码,说是恶作剧代码,实际上是一个小小的内容预告,在下一篇中,我们将一起学习面向对象编程的另一个重要概念——继承

class Slime
  def appear; @hp = 10; end
  def die; @hp = 0; end
  def bark
    if @hp == 0
      p 'Uuuu...'
    elsif @hp > 50
      p 'Golden——!'
    else
      p 'GaO!!'
    end
  end
end
class Golden_Slime < Slime
  def appear; @hp = 100; end
end
enemy = Golden_Slime.new
enemy.appear
enemy.bark

发表评论

电子邮件地址不会被公开。 必填项已用*标注