При сохранении или передаче какой-либо информации мы часто используем сериализацию . Сериализация берет объект Ruby и преобразует его в строку байтов и наоборот. Например, если у вас есть объект, представляющий информацию о пользователе, и вам необходимо отправить ее по сети, он должен быть сериализован в набор байтов, которые можно протолкнуть через сокет. Затем, на другом конце, получатель должен десериализовать объект, преобразовав его обратно во что-то, что может понять Ruby (или другой язык).
Оказывается, существует множество способов сериализации объектов Ruby. В этой статье я расскажу о YAML, JSON и MessagePack, исследуя их фортепиано и форты, чтобы увидеть их в действии с Ruby. В конце мы соберем модульный подход к сериализации, используя некоторые приемы метапрограммирования.
Давайте прыгать в!
YAML
YAML — это рекурсивная аббревиатура, которая расшифровывается как «YAML — это не язык разметки». Это формат сериализации, но он также (легко) удобочитаем, что означает, что его можно использовать в качестве языка конфигурации. На самом деле Rails использует YAML для выполнения всех видов конфигурации, например, подключения к базе данных.
Давайте посмотрим на пример:
name: "David" height: 124 age: 28 children: "John": age: 1 height: 10 "Adam": age: 2 height: 20 "Robert": age: 3 height: 30 traits: - smart - nice - caring
Формат YAML невероятно прост для понимания. Самый быстрый способ сделать это кликом — это преобразовать его в хеш-объект Ruby или Javascript. Мы пойдем с первым (сохранив вышеупомянутый YAML в test.yaml ):
require 'yaml' YAML.load File.read('test.yaml')
Запуск вышеупомянутого в Pry даст вам красиво отформатированный результат, который выглядит следующим образом:
{"name"=>"David", "height"=>124, "age"=>28, "children"=>{"John"=>{"age"=>1, "height"=>10}, "Adam"=>{"age"=>2, "height"=>20}, "Robert"=>{"age"=>3, "height"=>30}}, "traits"=>["smart", "nice", "caring"]}
Как видите, двоеточия представляют пары «ключ-значение», а вкладки создают новый хэш. Маленькие дефисы говорят YAML, что нам нужен список, а не хеш. Этот простой перевод между словарями YAML и Ruby является одним из основных преимуществ YAML.
require 'yaml' class Person attr_accessor :name, :age, :gender def initialize(name, age, gender) @name = name @age = age @gender = gender end def to_yaml YAML.dump ({ :name => @name, :age => @age, :gender => @gender }) end def self.from_yaml(string) data = YAML.load string p data self.new(data[:name], data[:age], data[:gender]) end end p = Person.new "David", 28, "male" p p.to_yaml p = Person.from_yaml(p.to_yaml) puts "Name #{p.name}" puts "Age #{p.age}" puts "Gender #{p.gender}"
Давайте разберем код. У нас to_yaml
метод to_yaml
:
def to_yaml YAML.dump ({ :name => @name, :age => @age, :gender => @gender }) end
Мы создаем хэш Ruby и превращаем его в строку YAML, используя модули, предоставляемые стандартной библиотекой. Чтобы перейти в другом направлении и преобразовать строку YAML в объект Ruby:
def self.from_yaml(string) data = YAML.load string p data self.new(data[:name], data[:age], data[:gender]) end
Здесь возьмите строку, преобразуйте ее в хеш Ruby, затем используйте содержимое нашего хеша с конструктором для создания нового экземпляра Person
.
Теперь давайте посмотрим, как YAML сравнивается с тяжеловесом из земли Javascript.
JSON
В некотором смысле, JSON очень похож на YAML. Он предназначен для восприятия человеком формата, который часто служит форматом конфигурации. Оба широко применяются в сообществе Ruby. Однако JSON отличается тем, что берет свои корни из Javascript. Фактически, JSON фактически означает нотацию объектов Javascript. Синтаксис для JSON почти такой же, как синтаксис для определения объектов Javascript (которые в некоторой степени аналогичны хэшам Ruby). Давайте посмотрим на пример:
{ "name": "David", "height": 124, "age": 28, "children": {"John": {"age": 1, "height": 10}, "Adam": {"age": 2, "height": 20}, "Robert": {"age": 3, "height": 30}}, "traits": ["smart", "nice", "caring"] }
Это действительно похоже на старый добрый хэш Ruby. Похоже, единственное отличие состоит в том, что отношение пары ключей выражается символом «:» в JSON вместо =>
мы находим в Ruby.
Давайте посмотрим, как именно выглядит пример в Ruby:
require 'json' JSON.load File.read("test.json") {"name"=>"David", "height"=>124, "age"=>28, "children"=>{"John"=>{"age"=>1, "height"=>10}, "Adam"=>{"age"=>2, "height"=>20}, "Robert"=>{"age"=>3, "height"=>30}}, "traits"=>["smart", "nice", "caring"]}
Мы можем добавить набор методов к классу Person
разработанному ранее, делая его JSON-сериализуемым:
require 'json' class Person ... def to_json JSON.dump ({ :name => @name, :age => @age, :gender => @gender }) end def self.from_json(string) data = JSON.load string self.new(data['name'], data['age'], data['gender']) end ... end
Основной код точно такой же, за исключением того факта, что методы используют JSON
вместо YAML
!
Что отличает JSON от всего остального, так это его сходство с синтаксисом Ruby и Javascript. При написании кода требуется некоторая умственная энергия для переключения между YAML и Ruby. С JSON такой проблемы нет, поскольку синтаксис почти идентичен синтаксису Ruby. Кроме того, многие современные браузеры по умолчанию имеют реализацию JSON Javascript, что делает его языком общения AJAX.
С другой стороны, YAML требует дополнительной библиотеки и просто не имеет столько последователей в сообществе Javascript. Если вашей основной целью для метода сериализации является взаимодействие с Javascript, сначала посмотрите на JSON.
MessagePack
До сих пор мы не обращали особого внимания на то, сколько места занимает сериализованный объект. Оказывается, что малый размер сериализации является очень важной характеристикой, особенно для систем, которые требуют малой задержки и высокой пропускной способности. Вот где вступает MessagePack .
В отличие от JSON и YAML, MessagePack не предназначен для чтения человеком! Это двоичный формат, который означает, что он представляет свою информацию в виде произвольных байтов, не обязательно байтов, которые представляют алфавит. Преимущество этого состоит в том, что его сериализации часто занимают значительно меньше места, чем их аналоги из YAML и JSON. Хотя это исключает MessagePack как формат файла конфигурации, это делает его очень привлекательным для тех, кто строит быстрые, распределенные системы.
Давайте посмотрим, как использовать его с Ruby. В отличие от YAML и JSON, MessagePack не поставляется в комплекте с Ruby (пока!). Итак, давайте возьмем себе копию:
gem install msgpack
Мы можем немного возиться с этим:
require 'msgpack' msg = {:height => 47, :width => 32, :depth => 16}.to_msgpack #prints out mumbo-jumbo p msg obj = MessagePack.unpack(msg) p obj
Сначала создайте стандартный хеш Ruby и вызовите to_msgpack
. Это возвращает сериализованную версию MessagePack хэша. Затем отмените сериализацию сериализованного хэша с помощью MessagePack.unpack
(мы должны вернуть оригинальный хеш). Конечно, мы можем использовать наши старые добрые методы конвертера (обратите внимание на похожий API):
class Person ... def to_msgpack MessagePack.dump ({ :name => @name, :age => @age, :gender => @gender }) end def self.from_msgpack(string) data = MessagePack.load string self.new(data['name'], data['age'], data['gender']) end ... end
Итак, MessagePack следует использовать, когда мы чувствуем потребность в скорости, JSON — для связи с Javascript, а YAML — для файлов конфигурации. Но, как правило, вы не будете уверены, какой из них выбрать при запуске большого проекта, так как же мы можем оставить наши варианты открытыми?
Модуляризация с помощью Mixins
Ruby — это динамический язык с некоторыми замечательными функциями метапрограммирования. Давайте использовать их, чтобы удостовериться, что мы не используем подход, о котором позже могли бы сожалеть. Прежде всего, обратите внимание, что созданные ранее методы сериализации / десериализации Person
кажутся очень похожими.
Давайте превратим это в миксин:
require 'json' #mixin module BasicSerializable #should point to a class; change to a different #class (eg MessagePack, JSON, YAML) to get a different #serialization @@serializer = JSON def serialize obj = {} instance_variables.map do |var| obj[var] = instance_variable_get(var) end @@serializer.dump obj end def unserialize(string) obj = @@serializer.parse(string) obj.keys.each do |key| instance_variable_set(key, obj[key]) end end end
Прежде всего, обратите внимание, что @@serializer
установлен на класс сериализации. Это означает, что мы можем немедленно изменить наш метод сериализации, если наши сериализуемые классы включают этот модуль.
При более внимательном рассмотрении кода в основном рассматриваются переменные экземпляра для сериализации и десериализации объекта / строки. В методе serialize
:
def serialize obj = {} instance_variables.map do |var| obj[var] = instance_variable_get(var) end @@serializer.dump obj end
Он зацикливается на instance_variables
и создает хэш Ruby для имен переменных и их значений. Затем просто используйте @@serializer
для выгрузки объекта. Если механизм сериализации не имеет метода dump
, мы можем просто создать его подкласс, чтобы присвоить ему этот метод!
Мы используем аналогичный подход с методом unserialize:
def unserialize(string) obj = @@serializer.parse(string) obj.keys.each do |key| instance_variable_set(key, obj[key]) end end
Здесь используйте сериализатор, чтобы получить хеш Ruby из строки и установить переменные экземпляра объекта в значения хеша.
Это делает наш класс Person
действительно простым в реализации:
class Person include BasicSerializable attr_accessor :name, :age, :gender def initialize(name, age, gender) @name = name @age = age @gender = gender end end
Обратите внимание, мы просто добавляем include BasicSerializable
! Давайте проверим это:
p = Person.new "David", 28, "male" p p.serialize p.unserialize (p.serialize) puts "Name #{p.name}" puts "Age #{p.age}" puts "Gender #{p.gender}"
Теперь, если вы тщательно прочесываете код (или просто понимаете основные понятия), вы можете заметить, что методы BasicSerializable
работают очень хорошо для объектов, которые имеют только сериализуемые переменные экземпляра (например, целые числа, строки, числа с плавающей точкой и т. Д. Или массивы и хэши). их). Однако это не удастся для объекта, который имеет другие BasicSerializable
объекты в качестве экземпляров.
Легко исправить эту проблему — переопределить методы serialize
и unserialize
в таких классах, например:
class People include BasicSerializable attr_accessor :persons def initialize @persons = [] end def serialize obj = @persons.map do |person| person.serialize end @@serializer.dump obj end def unserialize(string) obj = @@serializer.parse string @persons = [] obj.each do |person_string| person = Person.new "", 0, "" person.unserialize(person_string) @persons << person end end def <<(person) @persons << person end end
Заканчивать
Сериализация — довольно важная тема, которая часто упускается из виду. Выбор правильного метода сериализации может значительно облегчить вам жизнь, когда придет время оптимизации. Наряду с нашим охватом методов сериализации, модульный подход (возможно, потребуется изменить его для конкретных приложений) может помочь вам изменить свое решение на более позднем этапе.