Ruby Concepts - Singleton Classes
Bạn đã bao giờ tự hỏi “singleton class” là gì chưa? Hay khi bạn đang nói chuyện với ai đó hoặc đọc một bài đăng và bắt gặp "singleton class" hay "singleton method" được sử dụng, và lúc đó, bạn chỉ mỉm cười và gật đầu rồi note nó lại để tìm kiếm sau này? Bây giờ là lúc ...
Bạn đã bao giờ tự hỏi “singleton class” là gì chưa? Hay khi bạn đang nói chuyện với ai đó hoặc đọc một bài đăng và bắt gặp "singleton class" hay "singleton method" được sử dụng, và lúc đó, bạn chỉ mỉm cười và gật đầu rồi note nó lại để tìm kiếm sau này? Bây giờ là lúc để bạn hiểu rõ về nó. Hy vọng bài viết này sẽ giải thích khái niệm này bằng ngôn ngữ trực quan hơn và giúp bạn thấy sự tiện dụng của nó.
Lưu ý phụ: rất nhiều thông tin trong bài viết xuất phát từ việc đọc cuốn sách The Well-Grounded Rubyist của David A. Black. Cuốn sách này có rất nhiều thông tin tuyệt vời và hiện là một trong những cuốn sách yêu thích của tôi về Ruby.
Code
Nếu bạn đã từng làm việc nhiều với Ruby, bạn có thể đã sử dụng những "singleton class" này mà không biết! Đầu tiên, tôi sẽ cho bạn thấy đoạn code mà có thể bạn đã viết, hãy xem một số ngữ cảnh sau:
class Config def self.from_file(filename) Config.new(YAML.load_file(filename)) end end dev_config = Config.from_file("config.dev.yaml") # => Config object with dev settings
Hoặc bạn cũng có thể đã từng thấy đoạn code này:
module Geometry class << self def rect_area(length, awidth) length * awidth end end end Geometry.rect_area(4, 5) # => 20
Cho đến bây giờ, có thể bạn vẫn gọi chúng là "class methods" hay "các phương thức lớp". Dường như là bạn đã đúng. Nhưng tại sao chúng hoạt động? Điều gì đang xảy ra ở đây?
Cụ thể :
Đây là một khái niệm mà nó là yếu tố chính làm cho Ruby trở nên tuyệt vời. Mỗi đối tượng riêng lẻ, cho dù là có cùng class, đều khác nhau và chúng có thể có các phương thức khác nhau được định nghĩa. Dưới đây là một ví dụ về lớp Pet:
class Pet def smolder "Generic cute pet smolder" end end succulent = Pet.new momo = Pet.new willy = Pet.new def momo.smolder "sassy cat smolder" end def willy.smolder "well-meaning dingus smolder" end
Giờ khi chúng ta gọi smolder cho succulent, phương thức mà chúng ta đã không thay đổi, nó sẽ trả về giá trị mà chúng ta định nghĩa ban đầu:
succulent.smolder # => Generic cute pet smolder"
Nhưng khi chúng ta gọi smolder cho willy hoặc momo, nó sẽ trả về giá trị khác:
momo.smolder # => "sassy cat smolder" willy.smolder # => "well-meaning dingus smolder"
Vậy, việc này hoạt động như thế nào? Liệu chúng ta đã định nghĩa lại hàm smolder cho mỗi đối tượng pet? Giờ hãy thử gọi như ví dụ bên dưới và xem output của chúng nhé:
succulent.singleton_methods # => [] momo.singleton_methods # => [:smolder] willy.singleton_methods # => [:smolder]
Chính là nó! Bạn đang sử dụng một singleton method!
Và giờ, tôi nghĩ bạn đã sẵn sàng để nói về thế nào là một singleton class.
Singleton Class là gì?
Đầu tiên, giống như một chương trình, chúng ta chia nhỏ câu hỏi thành: một singleton là gì? Có nhiều định nghĩa khác nhau và chúng có thể cụ thể hơn trong các trường hợp khác nhau, nhưng cốt lõi, một singleton là một cái gì đó mà nó chỉ có một, là thứ duy nhất thuộc loại của nó.
Điều đó có ý nghĩa gì trong Ruby? Đó là: khi bạn khởi tạo một đối tượng từ một lớp trong Ruby, nó biết về các phương thức mà lớp của nó cung cấp cho nó. Nó cũng biết làm thế nào để tìm kiếm tất cả các lớp tổ tiên cho lớp của nó(lớp cha, lớp cha của lớp cha...). Đó là lý do tại sao lớp thừa kế hoạt động.
“Ồ, tại sao class của tớ không có phương thức đó? Hãy kiểm tra class cha của nó. Và class cha của class cha đó. Vv.”
Một trong những điều tuyệt vời về Ruby là chuỗi tổ tiên có thiết kế rất rõ ràng. Có một bộ quy tắc cụ thể cho các đối tượng tìm kiếm tổ tiên của chúng, sao cho không bao giờ có sự nhầm lẫn nào khi phương thức được gọi.
Ngoài việc biết về lớp của mình, mỗi đối tượng được tạo ra với một singleton class mà nó biết. Tất cả các lớp singleton là một loại "lớp ma"(ghost class) hoặc, đơn giản hơn, một cái túi để giữ bất kỳ phương thức nào được định nghĩa chỉ cho riêng đối tượng này. Hãy thử gọi như sau:
momo.singleton_class # => #<Class:#<Pet:0x00007fea40060220>>
Trong cây phân cấp thừa kế, lớp này đứng ngay trước lớp thực tế của đối tượng(Pet). Tuy nhiên, bạn không thể nhìn thấy nó bằng cách nhìn vào tổ tiên của đối tượng này.
momo.class.ancestors # => [Pet, Object, Kernel, BasicObject]
Nhưng nếu chúng ta tìm kiếm cây tổ tiên của singleton class của nó:
momo.singleton_class.ancestors # => [#<Class:#<Pet:0x00007fea40060220>>, Pet, Object, Kernel, BasicObject]
Bạn có thể thấy rằng nó xuất hiện ở ngay đầu chuỗi. Vì vậy, khi momo tìm kiếm phương thức smolder, nó sẽ tìm kiếm đầu tiên trong lớp singleton của nó. Vì có một phương pháp smolder ở đó, nó gọi hàm này, thay vì tìm kiếm thêm lên trên để tìm một hàm được định nghĩa trong lớp Pet.
Điều này có liên quan gì đến class method?
Bây giờ là khi chúng ta bắt đầu nhận thấy sức mạnh của singleton class. Đừng quên rằng mỗi lớp chỉ là một đối tượng của lớp Class.
Pet.class # => Class
Và Class chỉ là một lớp cung cấp một số phương thức cho mọi thực thể của nó (các class) mà bạn tạo ra, giống như bất kỳ class nào khác.
Class.instance_methods(false) # => [:new, :allocate, :superclass]
Vì vậy, khi bạn định nghĩa "class method" mà bạn dự định gọi trực tiếp trên class, thực tế, điều bạn đang thực hiện là định nghĩa các phương thức cho riêng đối tượng Class đó - trong singleton class của nó!
class Pet def self.random %w{cat dog bird fish banana}.sample end end Pet.singleton_methods # => [:random]
Và ... nếu singleton class tồn tại, nó sẽ biến lớp cha thành singleton_classes được kế thừa từ class chính. Ví dụ sau giúp bạn hiểu hơn:
class Pet def self.random %w{cat dog bird fish banana}.sample end end class Reptile < Pet def self.types %w{lizard snake other} end end Reptile.singleton_methods # => [:types, :random] Reptile.singleton_class.ancestors # => [#<Class:Reptile>, #<Class:Pet>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
Hãy xem cách mà singleton class của Reptile kế thừa từ singleton class của Pet. Vậy liệu các class method của Pet cũng có thể dùng cho Reptile?
Ngoài lề
Chúng ta đã cover hầu hết những điều quan trọng bên trên, nhưng có một vài điều thú vị liên quan mà tôi muốn giới thiệu thêm:
- Class << self
Có hai cách để sử dụng từ khóa class: dùng trực tiếp và được theo sau bởi 1 hằng số ( class Gelato), hoặc theo sau là "toán tử <<" và một đối tượng ( class << momo). Bạn đã biết về cái đầu tiên - đó là cách bạn thường khai báo một lớp! Hãy tập trung vào cái thứ hai, đó là cú pháp để trực tiếp tạo ra một singleton class của đối tượng. Bạn có thể hiểu nó giống như cách định nghĩa các phương thức như chúng ta đã làm ở trên, nghĩa là:
# cách này: def momo.snug "*snug*" end # giống với: class << momo def snug "*snug*" end end
Bạn luôn làm điều này mỗi khi bạn muốn định nghĩa lại class để thêm nhiều hàm hơn:
class Gelato attr_reader :solidity def initialize @solidity = 100 end def melt @solidity -= 10 end end # And re-open it to add one more method class Gelato def refreeze @solidity = 100 end end dessert = Gelato.new 5.times { dessert.melt } dessert.solidity # => 50 dessert.refreeze # => 100
Cú pháp class << object; end là một cách khác để định nghĩa lại singleton class của đối tượng. Lợi ích của nó là bạn có thể định nghĩa các hằng số và nhiều phương thức cùng một lúc thay vì một phương thức một lần:
# Thay vì: def momo.pounce "pounce!" end def momo.hiss "HISS" end def momo.lives 9 end # chúng ta có thể thực hiện: class << momo def pounce "pounce!" end def hiss "HISS" end def lives 9 end end momo.singleton_methods # => [:pounce, :hiss, :lives, :smolder]
Đây là một mô hình phổ biến khi thêm nhiều class methods cho 1 class, hãy xem ví dụ bên dưới:
class Pet class << self def random %w{cat dog bird fish banana}.sample end end end # Bởi vì "self" ở bên trong class ddax khai báo, # nghĩa là 'self == Pet', vì vậy, bạn cũng có thể gọi như sau: class Pet class << Pet def random # ... end end end
Có thể bạn đã từng nhìn thấy mô hình này và không biết nó là gì, hoặc có thể bạn biết nó dùng để thêm class method nhưng không biết tại sao. Giờ bạn đã biết về nó, tất cả là nhờ singleton class!
- class << self , def self.method , def Pet.method
Có một vài cách khác nhau để khai báo class method:
# 1. Trong global scope def Pet.random %w{cat dog bird fish banana}.sample end # 2. Bên trong lớp định nghĩa, sử dụng 'self' class Pet def self.random %w{cat dog bird fish banana}.sample end end # 3. Bên trong lớp định nghĩa, sử dụng << class Pet class << self def random %w{cat dog bird fish banana}.sample end end end # 4. Bên ngoài lớp định nghĩa, sử dụng << class << Pet def random %w{cat dog bird fish banana}.sample end end
Vậy sự khác biệt giữa chúng là gì? Khi nào thì bạn nên sử dụng một trong số chúng?
Tin tốt là về cơ bản tất cả đều giống nhau. Bạn có thể sử dụng cái nào làm cho bạn cảm thấy dễ chịu nhất và phù hợp với phong cách codebase của bạn. Sự khác biệt duy nhất là cách #3 và cách nó xử lý các hằng số và scope.
MAX_PETS = 3 def Pet.outer_max_pets MAX_PETS end class Pet MAX_PETS = 1000 class << self def inner_max_pets MAX_PETS end end end Pet.outer_max_pets # => 3 Pet.inner_max_pets # => 1000
Bạn có thấy hàm inner_max_pets có thể truy cập vào scope bên trong class Pet và các hằng số của nó không? Đó là sự khác biệt duy nhất.
Sử dụng Extend để sửa đổi Class đã tồn tại một cách an toàn
Hy vọng rằng, bạn đã đọc một bài đăng hoặc có ai đó cảnh báo bạn về những nguy hiểm khi định nghĩa lại các class Ruby đã được xây dựng sẵn. Bạn thực sự nên thật cẩn thận trong trường hợp này và có thể thực hiện như bên dưới:
class String def verbify self + "ify" end end "banana".verbify # => "bananaify"
Có một vài mối nguy hiểm có thể xảy ra: bạn có thể vô tình ghi đè các phương thức đã dựng sẵn, hoặc có các phương thức conflict với các thư viện khác trong cùng 1 project và làm cho mọi thứ không hoạt động giống như mong đợi. Từ khóa extend có thể giúp ta giải quyết các vấn đề đó!
Extend là gì?
Từ khóa extend khá giống với include, cho phép bạn load các function vào class/module của bạn từ class/module khác. Sự khác biệt là extend để các phương thức này trong singleton class của đối tượng.
module Wigglable def wiggle "*shimmy*" end end willy.extend(Wiggleable) willy.singleton_methods # => [:wiggle, :smolder]
Vì vậy, nếu bạn sử dụng extend trong 1 lớp thay vì include, các phương thức sẽ được thêm vào singleton class của lớp như các class method thay vì thêm vào chính lớp đó như các instance method.
module Hissy def hiss "HISS" end end class Reptile extend Hissy end snek = Reptile.new snek.hiss # => Error! Undefined method hiss for 'snek' Reptile.hiss # => "HISS"
Vậy nó giúp gì cho chúng ta?
Hãy nói rằng bạn thực sự cần có 1 phương thức verbify cho string mà bạn đang sử dụng. Trong khi bạn có thể tạo và sử dụng 1 lớp con của String, một lựa chọn khác là sử dụng extend cho một string riêng biệt!
module Verby def verbify self + "ify" end end noun = "pup" noun.extend(Verby) noun.verbify # => "pupify"
Như vậy, bạn đã thực sự biết về singleton class, và giờ bạn có thể định nghĩa riêng cho mình 1 class với method của chính bạn!
Link nguồn: Ruby Concepts - Singleton Classes