4-6 Class và sự kế thừa

4-6 Class và sự kế thừa

Phần trước đã giải thích cách tạo class thông qua việc tạo Slime Class. Phần này chúng ta sẽ thử cho nhân vật của 3 class là Slime Class, Ghost Class và Dragon Class đánh nhau

Những con quái vật xuất hiện

Chương này chúng ta sẽ tạo monster class. Những con quái thú xuất hiện là 3 loại sau.


  • Slime ........................ Là một con quái vật rất dễ thương, nhưng có chiêu đánh để tiêu diệt đối phương ngay lập tức
  • Ghost ....................... Là một linh hồn trong rất đáng sợ. Bằng những chiêu tấn công của mình có thể lấy máu của đối phương
  • Dragon .................... Quái vật mạnh nhất, bằng những chiêu tấn công của mình có thể thiêu chết đối thủ

Những con quái vật cũng mang trong mình những thông số.

  • hp: HP, đây là máu của quái vật nên nếu hp hạ xuống 0 thì con quái vật đó sẽ chết.
  • str: Strenght(Sức mạnh), giá trị này sẽ trở thành giá trị sức mạnh của con quái vật.
  • agi: Agility. Giá trị này càng cao thì cơ hội tấn công càng cao.

Từ đây chúng ta sẽ viết nên chương trình để tạo class cho các con quái vật và cho bọn chúng đánh nhau.

Slime Class

Đầu tiên chúng ta phải tạo Slime Class

monster04.rb

class Slime
  attr_reader :name, :str, :agi
  attr_accessor :hp

  def initialize(name)
    @name = name
    setup_params
  end

  def setup_params
    @hp = 10
    @str = 3
    @agi = 15
  end

  def attack(target)
    puts "#{name} đã tấn công"
    d = rand(@str) + 1
    target.hp -= d
    puts "Năng lượng đòn tấn công #{d} => #{target.name}(#{target.hp})"
  end

  def special(target)
    puts "#{@name} đã trúng 1 đòn vào tim
    d = rand(@str * 3) + 1
    target.hp -= d
    puts "#{d} năng lượng tấn công => => #{target.name}(#{target.hp})"
  end
end

Như vậy trong Slime Class đã có vài kĩ năng được lệnh hóa. Vậy chúng ta lần lượt đi xem từng bước một nhé. Đầu tiên chúng ta tạo một object từ Slime class bằng phương pháp sau

monster1 = Slime.new("A")

Tại Slime class chúng ta có thể đưa tên cho lệnh [new]. Tại đây, tên này sẽ được đặt cho con Slime mới được sinh ra từ Slime class. Chức năng nhận tên là lệnh [initialize] được định nghĩa trong Slime Class.[Initialize] nhận argument là [name] rồi thay tên đó vào [@name] là biến instance. Slime object được sinh ra từ Slime class có một vài thuộc tính được định nghĩa ở vài lệnh

monster = Slime.new("A")
p monster.name		# => "A"
p monster.hp		  # => 10
p monster.str		 # => 3
p monster.agi		 # => 15

Biến số ghi nhớ những thuộc tính (thông số) này của con Slime được đưa ra bởi lệnh [setup_params]. Lệnh [setup_params] được gọi ra từ lệnh initialize nên cùng với việc object được sinh ra thì những thông số thuộc tính đã được nhập lên object đó. Mặt khác, Slime class có mang theo mình những lệnh dùng để tấn công đối phương như [attack] - tấn công thông thường và [special] - tấn công đặc biệt.Nếu chúng ta nhập argument alf đối phương vào những lệnh này và gọi nó ra thì nó sẽ làm giảm máu của đối phương rồi đưa ra những tin nhắn thông báo.

monster1 = Slime.new("SlimeA")
monster2 = Slime.new("SlimeB")
monster1.attack monster2
monster1.special monster2

[Kết quả hiển thị]

SlimeA đã tấn công!
Năng lượng đòn tấn công là 3 => SlimeB(7)
SlimeB đã trúng một đòn vào tim!
7 năng lượng đòn tấn công => SlimeB(0)

