Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Object Marshalling trong Ruby: Hướng dẫn Thực tế

Trong bài viết này, chúng ta sẽ đi sâu vào object marshalling. Chúng ta sẽ giải thích nó là gì, nhìn vào module Marshall, và sau đó đi qua một ví dụ. Chúng ta sẽ sau đó đi sâu một bước nữa và so sánh các phương thức _dump và self._load. Cùng bắt đầu!

Object Marshalling là gì?

Khi bạn viết code, bạn có thể muốn lưu một object và truyền nó cho chương trình khác hoặc tái sử dụng nó trong lần chạy chương trình tiếp theo. Object marshalling được dùng trong Sidekiq, ví dụ; khi một Sidekiq job được enqueue trong Rails application, thì một serialization của job này — không gì hơn là một object — được chèn vào Redis. Process Sidekiq sau đó có thể deserialize JSON này và tái tạo job gốc từ JSON.

Trong lập trình máy tính, quá trình serialization và deserialization của một object này là những gì chúng ta thường gọi là object marshalling. Bây giờ, hãy nhìn vào những gì Ruby cung cấp sẵn để xử lý Object Marshalling.

Module Marshal

Vì Ruby là ngôn ngữ lập trình hướng đối tượng hoàn toàn, nó cung cấp cách để serialize và lưu trữ object dùng module Marshal trong thư viện chuẩn. Nó cho phép bạn serialize một object thành byte stream có thể được lưu trữ và deserialized trong một Ruby process khác.

Vậy, hãy serialize một string và nhìn gần hơn vào object đã serialized.

hello_world = 'hello world!' serialized_string = Marshal.dump(hello_world) # => "\x04\bI\"\x11hello world!\x06:\x06ET" serialized_string.class # => String deserialized_hello_world = Marshal.load(serialized_string) # => "hello world!" hello_world.object_id # => 70204420126020 deserialized_hello_world.object_id # => 70204419825700

Chúng ta sau đó gọi phương thức module Marshal.dump để serialize string của chúng ta. Chúng ta lưu giá trị trả về — chứa string đã serialized — trong biến serialized_string. String này có thể được lưu trong file và file có thể được tái sử dụng để tái tạo object gốc trong process khác. Chúng ta sau đó gọi phương thức Marshal.load để tái tạo object gốc từ byte stream.

Chúng ta có thể thấy string tái tạo mới này có object_id khác với string hello_world, nghĩa là nó là object khác, nhưng chứa cùng dữ liệu.

Khá cool! Nhưng module Marshal có thể tái tạo string như thế nào? Và, nếu tôi muốn kiểm soát attribute nào được serialize và deserialize thì sao?

Một Ví dụ Cụ thể về Object Marshalling

Để trả lời các câu hỏi này, hãy triển khai một marshalling strategy trên một custom struct tên User.

User = Struct.new(:fullname, :age, :roles) user = User.new('Mehdi Farsi', 42, [:admin, :operator])

Struct User định nghĩa 3 attribute: fullname, age, và roles. Cho ví dụ này chúng ta có business rule chỉ serialize khi match tiêu chí sau:

  • fullname chứa ít hơn 64 ký tự
  • Mảng roles không chứa role :admin

Để làm vậy, chúng ta có thể định nghĩa phương thức User#marshal_dump để triển khai custom serialization strategy của mình. Phương thức này sẽ được gọi khi chúng ta invoke phương thức Marshal.dump với instance của struct User làm tham số. Hãy định nghĩa phương thức này:

User = Struct.new(:age, :fullname, :roles) do def marshal_dump {}.tap do |result| result[:age] = age result[:fullname] = fullname if fullname.size <= 64 result[:roles] = roles unless roles.include? :admin end end end user = User.new(42, 'Mehdi Farsi', [:admin, :operator]) user_dump = Marshal.dump(user) # 'in User#marshal_dump' user_dump # => "\x04\bU:\tUser{\a:\bageI\"\x10Mehdi Farsi\x06:\x06ET:\rfullnamei/" 

Trong ví dụ trên, chúng ta có thể thấy phương thức User#marshal_dump của chúng ta được gọi khi chúng ta invoke Marshal.dump(user). Biến user_dump chứa string là serialization của instance User của chúng ta.

Bây giờ chúng ta đã có dump, hãy deserialize nó để tái tạo user. Để làm vậy, chúng ta định nghĩa phương thức User#marshal_load chịu trách nhiệm triển khai deserialization strategy của một User dump.

Vậy hãy định nghĩa phương thức này.

