Skip to content

Commit 8e33d49

Browse files
committed
finish old active model post
1 parent 38d710b commit 8e33d49

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
layout: post
3+
author: Tony Schneider
4+
title : A Peek Behind ActiveModel::Model
5+
date : 2020-06-24
6+
tags : software
7+
---
8+
9+
I find myself using `ActiveModel::Model` quite a bit.
10+
11+
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).
12+
13+
Aside from this compatibility, it makes your ruby object _feel_ a bit more like an `ActiveRecord` object.
14+
In fact, much of ActiveModel was extracted from ActiveRecord during the Rails 3 refactors of old.
15+
16+
I often reach for it when making form objects using [The Command Pattern]({% post_url 2020-06-24-the-command-pattern %}).
17+
18+
```ruby
19+
20+
class CancelSubscription
21+
include ActiveModel::Model
22+
23+
validates :subscription, :presence => true
24+
validates :cancellation_date, :presence => true
25+
26+
attr_accessor :subscription, :cancellation_date
27+
28+
def execute
29+
# logic for canceling a subscription
30+
end
31+
end
32+
33+
form = CancelSubscription.new(
34+
subscription: subscription,
35+
cancellation_date: cancellation_date
36+
)
37+
form.valid? # => true / false
38+
form.execute # => subscription cancelled!
39+
```
40+
41+
Let’s take a closer look at `ActiveModel::Model` to see what it’s doing for us behind the scenes.
42+
43+
It implements the following methods:
44+
45+
* `initialize`
46+
* `persisted?`
47+
48+
It includes these modules:
49+
50+
* `ActiveModel::AttributeAssignment`
51+
* `ActiveModel::Validations`
52+
* `ActiveModel::Conversion`
53+
54+
It is extended with these modules:
55+
56+
* `ActiveModel::Naming`
57+
* `ActiveModel::Translation`
58+
59+
Each of these modules serves a purpose, but can also be used in isolation.
60+
61+
### ActiveModel::AttributeAssignment
62+
63+
This module is included for the sole purpose of making this work:
64+
65+
```ruby
66+
form = CancelSubscription.new
67+
form.assign_attributes(subscription: subscription, cancellation_date: cancellation_date)
68+
form.subscription # => subscription
69+
form.cancellation_date # => cancellation_date
70+
```
71+
72+
The `ActiveModel::Model#initialize` method uses this functionality to implement the `ActiveRecord`-like constructor interface:
73+
74+
```ruby
75+
form = CancelSubscription.new(
76+
subscription: subscription,
77+
cancellation_date: cancellation_date
78+
)
79+
```
80+
81+
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`.
82+
83+
If a setter hasn’t been defined, it will raise an `UnknownAttributeError`.
84+
85+
### ActiveModel::Validation
86+
87+
Validations define a rich class-level DSL for expressing what your model considers “valid”.
88+
89+
As a result, instances of the model gain a handful of methods allowing you to inquire about the validity (e.g `model.valid?`).
90+
91+
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_.
92+
93+
The `model.errors` method returns an instance of `ActiveModel::Error`, which is a hash-like object for accessing attribute errors and messages.
94+
95+
This is extremely useful, especially when constructing models from user-provided data or data from programmer provided configuration.
96+
97+
```ruby
98+
form = CancelSubscription.new
99+
form.valid? # => false
100+
form.errors.full_messages # => ["subscription can't be blank", "cancellation_date can't be blank"]
101+
```
102+
103+
`Validations` comes with a whole sweet of built-in validations - some of which you've probably used with `ActiveRecord`.
104+
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.
105+
106+
### ActiveModel::Translation
107+
108+
As you may have guessed, this module is the glue between your model and the Rails i18n internationalization framework.
109+
110+
This module exposes a class level interface for defining translations that correspond to your attributes a la:
111+
112+
```ruby
113+
CancelSubscription.human_attribute_name(:cancellation_date)
114+
# => Fecha de cancelación
115+
```
116+
117+
Also, it allows you to define an `i18n_scope` method to control the expected i18n key path for your model's translations.
118+
119+
### ActiveModel::Naming
120+
121+
Extending this module gives you a class method called `model_name` that returns an instance of `ActiveModel::Name`.
122+
Also, instances of your class will delegate the `model_name` to your class.
123+
124+
`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`.
125+
126+
Understanding how this module works really pulls the curtain back from the infamous "Rails' Magic".
127+
128+
### ActiveModel::Conversion
129+
130+
Continuing the "Rails Magic" misnomer, `Conversion` is another common source of confusion.
131+
132+
It does the job of extracting data from your model to further inform rails conventions such as:
133+
134+
* Finding partial paths via `to_partial_path`
135+
* Constructing URLs via `to_param`
136+
137+
Worth noting that by default `to_param` will return `nil` unless your model is `persisted?`.
138+
However, you can certainly implement it yourself to your liking.
139+
140+
## Putting It All Together
141+
142+
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.
143+
144+
Once you realize what they do, it's pretty easy to customize them or omit them entirely to suit your needs.
145+
146+
For example, a few things that bother me about `ActiveModel::Model`:
147+
148+
1. I like to utilize keword args to fail fast.
149+
1. I dislike having to call super if I need to modify the constructor.
150+
1. I don't like that calling code has access to setters from `attr_accessor`.
151+
152+
```ruby
153+
module FormModel
154+
extend ActiveSupport::Concern
155+
156+
include ActiveModel::Validations
157+
include ActiveModel::Conversion
158+
159+
included do
160+
extend ActiveModel::Naming
161+
extend ActiveModel::Translation
162+
end
163+
164+
def persisted?
165+
false
166+
end
167+
end
168+
```
169+
170+
Now I can do something like this, opting out of the `AttributeAssignment` behavior I don't care for:
171+
172+
```ruby
173+
class CancelSubscription
174+
include FormModel
175+
176+
validates :subscription, :presence => true
177+
validates :cancellation_date, :presence => true
178+
179+
attr_reader :subscription, :cancellation_date
180+
181+
def initialize(subscription:, cancellation_date: Time.zone.today)
182+
@subscription = subscription
183+
@cancellation_date = cancellation_date
184+
end
185+
186+
def execute
187+
# logic for canceling a subscription
188+
end
189+
end
190+
```

_site/index.html

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ <h2>
4545
<img class="emoji" title=":computer:" alt=":computer:" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4bb.png" height="20" width="20"> Software</h2>
4646
<ul>
4747
<li>
48+
<span class="post-meta mono">2020-06-24</span>
49+
<a href="/2020/06/24/a-peek-behind-active-model.html">
50+
A Peek Behind ActiveModel::Model
51+
</a>
52+
</li>
53+
<li>
4854
<span class="post-meta mono">2020-06-24</span>
4955
<a href="/2020/06/24/the-command-pattern.html">
5056
The Command Pattern

0 commit comments

Comments
 (0)