Lệnh [special] là lệnh phát động tấn công đặc biệt vào đối phương.[Special] ra năng lượng tấn công lớn hơn nhiều so với lệnh tấn công thông thường là [attack]. Về tính toán năng lượng đòn tấn công và xử lý máu còn lại cho đối phương, các bạn hãy nhìn trong lệnh và xác định lại một lần nữa. Vì đòn tấn công sử dụng lệnh [rand] để tính toán số tấn công ảnh hưởng nên mỗi lần sẽ ra những kết quả khác nhau. Chúng ta có thẻ hiểu khi xem lệnh [attack] hay [special] nhưng nhìn lúc này thì chúng ta chỉ nhìn thấy HP của đối phương giảm dần đi. Để xử dụng phán đoán ai tấn công trước thì chúng tôi sẽ giới thiệu cách xử dụng lệnh [agi] và [battle].

Chương trình đấu trường

Tại đây chúng ta sẽ dùng 2 con slime để cho chúng đánh nhau. Chương trình để 2 object Slime đánh nhau đó chính là chương trình dưới đây.

monster05.rb

class Slime
  (lược)
end
#những lệnh xử lý để tạo đáu trường
def battle(monster1, monster2)
  while true
    # quyết định công thủ
    diff = rand(monster1.agi) - rand(monster2.agi)
    if diff == 0
      next# nếu không có sự khác nhau thì sẽ quay lại từ đầu loop
    elsif diff > 0
      a = monster1
      b = monster2
    else
      b = monster1
      a = monster2
    end
    # tấn công
    if rand(4) == 0
      a.spacial b
    else
      a.attack b
    end
    # check HP
    if b.hp <= 0
      puts "#{b.name} đã bị đánh gục"
    break		# => ra khổi loop
    end
  end
end

monster1 = Slime.new("SlimeA")
monster2 = Slime.new("SlimeB")
battle(monster1, monster2)

Để bắt đầu xử lý đấu trường thì chúng ta xử dụng lệnh [battle]. Cuối chương trình chúng ta đã tạo ra 2 con slime và chạy chương trình cho chúng đánh nhau. Trong cuộc đấu, HP của con nào về 0 trước thì cuộc chiến sẽ đến hồi kết và loop sẽ kết thúc.

Xử lý trong loop trên được diễn ra với trình tự như sau


  • Quyết định công thủ giữa hai con monster. Con tấn công được đặt tên là [a] còn con bị tấn công sẽ là [b].
  • Thực hiện lựa chọn đây là đòn tấn công thông thường hay đòn tấn công đặc biệt. Xác suất xuất hiện đòn tấn công đặc biệt là 1/4
  • Phán xét xem ai chết trước. Nếu HP của [b] trở về 0 thì cuộc chiến kết thúc tại đó
  • Nếu [b] không chết thì ta sẽ trở lại các thao tác từ đầu

Trong lệnh [battle], chỗ quyết định công thủ có một xử lý như sau:

diff = rand(monster1.agi) - rand(monster2.agi)

Tại đây chúng ta sẽ quyết định ai tấn công trước bằng cách so sánh thông số [agi] của từng con quái vật. Nó được quyết định ngẫu nhiên bằng lệnh [rand] giữa hai thông số này của [monster1] hay [monster2]. Nếu [agi] của monster1 lớn hơn thì [diff] sẽ lớn hơn 0, còn ngược lại, nếu số ngẫu nhiên của monster2 lớn hơn thì [diff] sẽ nhỏ hơn 0. Nếu diff bằng 0 thì sẽ là trường hợp không thể quyết định công thủ cho 2 nhân vật.

if diff == 0
  next
elsif diff > 0
(lược)

Nếu giá trị [diff] bằng 0 thì lệnh [next] sẽ được thực hiện. Nếu trong [while] mà chúng ta sử dụng [next] thì sẽ quay lại đầu loop, tức là quyết định công thủ sẽ được xem xét lại từ đầu. Còn nếu giá trị [diff] là khác 0 thì chúng ta sẽ quyết định công thủ như xử lý dưới đây

  elsif diff > 0
    a = monster1
    b = monster2
  else
    a = monster2
    b = monster1
  end

[Kết quả hiển thị]

SlimeB đã tấn công!
  Năng lượng đòn tấn công 2 => SlimeA(8)
SlimeA tấn công đòn đặc biệt!
  Năng lượng đòn tấn công 6 => SlimeB(4)