User = Struct.new(:age, :fullname, :roles) do def marshal_dump {}.tap do |result| result[:age] = age result[:fullname] = fullname if fullname.size <= 64 result[:roles] = roles unless roles.include? :admin end end def marshal_load(serialized_user) self.age = serialized_user[:age] self.fullname = serialized_user[:fullname] self.roles = serialized_user[:roles] || [] end end user = User.new(42, 'Mehdi Farsi', [:admin, :operator]) user_dump = Marshal.dump(user) # 'in User#marshal_dump' user_dump # => "\x04\bU:\tUser{\a:\bagei/:\rfullnameI\"\x10Mehdi Farsi\x06:\x06ET" original_user = Marshal.load(user_dump) # 'in User#marshal_load' original_user # => #<struct User age=42, fullname="Mehdi Farsi", roles=[]>

Trong ví dụ trên, chúng ta có thể thấy phương thức User#marshal_load của chúng ta được gọi khi chúng ta invoke Marshal.load(user_dump). Biến original_user chứa một struct là sự tái tạo của user instance của chúng ta.

Lưu ý rằng original_user.roles không giống với mảng user.roles vì trong quá trình serialization, user.roles bao gồm role :admin. Vì vậy user.roles không được serialized vào biến user_dump.

Các phương thức _dump và self._load

Khi Marshal.dump và Marshal.load được invoke, các phương thức này gọi marshal_dump và marshal_load trên object được truyền làm tham số của các phương thức này.

Nhưng, nếu tôi nói rằng phương thức Marshal.dump và Marshal.load thử gọi hai phương thức khác tên là _dump và self._load trên object được truyền làm tham số thì sao?

Phương thức _dump

Sự khác biệt giữa phương thức marshal_dump và _dump là:

  • bạn cần xử lý serialization strategy ở lower level khi dùng _dump — bạn cần trả về một string đại diện cho data cần serialize
  • phương thức marshal_dump có precedence cao hơn _dump nếu cả hai đều được định nghĩa

Hãy nhìn ví dụ sau:

User = Struct.new(:age, :fullname, :roles) do def _dump level [age, fullname].join(':') end end user = User.new(42, 'Mehdi Farsi', [:admin, :operator]) Marshal.dump(user) # => "\x04\bIu:\tUser\x1342:Mehdi Farsi\x06:\x06EF"

Trong phương thức User#_dump, chúng ta phải instantiate và trả về serialization object — string đại diện cho serialization của bạn.

Trong ví dụ sau, chúng ta định nghĩa cả User#marshal_dump và User#_dump và trả về string để xem phương thức nào được gọi

User = Struct.new(:age, :fullname, :roles) do def marshal_dump 'in User#marshal_dump' end def _dump level 'in User#_dump' end end user = User.new(42, 'Mehdi Farsi', [:admin, :operator]) user_dump = Marshal.dump(user) # "in User#marshal_dump"

Chúng ta có thể thấy chỉ User#marshal_dump được gọi dù cả hai đều được định nghĩa.

Phương thức self._load

Bây giờ, hãy nhìn vào phương thức marshal_load và _load.

Sự khác biệt giữa marshal_load và _load là:

  • Bạn cần xử lý deserialization strategy ở lower level khi dùng _load — Bạn chịu trách nhiệm instantiate object gốc.
  • Phương thức marshal_load nhận một deserialized object làm argument khi self._load nhận serialized string làm argument.
  • Phương thức marshal_load là instance method khi self._load là class method.

Hãy xem ví dụ sau:

User = Struct.new(:age, :fullname, :roles) do def _dump level [age, fullname].join(':') end def self._load serialized_user user_info = serialized_user.split(':') new(*user_info, Array.new) end end user = User.new(42, 'Mehdi Farsi', [:admin, :operator]) user_dump = Marshal.dump(user) user_dump # => "\x04\bIu:\tUser\x1342:Mehdi Farsi\x06:\x06EF" original_user = Marshal.load(user_dump) original_user # => #<struct User age="Mehdi Farsi", fullname=42, roles=[]>

Trong phương thức User._load:

  • chúng ta deserialize string được trả về bởi phương thức User#_dump
  • chúng ta instantiate một User mới bằng cách truyền thông tin đã deserialize

Chúng ta có thể thấy chúng ta chịu trách nhiệm allocate và instantiate object dùng để tái tạo user gốc.

Vì vậy Marshal.load kết hợp với marshal_load lo việc instantiate reconstituted original object. Sau đó nó gọi marshal_load với serialized object truyền làm argument trên object vừa instantiated.

Ngược lại, lời gọi Marshal.load kết hợp với _load để phương thức lớp self._load chịu trách nhiệm:

  • deserialize data được trả về bởi phương thức _dump
  • instantiate reconstituted original object

Kết luận

Tùy vào nhu cầu của bạn, bạn có thể quyết định triển khai serialization/deserialization strategy ở mức cao hoặc thấp. Để làm vậy, bạn có thể dùng module Marshal kết hợp với các Marshal hook methods thích hợp.

Voilà!