Object Oriented Programming with Ruby (Part 1)

Đây là bài viết được dịch từ một cuốn sách, các bạn có thể xem ở đây: Object Oriented Programming with Ruby

Tại sao lại lập trình hướng đối tượng?

Trong phần này sẽ giới thiệu cho bạn một cái nhìn tổng quan về lập trình hướng đối tượng (Object Oriented Programming).

Object Oriented Programming, thường được gọi với cái tên OOP, là một mô hình lập trình mà nó được tạo ra để phù hợp với sự phát triển càng phức tạp của các hệ thống phần mềm. Các lập trình viên đã tìm ra rất sớm các project mà phát triển về độ phức tạp và kích thước, chúng trở nên rất khó để bảo trì. Một thay đổi nhỏ ở bất kì chỗ nào của chương trình cũng dẫn đến xảy ra loạt lỗi ở toàn bộ chương trình do sự phụ thuộc lẫn nhau.

Các lập trình viên cần một phương pháp để tạo ra các nơi chứa (containers) cho dữ liệu để có thể thay đổi, thao tác mà không ảnh hưởng đến toàn bộ chương trình. Họ cần một cách để chia code của mình thành các phần để tiến hành chạy các hàm, do vậy chương trình của họ có thể thực thi với nhiều phần nhỏ của chương trình, so với một mảng lớn và phụ thuộc nhiều lẫn nhau.

Cùng bắt đầu với OOP, đầu tiên ta cần giới thiệu một số thuật ngữ, và sau đó sẽ đi sâu thông qua các ví dụ. Đóng gói (Encapsulation) là việc ẩn đi các phương thức, chức năng và làm cho nó không thể truy cập được từ bên ngoài. Nó giống như là một cái form chứa giữ liệu, vì vậy dữ liệu sẽ không được thao tác hoặc thay đổi mà không dự tính trước. Ruby, như nhiều ngôn ngữ hướng đối tượng khác, nó để lộ ra các interfaces (vd: methods) để tương tác với các đối tượng đó.

Một lợi ích khác của việc tạo ra các đối tượng là chúng cho phép các programmer suy nghĩ ở một tầng mới của trừu tượng. Các đối tượng được biểu diễn như là những sự vật và các phương thức mô tả các hành vi của đối tượng đó ở thế giới thực.

Đa hình (Polymorphism) là một khả năng mà dữ liệu có thể được biểu diễn lại bởi nhiều cách khác nhau. "Poly" thể hiện cho số nhiều và "morph" thể hiện cho "forms". OOP cung cấp cho chúng ta một cách linh hoạt khi sử dụng code đã được viết trước cho mục đích mới (tái sử dụng code).

Khái niệm về kế thừa (inheritance) được sử dụng ở Ruby nơi mà một class kế thừa các hành vi của class khác, hay còn gọi là superclass. Điều này đưa cho các lập trình viên khả năng để định nghĩa các class cơ bản với khả năng sử dụng lại lớn và các subclass nhỏ hơn để các hành vi được thể hiện chi tiết, rõ ràng hơn.

Cách khác để áp dụng kiến trúc đa hình trong lập trình Ruby là sử dụng Module. Module tương tự như class, trong đó chúng chứa các hành vi được chia sẻ. Tuy nhiên, bạn không thể tạo một đối tượng với một module. Một module phải được pha trộn vào một class thông qua từ khóa include. Nó được gọi là mixin. Sau khi pha trộn một module, các hành vi được khai báo trong module đó thì cũng sử dụng được ở trong class và các đối tượng của class đó.

Chúng ta sẽ đi qua những ví dụ cho những thuật ngữ được nêu ở phía trên ở những chương sau.

Đối tượng là gì?

Có lẽ trong cộng đồng Ruby bạn sẽ thường xuyên nghe đến câu nói, "In Ruby, everything is an object!" (Trong Ruby moitj thứ đều là đối tượng). Chúng ta tránh xa thực tế này quá xa vì đối tượng là một chủ đề advanced và nó cần được xử lý trên những cú pháp đơn giản của Ruby trước khi bạn bắt đầu nghĩ đến đối tượng là gì.

Nhưng điều đó là đúng, mọi thứ trong Ruby đều là một đối tượng. Đối tượng được tạo từ các class. Cứ nghĩ class là như là một cái khuôn còn đối tượng là những thứ được sinh ra từ cái khuôn đó. Từng đối tượng chứa những thông tin khác nhau so với những đối tượng khác khi mà chúng cùng đều là thực thể của cùng một class. Đây là một ví dụ về hai đối tượng của class String:

irb :001 > "hello".class
=> String
irb :002 > "world".class
=> String

Ở ví dụ trên, chúng ta sử dụng phương thức class để xác định class của mỗi đối tượng. Như bạn có thể thấy, mọi thứ chúng ta sử dùng, từ strings cho đến integers đều là các đối tượng, là thực thể của một class. Chúng ta sẽ đi sâu hơn về vấn đề này sớm thôi 😃.

