ActiveModel: пусть любой Ruby объект почувствует себя ActiveRecord

Автор оригинала: Yehuda Katz
  • Перевод
Yehuda Katz опубликовал эту запись в своем блоге 10 января 2010 года.

Огромное количество действительно хорошей функциональности 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.

Комментарии 19

  • НЛО прилетело и опубликовало эту надпись здесь
      0
      ммм… не совсем понял вопрос, моделью в MVC называется модель :)
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          модель хранит данные, это состояние системы на конкретный момент времени (если можно так выразиться)
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              управляет моделью?
                0
                обрабатывает данные модели и готовит их к представлению, обрабатывает пользовательские действия
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Контроллер
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        Нет. Никто ниче не путает. Может сформулировать не может это другое дело. Ведь маппинг может отражать таблицу как обьект а не только сущность в таблице как обьект. Вы осветили лишь часть орм. Так что ORM может иметь полное права отвечать за модель в целом
                        И касательно интеграла… это явно не модель, параметры от пользователя не должно там быть. Это либо стороняя библиотека некий хелпер либо контролер с применением этой либы. Например в doctrine есть такое понятие как template вот если уже интеграл вычиялть на стороне модели это как раз он и будет. Но к сущности он не имеет никакого отношения.
                          0
                          У меня такое ощущение, что вы занимаетесь троллингом.
                          Я уверен _в том_, что получение данных и вычисления на их основе _должен_ выполнять контроллер, а не модель, хотя в ней, разумеется, это тоже возможно.
                            0
                            Раньше ломал на этот счет голову. Ясность пришла только после прочтения «архитектуры корпаративных приложений» Фаулера.
                            Если вкратце, то есть 2 подхода:
                            1) Flat Controller, это когда модель отвечает только за сохранение, а бизнес в контроллере.
                            2) Thin Controller, бизнес логика в модели.

                            В рельсах принят 1-й способ, но второй более правильный. Фактически в рельсах упразнили слой сервисов, что и стало причиной «толстых» контроллеров.
                            MVC это общая концепция отделения представления от БЛ. Совсем не обязательно наличие одноименнх классов. Мне вот больше ближе «трехзвенка» (бд->сервисы->контроллер<->ui), в этой схеме ui не имеет прямого доступа к БД.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                теперь давайте вернемся к началу :) чем вызван вопрос? ответ «модель — это бизнес-логика» можно было написать сразу, но знание формулировок еще не означает понимание. что-то не так в статье?
                      0
                      в идеале
                      make_resourceful do
                      build :all
                      end
              0
              Почуствовует?
                0
                спасибо, посмеялся )))
                0
                Полезно, спасибо.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое