DRY trong Ruby bằng inheritance và mixin
Bài đăng này đã không được cập nhật trong 5 năm
Trong bài viết này mình xin đề cập đến cách cơ bản để giảm thiểu trùng lặp code trong ứng dụng, bạn sẽ hay nghe đến DRY - Don't Repeat Yourself.
Inheritance
Ruby là một ngôn ngữ lập trình hướng đối tượng (OOP) nên nó có tính chất inheritance, và nó dùng cho các class.
Trong ruby thì chỉ cần khai báo đơn giản bởi ký tự <
, và thứ tự là con <
cha. Hãy xem ví dụ đơn giản như sau.
class Animal
attr_accessor :color
def speak
"Hello!"
end
def self.is_human?
false
end
end
class Dog < Animal
attr_accessor :name
def initialize name
@name = name
end
def speak
"#{self.name} says arf!"
end
end
class Cat < Animal
end
Ở trên khai báo 3 class gồm Animal
, và Dog
, Cat
là 2 class con trực tiếp của Animal
. Khi đó ta có như sau:
2.5.1 :001 > Animal.class
=> Class
2.5.1 :002 > Dog.class
=> Class
2.5.1 :003 > Dog.superclass
=> Animal
2.5.1 :004 > Cat.superclass
=> Animal
superclass
của Dog
và Cat
đều là Animal
2.5.1 :005 > Cat.is_human?
=> false
2.5.1 :010 > cat = Cat.new
=> #<Cat:0x00000000026644a8>
2.5.1 :011 > cat.color = "grey"
=> "grey"
2.5.1 :012 > cat
=> #<Cat:0x00000000026644a8 @color="grey">
Cat
không cần định nghĩa gì cả nhưng vẫn thực hiện được câu lệnh trên là bởi vì Cat
thừa hưởng lại tất cả gồm class method, instance method và biến từ cha của nó là class Animal
.
2.5.1 :005 > Dog.is_human?
=> false
2.5.1 :006 > dog = Dog.new("Reck")
=> #<Dog:0x000000000254f8d8 @name="Reck">
2.5.1 :007 > dog.color = "brown"
=> "brown"
2.5.1 :008 > dog
=> #<Dog:0x000000000254f8d8 @name="Reck", @color="brown">
2.5.1 :032 > cat.name = "Mimi"
NoMethodError (undefined method `name=' for #<Cat:0x00000000026644a8 @color="grey">)
Dog
cũng được thừa hưởng các method và biến từ Animal
, ngoài biến color
từ cha thì nó còn định nghĩa thêm cho mình biến name
. Cho nên khác Cat
, Dog
có thêm biến cho object của mình.
2.5.1 :009 > dog.speak
=> "Reck says arf!"
2.5.1 :013 > cat.speak
=> "Hello!"
Trong Dog
và Animal
đều có method speak
, như này nghĩa là Dog
đang ghi đè lại chính method của cha mà nó kế thừa, còn Cat
thì không ghi đè method này. Khi gọi speak
như trên thì dog
sẽ cho nội dung bên trong method speak
của class Dog
, còn với cat
thì nó lấy luôn method speak
của class Animal
.
Việc ghi đè lại method của cha trong cây kế thừa chính là tính chất polymorphism
- đa hình - một trong 4 tính chất quan trọng của OOP.
super
Với tính kế thừa của mình, Ruby cung cấp cho bạn method rất hay đấy là super
. Khi bạn gọi super
tại một method trong class hiện tại, nó sẽ tìm ngược lên các class cha trong cây kế thừa để tìm ra method có cùng tên với method kia để thực hiện nó. Hãy xem ví dụ để dễ hình dung:
class Animal
def speak
"Hello!"
end
end
class Dog < Animal
def speak
super + " from GoodDog class"
end
end
2.5.1 :001 > dog = Dog.new
=> #<Dog:0x0000000001e88950>
2.5.1 :002 > dog.speak
=> "Hello! from Dog class"
Ta thấy, khi gọi super
trong method speak
của class Dog
thì nó đã tìm và thực hiện luôn method speak
trong class Animal
.
Một cách quen thuộc để dùng super
là lúc khởi tạo với method initialize
như sau:
class Animal
attr_accessor :name
def initialize(name)
@name = name
end
end
class Dog < Animal
attr_accessor :color
def initialize(name, color)
super(name)
@color = color
end
end
2.5.1 :001 > Dog.new("Reck", "brown")
=> #<Dog:0x0000000002a5c718 @name="Reck", @color="brown">
2.5.1 :002 > dog.name
=> "Reck"
2.5.1 :003 > dog.color
=> "brown"
Như bạn thấy ở trên, class Dog
không có khởi tạo cho thuộc tính name
, và thay vào đó nó gọi đến super
để tìm vào method initialize
trong class Animal
. Và thế là sau khi khởi tạo xong thì dog
có luôn cả name
và color
.
Mixing trong module.
Một cách thường gặp khác để DRY code đấy là sử dụng module
.
Nhắc lại định nghĩa về module một chút, trong Ruby, module được dùng để gom nhóm các method, biến, hằng số với nhau (khi gom nhóm các class thì từ khóa module
lúc này đóng vai trò là namespace). Ta mixin module vào trong class bằng từ khóa include
, prepend
hoặc extend
.
Ví dụ với một cây kế thừa đơn giản như sau:
module Swimmable
def swim
"I'm swimming!"
end
end
class Animal; end
class Fish < Animal
include Swimmable # mixing in Swimmable module
end
class Mammal < Animal
end
class Cat < Mammal
end
class Dog < Mammal
include Swimmable # mixing in Swimmable module
end
Chạy thử ta được kết quả:
2.5.1 :022 > reck = Dog.new
=> #<Dog:0x0000000000cf3a28>
2.5.1 :023 > nemo = Fish.new
=> #<Fish:0x0000000000cef798>
2.5.1 :024 > paws = Cat.new
=> #<Cat:0x0000000000ce37b8>
2.5.1 :026 > reck.swim
=> "I'm swimming!"
2.5.1 :027 > nemo.swim
=> "I'm swimming!"
2.5.1 :028 > paws.swim
NoMethodError (undefined method `swim' for #<Cat:0x0000000000ce37b8>)
Ta thấy reck
và nemo
có thể gọi đến method swim
còn paws
thì không, bởi vì chỉ có class Dog
và Fish
đã mixin module Swimmable
ở bên trong nó.
Ancestors method
Trên đây là 2 cách để DRY code. Phần này mình xin giới thiệu cho các bạn một thủ thuật nhỏ để khi bạn gọi một method từ một class hoặc từ một instance cụ thể của class đó, method ancestors
.
Giả sử bạn có mô hình kế thừa kết hợp mixin như sau:
module Swimmable
def swim
"I'm swimming!"
end
end
class Animal
def swim
"Animal swimmable"
end
end
class Fish < Animal
include Swimmable
def swim
"Fish swimmable"
end
end
2.5.1 :021 > Fish.new.swim
=> "Fish swimmable"
Ta thấy khi gọi đến method swim
từ instance của class Fish
, nó sẽ gọi đến method swim
trong chính class đó. Mặc dù trong Animal
và Swimmable
cũng đều có method này, đó là việc ghi đè đã được nhắc đến bên trên. Nhưng bản chất nó là gì, hãy gọi method ancestors
để xem nó trả về cho bạn cái gì.
2.5.1 :022 > Fish.ancestors
=> [Fish, Swimmable, Animal, Object, Kernel, BasicObject]
Method ancestors
sẽ trả về danh sách tất cả các class tổ tiên trong cây kế thừa và kể cả các module được mixin bên trong các class của cây đó (Kernel
là module được mixin trong class Object
). Và mỗi khi gọi một method từ class hoặc instance của class đó, Ruby sẽ lần lượt đi theo thứ tự từng class và module theo kết quả trả về của method ancestors
để tìm kiếm method được gọi đến. Như ví dụ trên, method sẽ được tìm kiếm theo thứ tự Fish
--> Swimmable
--> Animal
--> Object
--> Kernel
--> BasicObject
. Tại đâu phát hiện ra có method swim
, ta sẽ nhảy vào thực hiện và trả ra kết quả, và kết quả trả ra là method swim
của Fish
.
Thêm một ví dụ cuối cùng, giả sử mình tạo thêm module Pree
và mixin nó trong class Fish
bằng từ khóa prepend
module Pree
def swim
"Prepend swimming!"
end
end
module Swimmable
def swim
"I'm swimming!"
end
end
class Animal
def swim
"Animal swimmable"
end
end
class Fish < Animal
prepend Pree
include Swimmable
def swim
"Fish swimmable"
end
end
2.5.1 :027 > Fish.ancestors
=> [Pree, Fish, Swimmable, Animal, Object, Kernel, BasicObject]
Với ví dụ này thì method sẽ được tìm kiếm theo thứ tự Pree
--> Fish
--> Swimmable
--> Animal
--> Object
--> Kernel
--> BasicObject
. Bạn có thể để ý, prepend
sẽ chèn module lên trước, còn include
sẽ chèn module ngay sau class đó.
Inheritance vs Modules
Phần cuối cùng trong bài viết này, mình xin đưa ra một vài so sánh giữa inheritance và module
- Ruby không cung cấp đa kế thừa kiểu một con có nhiều cha, nhưng có thể mixin thoải mái nhiều module trong class.
- Không thể tạo một instance của module, module chỉ dùng nhóm các method, biến, hằng và làm namespace
- Với quan hệ
is-a
, hãy dùng kế thừa, ex:Dog
is-a
Animal
. Với quan hệhas-a
, hãy dùng mixin, ex:Dog
has-a
khả năngswim
Tham khảo
https://launchschool.com/books/oo_ruby/read/inheritance#classinheritance https://www.oreilly.com/learning/ruby-cookbook-modules-and-namespaces
Cám ơn bạn đã theo dõi bài viết.
All rights reserved