Class định nghĩa đối tượng

Ruby định nghĩa các thuộc tính, hành vi của các đối tượng trong class. Bạn có thể nghĩ class như là phác thảo của đối tượng mà ta muốn tạo nên và những gì nó có thể làm. Để định nghĩa một class, chúng ta sử dụng cú pháp tương tự như định nghĩa một phương thức. Chúng ta thay thế từ khóa def bằng class và sử dụng cách đặt tên CamelCase để tạo tên. Sau đó chúng ta sử dụng từ khóa end để hoàn thành việc định nghĩa. Tên file trong Ruby nên sử dụng cách đặt tên snake_case, ngược lại so với tên class. Do đó ở ví dụ phía dưới, tên file là good_dog.rb và tên class là GoodDog.

# good_dog.rb

class GoodDog
end

sparky = GoodDog.new

Ở ví dụ bên trên, chúng ta tạo một thực thể của class GoodDog và lưu nó trong biến sparky. Giờ chúng ta có một đối tượng. Chúng ta nói rằng sparky là một đối tượng hay thực thể của class GoodDog. Toàn bộ việc tạo đối tượng hay thực thể của class này thì được gọi là instantiation (thực thể hóa), vì vậy chúng ta có thể nói rằng chúng ta thực thể hóa một đối tượng gọi là sparky từ class GoodDog. Thuật ngữ trong OOP thì thỉnh thoảng bạn sẽ sử dụng, nhưng điều quan trọng ở đây là một đối tượng được trả về bằng cách gọi phương thức class new. Cùng xem qua hình bên dưới để trực quan hóa những gì chúng ta đang làm.

Như bạn có thể thấy, việc định nghĩa và tạo một đối tượng của một class thì khá đơn giản. Nhưng trước khi chúng ta đi xa hơn trong việc tạo ra các class phức tạp hơn, chúng ta cùng nói qua về module.

Modules

Như đã nhắc đến ở phía trước, module là một cách khác để áp dụng đa hình vào Ruby. Một module là một tập các hành vi mà có thể tái sử dụng ở những class khác thông qua mixins. Một module là kết hợp của một class sử dụng từ khóa include. Ví dụ chúng ta muốn class GoodDog có phương thức speak nhưng chúng ta có nhiều class khác muốn sử dụng phương thức speak nữa. Và đây là cách ta dùng nó.

# good_dog.rb

module Speak
  def speak(sound)
    puts "#{sound}"
  end
end

class GoodDog
  include Speak
end

class HumanBeing
  include Speak
end

sparky = GoodDog.new
sparky.speak("Arf!")        # => Arf!
bob = HumanBeing.new
bob.speak("Hello!")         # => Hello!

Chú ý là ở ví dụ phía trên, cả đối tượng sparky và bob đều sử dụng phương thức speak. Việc kết hợp module Speak là có thể, đó là khi chúng ta đưa method speak vòa trong class GoodDog và HumanBeing.

Method Lookup

Khi bạn gọi một phương thức, làm sao mà Ruby có thể biết tìm phương thức đó ở đâu? Ruby có đường dẫn tìm kiếm duy nhất mà nó sinh ra mỗi khi một phương thức được gọi. Chúng ta cùng sử dụng chương trình phía trên để thấy được đường dẫn đó là gì cho class GoodDog. Chúng ta có thể sử dụng phương thức ancestors ở bất cứ class nào để tìm được chuỗi đường dẫn tìm kiếm.

module Speak
  def speak(sound)
    puts "#{sound}"
  end
end

class GoodDog
  include Speak
end

class HumanBeing
  include Speak
end

puts "---GoodDog ancestors---"
puts GoodDog.ancestors
puts ''
puts "---HumanBeing ancestors---"
puts HumanBeing.ancestors

Kết quả ta nhận được là:

---GoodDog ancestors-
GoodDog
Speak
Object
Kernel
BasicObject

---HumanBeing ancestors-
HumanBeing
Speak
Object
Kernel
BasicObject

Module Speak là nơi đúng đắn giữa các class cần tùy chỉnh (vd: GoodDog và HumanBeing) và class Object từ Ruby. Trong việc kế thừa, bạn sẽ thấy cách một chuỗi đường dẫn tìm kiếm làm gì khi ta làm việc với cả mixin và kế thừa trong class.

Điều này có nghĩa là khi mà phương thức speak không được định nghĩa trong class GoodDog, thì chỗ tiếp theo mà nó tìm kiếm sẽ là ở module Speak. Điều này lặp lại tuần tự, tuyến tính cho đến khi phương thức được tìm ra hoặc không có còn chỗ nào cho nó tìm nữa.

Tổng kết

Tổng quan về OOP trong Ruby được trình bày khá nhanh. Chúng ta sẽ trải qua một số chương tiếp theo sẽ đi sâu vào chi tiết hơn.