SlimeB tấn công đòn dặc biệt!
  Năng lượng đòn tấn công 5 => SlimeA(2)
SlimeB đã tấn công!
  Năng lượng đòn tấn công 3 => SlimeA(0)
Slime A đã bị đánh gục!

Năng lượng đòn tấn công được quyết định một cách ngẫu nhiên nên mỗi lần chúng ta lại có một kết quả khác nhau. Bên nào có HP xuống dưới 0 thì thắng thua sẽ được quyết định và chương trình sẽ kết thúc tại đó.

Ghost Class

Chúng ta đã hoàn thành chương trình tranh đấu giữa những con Slime. Tiếp theo chúng ta thử tạo Ghost class nhé!

class Ghost
  attr_reader :name :str :agi
  attr_accessor :hp
  def initialize(name)
    @name = name
    setup_params
  end
  def setup_params
    @hp = 12
    @str = 5
    @agi = 9
  end

  def attack(target)
    puts "#{@name} đã tấn công!"
    d = rand(@str) + 1
    tarnget.hp -= d
    puts " Năng lượng đòn tấn công #{d} => #{target.name}(#{target.hp})"
  end

  def special(target)
    puts "#{@name} đã tấn công đòn đặc biệt"
    d = rand(@str) + 1
    target.hp -= d
    @hp += d
    puts " Năng lượng tấn công #{d} => #{target.name}(#{target.hp})"
    puts " #{d} năng lượng được hút vào => #{@name}(#{@hp})"
  end
end

Ghost class với Slime Class có những điểm rất giống nhau.Ghost class chỉ khác là ở đây, thông số thuộc tính và đòn tấn công đặc biệt khác thôi. Đòn tấn công đặc biệt của ghost đó chính là hút máu của đối phương để đó thành máu của mình. Hãy nhìn lệnh [special]. Ở đây chúng ta trừ máu của đối phương rồi cộng số máu đó vào số máu của mình. Cách sử dụng Ghost class giống hoàn toàn so với sử dụng Slime class. Dưới đây là chương trình chúng ta cho Ghost và Slime đánh nhau.

monster06.rb

class Slime
(lược)
end

class Ghost
(lược)
end

def battle(monster1, monster2)
(lược)
end

monster1 = Slime.new("Slime")
monster2 = Ghost.new("Ghost")
battle(monster1, monster2)

[Kết quả hiển thị]

Đòn tấn công đặc biệt của Ghost!
  Năng lượng đòn tấn công 4 => Slime(6)
  4 năng lượng đã được hút vào => Ghost(16)
Ghost đã tấn công
  Năng lượng tấn công 1 => Slime(5)
Slime đã tấn công
  Năng lượng tấn công 1 => Ghost(15)
Ghost đã tấn công
  Năng lượng tấn công 5 => Slime(0)
Slime đã bị đánh gục

Sử dụng lệnh thừa kế

Tất nhiên, đến đây chúng ta nghĩ, thôi tạo luôn ra Dragon class rồi cho 3 bọn chúng chiến đấu lẫn nhau, nhưng chúng ta dừng tại đây 1 chút. Hãy xem lại một lần nữa Slime class và Ghost class. Chương trình của 2 class này khá giống nhau. Nếu xem kĩ thì chúng ta còn thấy lệnh của 2 class này là hoàn toàn giống nhau. Chúng ta hãy tìm những điểm giống nhau trong chương trình nhé!

Đầu tiên, lệnh sử dụng access là giống nhau

attr_reader :name :str :agi
attr_accessor :hp

Sau đó lệnh initialize cũng giống nhau.

def initialize(name)
  @name = name
  setup_params
end

Rồi lệnh [attack] cũng hoàn toàn giống nhau.

  def attack(target)
    puts "#{@name} đã tấn công"
    d = ràn (@str) + 1
    target.hp -= d
    puts "Năng lượng đòn tấn công #{d} => #{target.name}(#{target.hp})"
end

Trong những lúc này thì lệnh về thừa kế là tiện lợi nhất. Sử dụng [thừa kế] thì chúng ta có thể định nghĩa ra 1 class riêng bao gồm bộ phận mà 2 class giống nhau. Bộ phận mà Slime Class và Ghost Class giống nhau thì chúng ta thử định nghĩa nó vào một class riêng gọi là Monster class.

