Daily Archives: 01/05/2016

Ruby – Hướng đối tượng – Phần 2

Trong phần này chúng ta tiếp tục tìm hiểu về hướng đối tượng trong Ruby.

Truy xuất thuộc tính

Như đã nói ở bài trước, mặc định tất cả các thuộc tính trong Ruby đều là private, tức là chúng ta chỉ có thể truy xuất được thông qua phương thức của đối tượng. Trong thực tế thì khi thiết kế lớp, với mỗi thuộc tính chúng ta sẽ định nghĩa 2 phương thức là getter và setter, mục đích của 2 phương thức này là để truy xuất dữ liệu và chỉnh sửa chúng, việc định nghĩa 2 phương thức trên hầu như là công việc tuy không bắt buộc nhưng nhất định phải làm 🙂 Do đó trong một số IDE có sẵn chức năng tự tạo các phương thức getter và setter luôn. Đối với Ruby thì có sẵn 3 phương thức tương tự là attr_reader, attr_writerattr_accessor.

Phương thức attr_reader sẽ tạo các phương thức getter trong khi phương thức attr_writer sẽ tạo các phương thức setter. Phương thức attr_accessor sẽ tạo cả getter, setter và các biến instance.

Ví dụ 1:

class Car
 
    attr_reader :name, :price
    attr_writer :name, :price 
 
    def to_s
        "#{@name}: #{@price}"
    end

end


c1 = Car.new
c2 = Car.new

c1.name = "Porsche"
c1.price = 23500

c2.name = "Volkswagen"
c2.price = 9500

puts "#{c1.name}: #{c1.price}"

p c1
p c2

Trong ví dụ này chúng ta định nghĩa lớp Car. Trong lớp này chúng ta định nghĩa các phương thức getter và setter bằng cách sử dụng phương thức attr_readerattr_writer.

attr_reader :name, :price

Để tạo các phương thức getter thì chúng ta ghi tên phương thức đó dưới dạng Symbol phía sau phương thức attr_reader.

attr_writer :name, :price  

Tương tự với các phương thức setter, chúng ta cũng ghi sau phương thức attr_writer.

c1.name = "Porsche"
c1.price = 23500

Chúng ta gọi các phương thức setter như gọi các phương thức bình thường, rồi dùng toán tử gán "=" để thiết lập dữ liệu.

puts "#{c1.name}: #{c1.price}"

Tương tự, các phương thức getter cũng vậy.

Porsche: 23500
#<Car:0x2517d98 @name="Porsche", @price=23500>
#<Car:0x2517d80 @name="Volkswagen", @price=9500>

Ví dụ 2:

class Book
   attr_accessor :title, :pages    
end

b1 = Book.new
b1.title = "Hidden motives"
b1.pages = 255

p "The book #{b1.title} has #{b1.pages} pages"

Như đã nói ở trên, phương thức attr_accessor sẽ tạo luôn cả phương thức getter và setter.

"The book Hidden motives has 255 pages"

Hằng số lớp

class MMath

    PI = 3.141592
end


puts MMath::PI

Các hằng số được định nghĩa bên trong một lớp có tác dụng giống như biến class vậy, tức là chúng được dùng chung bởi tất cả các đối tượng thuộc lớp đó, chứ không có lớp nào có biến riêng cả. Trong ví dụ trên chúng ta định nghĩa lớp NMath có hằng PI.

PI = 3.141592

Hằng số được đặt tên bắt đầu bằng chữ cái in hoa.

puts MMath::PI

Để truy xuất giá trị của hằng số thì chúng ta dùng toán tử :: theo sau tên lớp.

3.141592

Phương thức to_s

Tất cả các đối tượng trong Ruby đều có phương thức to_s, phương thức này được kế thừa từ lớp Object gốc trong Ruby. Phương thức trả về một chuỗi string mô tả về đối tượng đó, khi chúng ta dùng phương thức puts để in ra một đối tượng thì phương thức puts sẽ gọi đến phương thức to_s của đối tượng đó.

class Being

    def to_s
        "This is Being class"
    end
end

b = Being.new
puts b.to_s
puts b

Trong ví dụ này chúng ta định nghĩa lớp Being có phương thức to_s.

def to_s
    "This is Being class"
