Yehuda Katz опубликовал эту запись в своем блоге 10 января 2010 года.
Огромное количество действительно хорошей функциональности Rails 2.3 скрыты в его монолитных компонентах. Я уже публиковал несколько сообщений о том, как мы упростили код маршрутизатора, диспетчера и некоторых частей ActionController, частично реорганизовав функциональность ActionPack. ActiveModel — еще один модуль, появившийся в Rails 3 после реорганизации полезной функциональности.
ActiveModel имеет два главных элемента. Первый — это API, интерфейс, которому должны соответствовать модели для совместимости с хелперами ActionPack. Дальше я расскажу о нем подробнее, а для начала важная деталь: вашу модель можно сделать подобной ActiveModel без единой строки Rails-кода.
Чтобы убедиться, подходят ли ваши модели для этого, ActiveModel предлагает модуль
Тесты модуля
Второй интересной частью ActiveModel является набор модулей для реализации стандартной функциональности в ваших собственных моделях. Код для них был изъят из ActiveRecord, а теперь они отдельно включены в него уже сами по себе.
Поскольку мы сами пользуемся этими модулями, вы можете быть уверены, что функции API, которые вы добавите в свои модели, будут оставаться совместимыми с ActiveRecord, и что они будут поддерживаться в будущих релизах Rails.
Встроенная в ActiveModel интернационализация дает широкие возможности сообществу для работы над переводом сообщений об ошибках и тому подобного.
Наверное, валидация была одним из наиболее разочаровывающих мест ActiveRecord, потому что людям, писавшим библиотеки, например, для CouchDB, приходилось выбирать между буквальным переписыванием API с возможностью внести разные несоответствия в процессе переписывания и изобретением полностью нового API.
В валидации присутствуют несколько новых элементов.
Во-первых, объявление самой валидации. Вы помните как это было раньше в ActiveRecord:
Система валидации вызывает
Для изменения способа поиска атрибута можно переопределить
Давайте посмотрим что такое валидатор по сути. В первую очередь, метод
Как видите,
Метод
Метод
В результате сообщение об ошибке будет выглядеть как
Объект Error также является частью ActiveModel.
В ActiveRecord также встроена сериализация для JSON и XML, позволяющая делать вещи типа
Важнейшая вещь для сериализации — это поддержка общего набора атрибутов, принимаемых всеми сериализаторами. То есть чтобы можно сделать
Чтобы добавить поддержку сериализации в вашу собственную модель, вам нужно добавить (заинклудить) модуль сериализации и реализацию метода
Для того, чтобы конкретные атрибуты преобразовывались какими-то методами, можно передать опцию
Вот модель Person с валидацией и сериализацией:
Мы познакомились всего с двумя модулями ActiveModel. Коротко об остальных:
Josh Peek реорганизовал методы из ActiveRecord в отдельные модули в рамках своего проекта для Google Summer of Code прошлым летом, и это только первый шаг всего процесса. Со временем я ожидаю увидеть больше вещей, выделенных из ActiveRecord, и больше абстракций вокруг ActiveModel.
Я также ожидаю от сообщества новых валидаторов, переводов, сериалайзеров и т.д., особенно сейчас, когда их можно использовать не только в ActiveRecord, но и в MongoMapper, Cassandra Object и других ORM, использующих модули ActiveModel.
Огромное количество действительно хорошей функциональности Rails 2.3 скрыты в его монолитных компонентах. Я уже публиковал несколько сообщений о том, как мы упростили код маршрутизатора, диспетчера и некоторых частей ActionController, частично реорганизовав функциональность ActionPack. ActiveModel — еще один модуль, появившийся в Rails 3 после реорганизации полезной функциональности.
Для начала — ActiveModel API
ActiveModel имеет два главных элемента. Первый — это API, интерфейс, которому должны соответствовать модели для совместимости с хелперами ActionPack. Дальше я расскажу о нем подробнее, а для начала важная деталь: вашу модель можно сделать подобной ActiveModel без единой строки Rails-кода.
Чтобы убедиться, подходят ли ваши модели для этого, ActiveModel предлагает модуль
ActiveModel::Lint
для тестирования совместимости с API — его нужно просто подключить (заинклудить) в тест:Copy Source | Copy HTML<br/>class LintTest < ActiveModel::TestCase<br/> include ActiveModel::Lint::Tests<br/> <br/> class CompliantModel<br/> extend ActiveModel::Naming<br/> <br/> def to_model<br/> self<br/> end<br/> <br/> def valid?() true end<br/> def new_record?() true end<br/> def destroyed?() true end<br/> <br/> def errors<br/> obj = Object.new<br/> def obj.[](key) [] end<br/> def obj.full_messages() [] end<br/> obj<br/> end<br/> end<br/> <br/> def setup<br/> @model = CompliantModel.new<br/> end<br/>end <br/>
Тесты модуля
ActiveModel::Lint::Tests
проверяют совместимость объекта @model
.Модули ActiveModel
Второй интересной частью ActiveModel является набор модулей для реализации стандартной функциональности в ваших собственных моделях. Код для них был изъят из ActiveRecord, а теперь они отдельно включены в него уже сами по себе.
Поскольку мы сами пользуемся этими модулями, вы можете быть уверены, что функции API, которые вы добавите в свои модели, будут оставаться совместимыми с ActiveRecord, и что они будут поддерживаться в будущих релизах Rails.
Встроенная в ActiveModel интернационализация дает широкие возможности сообществу для работы над переводом сообщений об ошибках и тому подобного.
Система валидации
Наверное, валидация была одним из наиболее разочаровывающих мест ActiveRecord, потому что людям, писавшим библиотеки, например, для CouchDB, приходилось выбирать между буквальным переписыванием API с возможностью внести разные несоответствия в процессе переписывания и изобретением полностью нового API.
В валидации присутствуют несколько новых элементов.
Во-первых, объявление самой валидации. Вы помните как это было раньше в ActiveRecord:
Copy Source | Copy HTML<br/>class Person < ActiveRecord::Base<br/> validates_presence_of :first_name, :last_name<br/>end<br/>Чтобы сделать то же самое на обыкновенном объекте Ruby, просто сделайте следующее:<br/> <br/>class Person<br/> include ActiveModel::Validations<br/> <br/> validates_presence_of :first_name, :last_name<br/> <br/> attr_accessor :first_name, :last_name<br/> def initialize(first_name, last_name)<br/> @first_name, @last_name = first_name, last_name<br/> end<br/>end <br/>
Система валидации вызывает
read_attribute_for_validation
для получения атрибута, но по умолчанию это просто алиас для send
, который поддерживает стандартную систему атрибутов Ruby через attr_accessor
.Для изменения способа поиска атрибута можно переопределить
read_attribute_for_validation
:Copy Source | Copy HTML<br/>class Person<br/> include ActiveModel::Validations<br/> <br/> validates_presence_of :first_name, :last_name<br/> <br/> def initialize(attributes = {})<br/> @attributes = attributes<br/> end<br/> <br/> def read_attribute_for_validation(key)<br/> @attributes[key]<br/> end<br/>end <br/>
Давайте посмотрим что такое валидатор по сути. В первую очередь, метод
validates_presence_of
:Copy Source | Copy HTML<br/>def validates_presence_of(*attr_names)<br/> validates_with PresenceValidator, _merge_attributes(attr_names)<br/>end <br/>
Как видите,
validates_presence_of
использует более примитивный validates_with
, передавая ему класс валидатора и добавляя к attr_names
ключ {:attributes => attribute_names}
. Дальше сам класс-валидатор:Copy Source | Copy HTML<br/>class PresenceValidator < EachValidator<br/> def validate(record)<br/> record.errors.add_on_blank(attributes, options[:message])<br/> end<br/>end<br/>
Метод
validate
в классе EachValidator
валидирует каждый атрибут. В данном случае он переопределен и добавляет сообщение об ошибке в объект только в том случае, если атрибут пустой.Метод
add_on_blank
вызывает add(attribute, :blank, :default => custom_message)
если value.blank?
(среди всего прочего), который добавляет локализированное :blank
сообщение в объект. Встроенный файл локализации для английского языка locale/en.yml
выглядит следующим образом:Copy Source | Copy HTML<br/>en:<br/> errors:<br/> # Полный формат сообщения об ошибке по умолчанию.<br/> format: "{{attribute}} {{message}}"<br/> <br/> # Значения :model, :attribute и :value всегда доступны для изменения<br/> # Значение :count доступно если оно применимо. Может быть использовано для множественного числа.<br/> messages:<br/> inclusion: "is not included in the list"<br/> exclusion: "is reserved"<br/> invalid: "is invalid"<br/> confirmation: "doesn't match confirmation"<br/> accepted: "must be accepted"<br/> empty: "can't be empty"<br/> blank: "can't be blank"<br/> too_long: "is too long (maximum is {{count}} characters)"<br/> too_short: "is too short (minimum is {{count}} characters)"<br/> wrong_length: "is the wrong length (should be {{count}} characters)"<br/> not_a_number: "is not a number"<br/> greater_than: "must be greater than {{count}}"<br/> greater_than_or_equal_to: "must be greater than or equal to {{count}}"<br/> equal_to: "must be equal to {{count}}"<br/> less_than: "must be less than {{count}}"<br/> less_than_or_equal_to: "must be less than or equal to {{count}}"<br/> odd: "must be odd"<br/> even: "must be even"<br/>
В результате сообщение об ошибке будет выглядеть как
first_name can't be blank
.Объект Error также является частью ActiveModel.
Сериализация
В ActiveRecord также встроена сериализация для JSON и XML, позволяющая делать вещи типа
@person.to_json(:except => :comment)
.Важнейшая вещь для сериализации — это поддержка общего набора атрибутов, принимаемых всеми сериализаторами. То есть чтобы можно сделать
@person.to_xml(:except => :comment)
.Чтобы добавить поддержку сериализации в вашу собственную модель, вам нужно добавить (заинклудить) модуль сериализации и реализацию метода
attributes
. Смотрите:Copy Source | Copy HTML<br/>class Person<br/> include ActiveModel::Serialization<br/> <br/> attr_accessor :attributes<br/> def initialize(attributes)<br/> @attributes = attributes<br/> end<br/>end<br/> <br/>p = Person.new(:first_name => "Yukihiro", :last_name => "Matsumoto")<br/>p.to_json #=> %|{"first_name": "Yukihiro", "last_name": "Matsumoto"}|<br/>p.to_json(:only => :first_name) #=> %|{"first_name": "Yukihiro"}|<br/>
Для того, чтобы конкретные атрибуты преобразовывались какими-то методами, можно передать опцию
:methods
; эти методы тогда будут вызваны динамически.Вот модель Person с валидацией и сериализацией:
Copy Source | Copy HTML<br/>class Person<br/> include ActiveModel::Validations<br/> include ActiveModel::Serialization<br/> <br/> validates_presence_of :first_name, :last_name<br/> <br/> attr_accessor :attributes<br/> def initialize(attributes = {})<br/> @attributes = attributes<br/> end<br/> <br/> def read_attribute_for_validation(key)<br/> @attributes[key]<br/> end<br/>end<br/>
Другие модули
Мы познакомились всего с двумя модулями ActiveModel. Коротко об остальных:
AttributeMethods
: Упрощает добавление методов класса для управления атрибутами типа table_name :foo
.Callbacks
: Колбеки жизненного цикла объекта в стиле ActiveRecord.Dirty
: Поддержка «грязных» объектов.Naming
: Дефолтные реализации model.model_name
, которые используются ActionPack (например, при render :partial => model
).Observing
: Обзерверы (наблюдатели) в стиле ActiveRecord.StateMachine
: Простая реализация конечного автомата.Translation
: Базовая поддержка переводов на другие языки (интеграция с фреймворком интернационализации I18n).Josh Peek реорганизовал методы из ActiveRecord в отдельные модули в рамках своего проекта для Google Summer of Code прошлым летом, и это только первый шаг всего процесса. Со временем я ожидаю увидеть больше вещей, выделенных из ActiveRecord, и больше абстракций вокруг ActiveModel.
Я также ожидаю от сообщества новых валидаторов, переводов, сериалайзеров и т.д., особенно сейчас, когда их можно использовать не только в ActiveRecord, но и в MongoMapper, Cassandra Object и других ORM, использующих модули ActiveModel.