class Monster
  attr_reader :name :str :agi
  attr_accessor :hp

  def initialiaze(name)
    @name = name
    setup_params
  end

  def attack(target)
    puts "#{name} đã tấn công"
    d = rand(@str) + 1
    target.hp -= d
    puts "Năng lượng đòn tấn công #{d} => #{target.name}(#{target.hp})"
  end
end

Trong Monster class có định nghĩa những lệnh mà Slime class và Ghost class giống nhau. Tiếp theo đây chúng ta sẽ để Slime class thừa kế những lệnh này của Monster class.

class Slime < Monster
end

Như vậy chúng ta đã tạo ra được Slime class thừa kế những thông tin trong Monster class. Như vậy nếu muốn làm cho A class thừa kế những thông tin trong B class thì chúng ta chỉ cần làm những bước trong chương trình dưới đây.

class A<B
end

Như vậy class A sẽ thừa kế những chức năng, kĩ năng trong class B. Nếu chúng ta sử dụng lệnh thừa kế thì ta có thể để class mới thừa kế tất cả những chức năng có trong class cũ. Tức là, Slime class và Ghost class có thể thừa kế tất cả những lệnh được định nghĩa trong Monster class.

Chỉ định nghĩa những chức năng cần thiết

Monster class không định nghĩa tất cả những chức năng có trong Slime class. Tại đó, chúng ta cần định nghĩa cũng lệnh, kĩ năng của Slime class mà không có trong monster class.

class Monster
(lược)
end

class Slime<Monster
  def setup_params
    @hp = 10
    @str = 3
    @agi = 15
  end

  def special(target)
    puts "#{@name} đã trúng 1 đòn vào tim
    d = rand(@str * 3) + 1
    target.hp -= d
    puts "#{d} năng lượng tấn công => => #{target.name}(#{target.hp})"
  end
end

Như vậy chúng ta đã hoàn thành Slime class có những tính năng giống hệt Slime class cũ. Tiếp theo chúng ta cũng làm tương tự với Ghost class và định nghĩa thêm những tính năng cần thiết khác.

class Ghost < Monster
  def setup_params
    @hp = 12
    @str = 5
    @agi = 9
  end

  def special(target)
    puts "#{@name} đã tấn công đòn đặc biệt"
    d = rand(@str) + 1
    target.hp -= d
    @hp += d
    puts " Năng lượng tấn công #{d} => #{target.name}(#{target.hp})"
    puts " #{d} năng lượng được hút vào => #{@name}(#{@hp})"
  end
end

Nhưu vậy chúng ta đã tạo ra Slime class với Ghost class với những chức năng giống y hệt với class cũ trước khi sử dụng lệnh thừa kế.

Dragon class

Tiếp theo chúng ta sẽ tạo nên Dragon class. Dragon class được tạo ra cũng sử dụng chức năng thừa kế từ Monster class.

class Dragon < Monster
  def setup_params
    @hp = 20
    @str = 7
    @agi = 5
  end

  def special(target)
    puts "#{@name} đã tấn công đặc biệt"
    d = @hp/2
    target.hp -= d
    puts " Năng lượng tấn công #{d} => #{target.name}(#{target.hp})"
  end
end

Dragon class thừa kế những chức năng của Monster class nên chúng ta chỉ cần định nghĩa thêm những lệnh cần thiết như [setup_params] và [special] thôi. Đòn tấn công đặc biệt của Dragon class đó chính là một đòn tấn công rất mạnh. Chúng ta xem lại cách tấn công vào máu của đối phương khi object trong Dragon class dùng đòn này nhé!

  d = @hp/2

Đòn tấn công này của rồng có sức mạnh bằng một nửa số máu con rồng đang có. Cũng như những con quái vật khác, Dragon class cũng có thể tham gia đấu trường.

Sub Class và Super Class

Trường hợp này thì Monster Class được gọi là Super Class còn Slime Class được gọi là Sub class. Sub class và Super class là để chỉ quan hệ trên dưới trong quá trình kế thừa thong tin và sẽ được dùng như dưới đây


  • Super class của Slime class là Monster class
  • Slime class là Sub class của Monster class