end

Nếu chúng ta không định nghĩa lại phương thức to_s thì phương thức puts sẽ gọi phương thức to_s của lớp Object gốc, mà phương thức to_s gốc sẽ in ra địa chỉ bộ nhớ của đối tượng.

b = Being.new
puts b.to_s
puts b

Khi dùng chúng ta có thể gọi ra một cách rõ ràng hoặc không gọi cũng được.

This is Being class
This is Being class

Quá tải toán tử

Quá tải toán tử tức là một toán tử có thể dùng cho nhiều kiểu dữ liệu khác nhau, giống như chúng ta có nhiều phương thức và mỗi phương thức nhận nhiều tham số khác nhau vậy.

class Circle
   
    attr_accessor :radius
    
    def initialize r
        @radius = r
    end

    def +(other)
        Circle.new @radius + other.radius
    end
    
    def to_s
        "Circle with radius: #{@radius}"
    end
end


c1 = Circle.new 5
c2 = Circle.new 6
c3 = c1 + c2

p c3

Trong ví dụ này chúng ta định nghĩa lớp Circle có toán tử +.

def +(other)
    Circle.new @radius + other.radius
end

Để định nghĩa một toán tử thì chúng ta cũng dùng cặp từ khóa def...end giống như định nghĩa một phương thức với tên toán tử phía sau từ khóa def, sau đó đặt tham số trong cặp dấu ().

Ở đây lớp Circle có nghĩa là hình tròn, thuộc tính radius là bán kính. Toán tử + có chức năng cộng 2 bán kính của 2 hình tròn.

c1 = Circle.new 5
c2 = Circle.new 6
c3 = c1 + c2

Toán tử được dùng như cách dùng với các kiểu dữ liệu bình thường

Circle with radius: 11

Phương thức class

Các phương thức trong Ruby có 2 loại là phương thức class và phương thức instance, giống như biến cũng có biến class và biến instance vậy. Và chức năng của 2 loại phương thức này cũng giống hệt như đối với biến. Tức là phương thức instance là phương thức của riêng từng đối tượng, trong khi phương thức class là phương thức dùng chung, tất cả các đối tượng được tạo từ cùng một lớp sẽ dùng chung một phương thức class.

Phương thức class không thể truy xuất các biến instance mà chỉ có thể truy xuất các biến class.

Ví dụ:

class Circle
    
    def initialize x
        @r = x
    end
   
    def self.info
       "This is a Circle class" 
    end
    
    def area
        @r * @r * 3.141592
    end

end


p Circle.info
c = Circle.new 3
p c.area

Trong ví dụ này chúng ta định nghĩa lớp Circle có một phương thức class.

def self.info
    "This is a Circle class" 
end

Phương thức class được định nghĩa bằng cách thêm từ khóa self vào trước tên phương thức.

def area
    "Circle, radius: #{@r}"
end

Phương thức area là phương thức instance vì không chứa từ khóa self, vậy tức là các phương thức mà chúng ta đã định nghĩa từ trước tới giờ đều là phương thức instance.

p Circle.info

Để gọi phương thức class thì chúng ta gọi từ tên lớp chứ không gọi từ tên đối tượng.

c = Circle.new 3
p c.area

Để gọi phương thức instance thì chúng ta phải tạo một đối tượng rồi gọi từ tên đối tượng đó như trước giờ chúng ta vẫn làm. Ở đoạn code trên chúng ta tạo một đối tượng Circle có tên là và gọi phương thức instance area của đối tượng đó.

"This is a Circle class"
28.274328

Ví dụ 2:

Ngoài cách định nghĩa như trên chúng ta còn có 2 cách định nghĩa phương thức class khác.

class Wood
     
    def self.info
       "This is a Wood class" 
    end
end

class Brick
     
    class << self
        def info
           "This is a Brick class" 
        end
    end
end

class Rock
     
end

def Rock.info
   "This is a Rock class" 
end

p Wood.info
p Brick.info
p Rock.info

Trong ví dụ này chúng ta dùng 3 cách định nghĩa phương thức class khác nhau.

def self.info
    "This is a Wood class" 
end

Cách đầu tiên đã giới thiệu ở trên là dùng từ khóa self.

