This post was published more than 4 years ago. It's likely the contents are obsolete or nostolgic. If you think the contents are worth updating, open an issue. Otherwise, sit back and enjoy the stroll down memory lane.
A Peek Behind ActiveModel::Model
I find myself using ActiveModel::Model
quite a bit.
It’s a quick and easy way to supercharge your ruby objects with all kinds of functionality to make them compatible with ActionView
(forms) and ActionPack
(routing).
Aside from this compatibility, it makes your ruby object feel a bit more like an ActiveRecord
object.
In fact, much of ActiveModel was extracted from ActiveRecord during the Rails 3 refactors of old.
I often reach for it when making form objects using The Command Pattern.
class CancelSubscription
include ActiveModel::Model
validates :subscription, :presence => true
validates :cancellation_date, :presence => true
attr_accessor :subscription, :cancellation_date
def execute
# logic for canceling a subscription
end
end
form = CancelSubscription.new(
subscription: subscription,
cancellation_date: cancellation_date
)
form.valid? # => true / false
form.execute # => subscription cancelled!
Let’s take a closer look at ActiveModel::Model
to see what it’s doing for us behind the scenes.
It implements the following methods:
initialize
persisted?
It includes these modules:
ActiveModel::AttributeAssignment
ActiveModel::Validations
ActiveModel::Conversion
It is extended with these modules:
ActiveModel::Naming
ActiveModel::Translation
Each of these modules serves a purpose, but can also be used in isolation.
ActiveModel::AttributeAssignment
This module is included for the sole purpose of making this work:
form = CancelSubscription.new
form.assign_attributes(subscription: subscription, cancellation_date: cancellation_date)
form.subscription # => subscription
form.cancellation_date # => cancellation_date
The ActiveModel::Model#initialize
method uses this functionality to implement the ActiveRecord
-like constructor interface:
form = CancelSubscription.new(
subscription: subscription,
cancellation_date: cancellation_date
)
It iterates over the hash of parameters passed into the constructor, assigning values using the setters you’re expected to have defined via attr_accessor
.
If a setter hasn’t been defined, it will raise an UnknownAttributeError
.
ActiveModel::Validation
Validations define a rich class-level DSL for expressing what your model considers “valid”.
As a result, instances of the model gain a handful of methods allowing you to inquire about the validity (e.g model.valid?
).
Once model.valid?
has been called, it populates model.errors
with error messages indicating that your model has attribute values that the classes’ validators consider invalid.
The model.errors
method returns an instance of ActiveModel::Error
, which is a hash-like object for accessing attribute errors and messages.
This is extremely useful, especially when constructing models from user-provided data or data from programmer provided configuration.
form = CancelSubscription.new
form.valid? # => false
form.errors.full_messages # => ["subscription can't be blank", "cancellation_date can't be blank"]
Validations
comes with a whole sweet of built-in validations - some of which you’ve probably used with ActiveRecord
.
It also provides an easy way to define ad hoc validations via validate
as well as a full-featured Validator
interface for more re-usable validations.
ActiveModel::Translation
As you may have guessed, this module is the glue between your model and the Rails i18n internationalization framework.
This module exposes a class level interface for defining translations that correspond to your attributes a la:
CancelSubscription.human_attribute_name(:cancellation_date)
# => Fecha de cancelación
Also, it allows you to define an i18n_scope
method to control the expected i18n key path for your model’s translations.
ActiveModel::Naming
Extending this module gives you a class method called model_name
that returns an instance of ActiveModel::Name
.
Also, instances of your class will delegate the model_name
to your class.
ActiveModel::Name
implements much of what powers Rails’ “convention over configuration” by taking the name of your class and hooking it up with ActiveSupport::Inflector
.
Understanding how this module works really pulls the curtain back from the infamous “Rails’ Magic”.
ActiveModel::Conversion
Continuing the “Rails Magic” misnomer, Conversion
is another common source of confusion.
It does the job of extracting data from your model to further inform rails conventions such as:
- Finding partial paths via
to_partial_path
- Constructing URLs via
to_param
Worth noting that by default to_param
will return nil
unless your model is persisted?
.
However, you can certainly implement it yourself to your liking.
Putting It All Together
Hopefully, now you see that ActiveModel::Model
is nothing more than a series of modules meant to make your Ruby objects work more seamlessly with the Rails framework.
Once you realize what they do, it’s pretty easy to customize them or omit them entirely to suit your needs.
For example, a few things that bother me about ActiveModel::Model
:
- I like to utilize keword args to fail fast.
- I dislike having to call super if I need to modify the constructor.
- I don’t like that calling code has access to setters from
attr_accessor
.
module FormModel
extend ActiveSupport::Concern
include ActiveModel::Validations
include ActiveModel::Conversion
included do
extend ActiveModel::Naming
extend ActiveModel::Translation
end
def persisted?
false
end
end
Now I can do something like this, opting out of the AttributeAssignment
behavior I don’t care for:
class CancelSubscription
include FormModel
validates :subscription, :presence => true
validates :cancellation_date, :presence => true
attr_reader :subscription, :cancellation_date
def initialize(subscription:, cancellation_date: Time.zone.today)
@subscription = subscription
@cancellation_date = cancellation_date
end
def execute
# logic for canceling a subscription
end
end
See a mistake? Kindly let me know by filing an issue