Tái định nghĩa lệnh

Nếu chúng ta thêm lệnh [special] như dưới đây vào Monster class thì kể cả khi trong Sub class không định nghĩa lệnh [special] thì chúng ta cũng có thể gọi lệnh này ra.

monster08.rb

class Monster
(lược)

  def special(target)
    puts "Đòn tấn công đặc biệt của #{@name}"
    puts "....Nhưng không có chuyện gì xảy ra cả"
  end
end

class Punya<Monster
  def setup_params
    @hp = 1
    @str = 1
    @agi = 1
  end
end

monster1 = Punya.new("PunyaA")
monster2 = Punya.new("PunyaB")
monster1.special monster2

[Kết quả hiển thị]

Đòn tấn công đặc biệt của Punya!
....Nhưng không có gì xảy ra cả"

Cứ như vậy kể cả ta không định nghĩa lệnh [special] ở Punya claaa thì chúng ta cũng có thể gọi lệnh đó trong punya class. Nếu chúng ta cũng làm cho Ghost class hay Slime class thừa kế lệnh này trong monster class, nhưng lệnh này đã được định nghĩa riêng trong từng class như vậy, liệu điều này có được cho phép không?

Không cần phải lo lắng! Trong trường hợp những lệnh được định nghĩa trong Monster class mà lại được định nghĩa lại trong Slime class thì lệnh được định nghĩa trong Slime class sẽ được dùng. Lý do tại vì những lệnh được định nghĩa trong [Sub class] như Slime class hay Ghost class sẽ được ưu tiên hơn. Những lệnh được đinh nghĩa trong Super class (Ở đây là Monster class) mà được định nghĩa lại trong Sub class với tên y hệt như vậy thì chúng ta gọi là [Override]

Super

Trong trường hợp tái định nghĩa lệnh có trong Super class tại Sub class, nếu sử dụng [super] thì chúng ta có thể gọi lệnh Super class từ Sub class.

class A
  def Hello
    puts "Hello"
  end
end

class B<A
  def Hello
    super
  end
end

Chúng ta hãy dùng lệnh [super] này để tạo nên một con Slime cường lực tên là [High Slime] thôi.High Slime là con Slime được cường lực hóa trở thành một super monster, so với tấn công thông thường thì có thể tăng 50% cơ hội được tấn công liên tiếp. Chúng ta thử định nghĩa class [HighSlime]. [HighSlime] sẽ kế thừa năng lưc của Slime.

class HighSlime < Slime
  def attack(target)
    super
    if rand(2) == 0
      puts "#{@name} tấn công liên tục"
      super
    end
  end
end

Ở khu vực [super] thì lệnh [attack] của [Super class] đã được gội ra. Lệnh [attack] của HighSlime class đầu tiên thực hiện lệnh [super] rồi gọi ra lệnh [attack] của Slime class. Tiếp theo với xác suất là 50% thì một lần nữa lệnh [super] được thực hiện. Nếu lệnh [super] được thực hiện lần thứ hai thì có nghĩa tấn công đã được thực hiện 2 lần. Mặt khác, nếu chúng ta giản lược argument đưa cho lệnh super thì nó sẽ được giũ nguyên argument như ở trong lệnh ban đầu. Tại đây vì argument được giản lược nên argument được trao cho [super] chính là [target] được nêu lên ở ban đầu.

Hơn nữa chúng ta thử cho thêm những khả năng mới cho đòn tấn công đặc biệt thôi. Đòn tấn công đặc biệt của HighSlime đó chính là đòn tấn công đặc biệt của Slime công thêm 50% xác suất được tấn công tiếp.

class HighSlime < Slime
(lược)
  def special(target)
    super
    if rand(2) == 0
      puts "#{@name} theo đuổi tấn công"
      attack(target)
    end
  end
end

Đầu tiên, lệnh [special] của Slime class được thực hiện một lần. Sau đó với xác suất là 50% thì lệnh theo đuổi tấn công sẽ được phát động. Theo đuổi tấn công sẽ tấn công đối thủ với sức mạnh tấn công bằng với đòn tấn công thông thường.

Như vậy nếu chúng ta sử dụng "kế thừa" và "super" thì chúng ta cũng có thể thêm vào class một cài chức năng khác nữa!