class << self
    def info
        "This is a Brick class" 
    end
end

Cách thứ 2 là khai báo phần định nghĩa trong khối class << self...end.

def Rock.info
   "This is a Rock class" 
end

Cách thứ 3 là hay vì dùng từ khóa self thì chúng ta dùng luôn tên lớp.

"This is a Wood class"
"This is a Brick class"
"This is a Rock class"

Đa hình

Tính đa hình là tính năng cho phép chúng ta thực thi toán tử hay phương thức với nhiều kiểu dữ liệu khác nhau. Khi một lớp thừa kế từ một lớp khác thì lớp con ngoài việc kế thừa lại những gì có ở lớp cha thì có thể định nghĩa lại hoặc mở rộng thêm nữa. Nói một cách tổng quát thì đa hình là tính năng cho phép định nghĩa lại các phương thức ở lớp con.

Ví dụ:

class Animal
    
    def make_noise 
        "Some noise"
    end

    def sleep 
        puts "#{self.class.name} is sleeping." 
    end
  
end

class Dog < Animal
    
    def make_noise 
        'Woof!'         
    end 
    
end

class Cat < Animal 
    
    def make_noise 
        'Meow!' 
    end 
end

[Animal.new, Dog.new, Cat.new].each do |animal|
  puts animal.make_noise
  animal.sleep
end

Trong ví dụ trên chúng ta có lớp cơ sở Animal, lớp dẫn xuất DogCat kế thừa từ lớp Animal. Cả 3 lớp này đều có phương thức make_noise nhưng kết quả của phương thức này ở 3 lớp là khác nhau.

[Animal.new, Dog.new, Cat.new].each do |animal|
  puts animal.make_noise
  animal.sleep
end

Việc định nghĩa lại phương thức được kế thừa ở lớp cha còn được gọi là override. Ở đây phương thức make_noise được override lại ở 2 lớp DogCat, trong khi ở lớp cơ sở có phương thức sleep nữa nhưng không được override lại.

Khi chúng ta gọi phương thức make_noise ở lớp con thì Ruby sẽ tìm trong lớp con đó có phương thức đó hay không, nếu có thì dùng phương thức ở lớp con, nếu không thì gọi lên phương thức đó ở lớp cha.

Some noise
Animal is sleeping.
Woof!
Dog is sleeping.
Meow!
Cat is sleeping.

Ruby – Hướng đối tượng – Phần 1

Chúng ta đã tìm hiểu sơ qua về đối tượng trong các bài trước, trong bài này chúng ta sẽ tìm hiểu sâu hơn.

Ngôn ngữ lập trình được phân ra làm nhiều loại mô hình như mô hình lập trình hướng thủ tục, lập trình hướng hàm, lập trình hướng đối tượng… Ruby là ngôn ngữ lập trình hướng đối tượng.

Lập trình hướng đối tượng (Object-oriented programming – OOP) là mô hình lập trình sử dụng các đối tượng để thiết kế nên kiến trúc của chương trình ứng dụng.

OOP có các khái niệm cơ bản sau đây:

  • Trừu tượng (Abstraction)
  • Đa hình (Polymorphism)
  • Đóng gói (Encapsulation)
  • Thừa kế (Inheritance)

Khái niệm đối tượng

Đây là các thành phần cấu tạo nên một chương trình hướng đối tượng. Một đối tượng trong OOP chứa 2 thành phần là thuộc tính và phương thức, trong đó thuộc tính đơn giản chỉ là các biến chứa dữ liệu, phương thức chỉ là các hàm/thủ tục. Các đối tượng sẽ giao tiếp với nhau thông qua phương thức của chúng. Mỗi đối tượng có thể nhận/gửi thông điệp cho nhau và xử lý dữ liệu của chúng.

Để tạo một đối tượng thì chúng ta phải có lớp, lớp chính là một cái khuôn/bản vẽ/bản thiết kế… để tạo nên các đối tượng.

Ví dụ khi nói lớp Người thì chúng ta biết rằng một người bao gồm tên, tuổi, màu da, giới tính… có thể ăn, nói, nhảy múa… Còn khi nói đối tượng người thì chúng ta có đối tượng Jack, 23 tuổi, quốc tịch Mỹ, biết hát, đối tượng Jennifer 19 tuổi, quốc tịch Nauy, biết nấu ăn…

Ví dụ:

class Being
  
end

b = Being.new
puts b

Trong đoạn code trên chúng ta định nghĩa lớp và tạo một đối tượng từ lớp đó.

class Being
  
end

Để định nghĩa lớp thì chúng ta dùng cặp từ khóa class...end với tên lớp mà chúng ta muốn đặt sau từ khóa class. Hiện tại lớp này là lớp rỗng, không có gì cả.

b = Being.new

Để tạo một đối tượng thuộc lớp Being thì chúng ta ghi tên lớp rồi dùng phương thức new. Phương thức này sẽ trả về địa chỉ tham chiếu đến đối tượng vừa tạo, ở trên chúng ta lưu lại đối tượng này vào biến b.

puts b

Khi chúng ta gọi phương thức puts lên một đối tượng, phương thức này sẽ gọi phương thức to_s có trong mỗi đối tượng. Trong trường hợp của chúng ta thì do chưa định nghĩa phương thức to_s nên phương thức puts sẽ trả về địa chỉ tham chiếu đến đối tượng.

#<Being:0x9f3c290>

Phương thức khởi tạo

Phương thức khởi tạo là một phương thức đặc biệt, phương thức này từ động được gọi khi chúng ta tạo một đối tượng. Phương thức khởi tạo không trả về một giá trị nào cả. Mục đích chính của phương thức khởi tạo chỉ là thiết lập trạng thái cho đối tượng. Tất cả các phương thức khởi tạo trong Ruby đều có tên là initialize.

Ví dụ 1:

class Being

    def initialize
        puts "Being is created"
    end

end

Being.new

Trong ví dụ trên chúng ta định nghĩa phương thức initialize. Trong phương thức này chúng ta in một chuỗi string ra màn hình. Khi chúng ta gọi phương thức new, phương thức này sẽ tự động gọi phương thức initialize.

Để định nghĩa một phương thức thì chúng ta dùng cặp từ khóa def...end với tên phương thức nằm phía sau từ khóa def.

Being is created

Ví dụ 2:

class Person

    def initialize name
        @name = name
    end

    def get_name
        @name
    end

end

p1 = Person.new "Jane"
p2 = Person.new "Beky"

puts p1.get_name
puts p2.get_name

Thuộc tính của một đối tượng là các biến lưu trữ giá trị của đối tượng đó. Các biến này còn được gọi là biến instance. Mỗi đối tượng đều có thuộc tính của riêng nó, tức là các đối tượng thuộc cùng một lớp thì có các biến instance khác nhau.

class Person

    def initialize name
        @name = name
    end

Trong đoạn code trên, hàm khởi tạo initialize nhận vào một biến tham số có tên là name, chúng ta gán giá trị của tham số đó vào biến instance @name.

def get_name
    @name
end

Chúng ta định nghĩa phương thức get_name, phương thức này trả về giá trị của biến @name. Trong Ruby các biến instance chỉ có thể truy xuất trong các phương thức.

p1 = Person.new "Jane"
p2 = Person.new "Beky"

Để truyền tham số thì chúng ta ghi phía sau tên phương thức khi gọi. Trong đoạn code trên các chuỗi “Jane”, “Beky” sẽ được truyền vào hàm initialize.

Jane
Beky

Phương thức thành phần

Phương thức thành phần (hay gọi ngắn gọn là phương thức) là các hàm được định nghĩa bên trong một lớp, mục đích của phương thức là thực hiện một công việc nào đó. Thường thì phương thức sẽ làm việc với các thuộc tính của đối tượng. Phương thức rất quan trọng với tính năng đóng gói trong lập trình hướng đối tượng, đóng gói tức là chúng ta không quan tâm phương thức làm những gì mà chỉ quan tâm đến kết quả cuối cùng của phương thức mà thôi.

Ví dụ 1:

class Person

    def initialize name
        @name = name
    end

    def get_name
        @name
    end

end

per = Person.new "Jane"

puts per.get_name
puts per.send :get_name

Để gọi một phương thức thì chúng ta có 2 cách.

puts per.get_name

Cách đầu tiên và cũng là phổ biến nhất là ghi tên đối tượng, dấu chấm rồi đến tên phương thức.

puts per.send :get_name

Cách thứ hai là gọi phương thức send, theo sau là tên phương thức nhưng viết dưới dạng một Symbol, tức là thêm dấu 2 chấm ":" vào trước tên phương thức.

Ví dụ 2:

class Circle
   
    @@PI = 3.141592

    def initialize
        @radius = 0
    end

    def set_radius radius
        @radius = radius
    end

    def area
        @radius * @radius * @@PI
    end

end


c = Circle.new
c.set_radius 5
puts c.area

Trong đoạn code trên chúng ta định nghĩa lớp Circle có 2 phương thức.

@@PI = 3.141592

Trong lớp Circle chúng ta định nghĩa một biến class là @@PI, biến class là biến dùng chung cho tất cả các đối tượng.

def initialize
    @radius = 0
end

Chúng ta định nghĩa một biến instance là @radius.

def set_radius radius
    @radius = radius
end

Phương thức set_radius sẽ nhận một tham số đầu vào và gán giá trị đó cho biến @radius.

def area
    @radius * @radius * @@PI
end

Phương thức area sẽ tính diện tích hình tròn với biến @radius@@PI.

c = Circle.new
c.set_radius 5
puts c.area

Nếu như trong các ngôn ngữ khác, chúng ta có từ khóa return để trả về một giá trị của một phương thức thì trong Ruby giá trị này sẽ tự động được trả về ngầm bằng giá trị của câu lệnh cuối cùng được thực hiện trong phương thức. Trong đoạn code trên, phương thức puts c.area sẽ in ra giá trị được trả về là diện tích được tính bên trong phương thức area.

78.5398

Quyền truy cập

Quyền truy cập tức là phạm vi truy xuất các thuộc tính và phương thức của mỗi đối tượng. Ruby có 3 loại quyền truy cập là public, protectedprivate. Trong Ruby, tất cả các thuộc tính đều có quyền truy cập là private và không thể thay đổi được, còn các phương thức thì mặc định có quyền truy cập là public nhưng có thể thay đổi được.

Quyền truy cập loại public cho phép chúng ta truy cập thành phần của đối tượng ở bên trong lẫn bên ngoài lớp. Quyền protectedprivate giống nhau ở chỗ đều không cho phép truy cập thành phần của đối tượng ở bên ngoài lớp, khác nhau ở chỗ private không gọi được với từ khóa self, còn protected thì được.

Quyền truy cập đảm bảo an toàn cho dữ liệu không bị thay đổi cho dù là cố ý hay vô ý.

Ví dụ 1:

class Some
        
     def method1
         puts "public method1 called"
     end

    public
    
     def method2
         puts "public method2 called"  
     end
     
     def method3
         puts "public method3 called"
         method1
         self.method1
     end          
end

s = Some.new
s.method1
s.method2
s.method3

Trong ví dụ này chúng ta sử dụng quyền truy cập public.

def method1
    puts "public method1 called"
end

Phương thức method1 có quyền truy cập mặc định là public.

public

  def method2
      puts "public method2 called"  
  end

  ...

Ngoài ra chúng ta có thể chỉ rõ cho Ruby biết là phương thức nào có quyền public bằng cách ghi từ khóa public lên trước phần định nghĩa phương thức đó, trong trường hợp này cả method2method3 đều có quyền truy cập là public.

s = Some.new
s.method1
s.method2
s.method3

Chỉ có các phương thức public mới có thể truy cập ở bên ngoài phần định nghĩa lớp.

public method1 called
public method2 called
public method3 called
public method1 called
public method1 called

Ví dụ 2:

class Some
    
    def initialize
        method1
        # self.method1
    end

    private
    
     def method1
         puts "private method1 called"  
     end
           
end


s = Some.new
# s.method1

Để chỉ định phương thức nào là private thì chúng ta đặt từ khóa private lên trước định nghĩa phương thức đó. Các phương thức private chỉ có thể gọi được trong phần định nghĩa lớp nhưng không được sử dụng từ khóa self.

private method called

Ví dụ 3:

class Some
    
    def initialize
        method1
        self.method1
    end

    protected
    
     def method1
         puts "protected method1 called"  
     end
           
end


