Module hẳn là một tính năng thú vị và quen thuộc với Ruby developer, bạn có thể sử dụng chúng để tổ chức các chức năng đặc biệt và đính kèm class thay vì sử dụng kế thừa.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">module</span> <span class="token constant">MyModule</span> <span class="token keyword">def</span> my_method <span class="token comment"># do something</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">MyClass</span> include <span class="token constant">MyModule</span> <span class="token keyword">end</span> obj <span class="token operator">=</span> <span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">obj<span class="token punctuation">.</span>my_method</span> |
include
là cách phổ biến nhất để import code từ module vào một class, ngoài ra Ruby còn cung cấp 2 cách khác để làm điều này là extennd
và prepend
. Bài viết hôm nay mình sẽ phân biệt sự khác nhau khi sử dụng 3 cách để import code của module vào class.
Ancestors chain
Tạm dịch là chuỗi tổ tiên (nghe hơi chuối nên mình xin được sử dụng cụm từ ancestors chain)
Khi một class được tạo ra, nó sẽ có một list ancestors – là tên của class mà nó kế thừa, và những module mà nó import vào. Ví dụ:
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">module</span> <span class="token constant">MyModule</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">MyClass</span> include <span class="token constant">MyModule</span> <span class="token keyword">end</span> <span class="token operator">></span> <span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token constant">MyClass</span><span class="token punctuation">,</span> <span class="token constant">MyModule</span><span class="token punctuation">,</span> <span class="token builtin">Object</span><span class="token punctuation">,</span> <span class="token constant">Kernel</span><span class="token punctuation">,</span> <span class="token constant">BasicObject</span><span class="token punctuation">]</span> |
Như ta thấy ancestor chain này nó sẽ đi theo thứ tự kế thừa và include. Đầu tiên sẽ là class MyClass, và kết thúc là BasicObject – là class tổ tiên của Ruby. Khi tìm kiếm một method được gọi, Ruby sẽ thực hiện tìm kiếm qua từng class theo thứ tự trong list này, cho đến khi tìm thấy thì dừng lại, hoặc trả về method missing
nếu đã tìm tới BasicObject mà không thấy.
Hiểu được cơ bản về ancestors chain của Ruby class, bây giờ chúng ta có thể so sánh sự khác nhau giữa 3 cách import module.
include
include
là cách đơn giản nhất và thường được sử dụng khi muốn import một module. Khi include một module vào một class, Ruby sẽ add module này vào ancestors chain của class đó, ngay sau class đó, và trước superclass của class đó.
Với ví dụ bên trên, ta có thể thấy MyModule được insert vào ancestors chain của MyClass ngay phía sau MyClass và phía trước Object – superclass của MyClass.
Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">module</span> <span class="token constant">M1</span> <span class="token keyword">def</span> run puts <span class="token string">"I am M1"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">C1</span> include <span class="token constant">M1</span> <span class="token keyword">def</span> run puts <span class="token string">"I am C1"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span>ancestors <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token constant">C1</span><span class="token punctuation">,</span> <span class="token constant">M1</span><span class="token punctuation">,</span> <span class="token builtin">Object</span><span class="token punctuation">,</span> <span class="token constant">Kernel</span><span class="token punctuation">,</span> <span class="token constant">BasicObject</span><span class="token punctuation">]</span> <span class="token operator">></span> obj <span class="token operator">=</span> <span class="token constant">C1</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token operator">></span> obj<span class="token punctuation">.</span>run <span class="token operator">=</span><span class="token operator">></span> <span class="token string">"I am C1"</span> |
Khi ta gọi obj.run
, Ruby sẽ thực hiện tìm kiếm method my_method
trong ancestors chain của class của obj
là C1. Nó sẽ tìm kiếm từ dưới lên trên và thực thi method đầu tiên tìm thấy.
prepend
prepend
được available từ Ruby 2.0, tuy nhiên nó có vẻ ít được sử dụng hơn so với include
và extend
.
prepend
hoạt động tương tự như include
, tuy nhiên có điểm khác biệt là vị trí nó được import vào ancestors chain. Khi ta thực hiên prepend
một module vào class, module này sẽ được import vào trước class đó trong ancestors chain.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">module</span> <span class="token constant">M1</span> <span class="token keyword">def</span> run puts <span class="token string">"I am M1"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">C1</span> prepend <span class="token constant">M1</span> <span class="token keyword">def</span> run puts <span class="token string">"I am C1"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span>ancestors <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token constant">M1</span><span class="token punctuation">,</span> <span class="token constant">C1</span><span class="token punctuation">,</span> <span class="token builtin">Object</span><span class="token punctuation">,</span> <span class="token constant">Kernel</span><span class="token punctuation">,</span> <span class="token constant">BasicObject</span><span class="token punctuation">]</span> <span class="token operator">></span> obj <span class="token operator">=</span> <span class="token constant">C1</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token operator">></span> obj<span class="token punctuation">.</span>run <span class="token operator">=</span><span class="token operator">></span> <span class="token string">"I am M1"</span> |
extend
Khác với include
và prepend
, khi sử dụng extend
trong một class các method sẽ được import vào như là những class method chứ không phải là instance method.
Nếu ta sử dụng extend
thì module được extend vào class sẽ không được add vào ancestors chain của class đó. Vì vậy chúng ta không thể gọi những method trong module bằng object của class đó được.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">module</span> <span class="token constant">M1</span> <span class="token keyword">def</span> run puts <span class="token string">"I am M1"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">C1</span> extend <span class="token constant">M1</span> <span class="token keyword">end</span> <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span>ancestors <span class="token comment"># => [C1, Object, Kernel, BasicObject]</span> <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>run <span class="token comment"># => undefined method `run' for #<C1:....></span> |
Vậy thì khi sử dụng extend, module và class sẽ liên kết với nhau như thế nào? Khi sử dụng extend
Ruby sẽ import những method của module vào ancestors chain của singleton class
của class đó. Singleton class là những class chứa các singleton method và class method của một object (hoặc một class). Để hiểu về định nghĩa singleton class, singleton method, các bạn có thể tìm kiếm trong Viblo.
Khi các method được import vào singleton class, ta có thể gọi các method đó như là class method:
1 2 3 | <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span>run <span class="token comment"># => I am M1</span> |
Một cách thông thường để bạn có thể sử dụng 1 module mà có thể import cả class methods và instance methods là sử dụng included
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <span class="token keyword">module</span> <span class="token constant">M1</span> <span class="token keyword">module</span> <span class="token constant">ClassMethods</span> <span class="token keyword">def</span> my_class_method <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">included</span><span class="token punctuation">(</span>base<span class="token punctuation">)</span> base<span class="token punctuation">.</span><span class="token function">extend</span><span class="token punctuation">(</span><span class="token constant">ClassMethods</span><span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">def</span> my_instance_method <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">C1</span> include <span class="token constant">M1</span> <span class="token keyword">end</span> <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span>my_class_method <span class="token operator">></span> <span class="token constant">C1</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>my_instance_method |
Qua bài viết hi vọng các bạn hiểu được sự khác nhau của 3 cách import module vào class mà mình đã đề cập.
Thank for your reading!
Bài viết tham khảo từ: https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073