s = Some.new
# s.method1

Tương tự như private, để chỉ định phương thức protected thì chúng ta đặt từ khóa protected lên trước định nghĩa phương thức. Phương thức protected khác private ở chỗ là chúng có thể truy cập với từ khóa self, giống private ở chỗ là không thể truy cập được ở bên ngoài phần định nghĩa lớp.

Thừa kế

Thừa kế là tính năng cho phép định nghĩa các lớp dựa trên các lớp đã có. Lớp thừa kế từ một lớp khác được gọi là lớp dẫn xuất, lớp được lớp khác thừa kế lại được gọi là lớp cơ sở. Lớp dẫn xuất thừa hưởng các thành phần của lớp cơ sở và có thể có thêm các thành phần của riêng chúng. Tính năng thừa kế cho phép lập trình viên giảm thiểu sự phức tạp của chương trình.

Ví dụ 1:

class Being

    def initialize
        puts "Being class created"
    end
end

class Human < Being

   def initialize
       super
       puts "Human class created"
   end
end

Being.new
Human.new

Trong ví dụ trên chúng ta có 2 lớp là lớp Being và lớp Human, trong đó lớp Being là lớp cơ sở, lớp Human là lớp dẫn xuất. Tức là lớp Human kế thừa từ lớp Being.

class Human < Being

Để một lớp kế thừa từ một lớp khác thì chúng ta ghi dấu "<" vào sau tên lớp và ghi tên lớp dẫn xuất phía sau.

def initialize
    super
    puts "Human class created"
end

Phương thức super có tác dụng gọi đến hàm khởi tạo của lớp cha.

Being class created
Being class created
Human class created

Ví dụ 2:

Một lớp có thể có nhiều lớp cơ sở. Mỗi lớp trong Ruby mặc định có phương thức ancestors, phương thức này trả về danh sách các lớp cơ sở của lớp đó. Và mặc định tất cả các lớp trong Ruby đều kế thừa từ một lớp gốc có tên là Object và BasicObject trong module Kernel.

class Being 
end

class Living < Being 
end

class Mammal < Living 
end

class Human < Mammal 
end
    
    
p Human.ancestors

Trong ví dụ này chúng ta có bốn lớp là Human kế thừa từ Mammal, lớp Mammal lại kế thừa từ lớp Living và lớp Living kế thừa từ lớp Being.

p Human.ancestors

Phương thức ancestors sẽ in danh sách các lớp cơ sở.

[Human, Mammal, Living, Being, Object, Kernel, BasicObject]

Lưu ý là tính năng thừa kế trong Ruby hơi khác so với các ngôn ngữ như C++, C#, Java… ở chỗ là trong các ngôn ngữ khác thì các thành phần publicprotected đều được truyền lại từ lớp cha đến lớp con còn thành phần private thì không, nhưng trong Ruby thì cả 3 loại public, protectedprivate đều được truyền lại cho lớp con, tức là tính năng thừa kế trong Ruby không có liên quan gì đến quyền truy cập cả.

Phương thức super

Phương thức super có tác dụng gọi đến phương thức cùng tên ở lớp cha.

class Base
   
    def show x=0, y=0
        p "Base class, x: #{x}, y: #{y}"
    end
end

class Derived < Base

    def show x, y
        super
        super x
        super x, y
        super()
    end
end


d = Derived.new
d.show 3, 3

Trong ví dụ trên chúng ta có lớp Base và lớp Derived, trong đó lớp Derived kế thừa từ lớp Base. Cả 2 lớp này đều có phương thức show.

def show x, y
    super
    super x
    super x, y
    super()
end

Việc gọi super trong phương thức show ở lớp con sẽ gọi đến phương thức show ở lớp cha. Nếu phương thức ở lớp cha có nhận tham số mà chúng ta không truyền vào phương thức super thì phương thức này sẽ tự động nhận tham số của phương thức con, tức là các tham số truyền vào phương thức con sẽ tự động truyền vào trong lời gọi phương thức super luôn. Hoặc chúng ta có thể gọi super() để không truyền vào một tham số nào cả.

"Base class, x: 3, y: 3"
"Base class, x: 3, y: 0"
"Base class, x: 3, y: 3"
"Base class, x: 0, y: 0"