rails-patterns 0.4.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: af39ea7538e043642610c2ea2ff35a9fa5c8a0e7
4
- data.tar.gz: ff3231a21b94a92280acf664497a40b3039f7140
2
+ SHA256:
3
+ metadata.gz: 738d61388a1488705037348a76d42d69b4b92a83cdd6753a2d14d061768302a1
4
+ data.tar.gz: 9dcc97a76fa682b76ff8910e4146da62eff304c3a7f511343e31d7b8ed222c36
5
5
  SHA512:
6
- metadata.gz: c3653e8ce337a6225ad14162600f8b2a462321fcca8595f8f1463920935749e17b48d3f7359530dff528bc7ff3f7175ddaa23b7ac1203903ff42b296bfb8c51d
7
- data.tar.gz: 37f4e6cb0fc581140dfee9372386b9d37b77a092d63d798fb98d8617475ac249d2e8f49aa9d8092ae5630e3de5e48a0aa6677ed7d62e89429a788c9fb51e44f2
6
+ metadata.gz: 9961fcdafa85dca7ef1a91641c4867fa348cdeb20a30abda8e8a91ab3a33c9d3ae456d57a2761615c127bf656b793c8bda94e7cc21371a42bd413e57336babfc
7
+ data.tar.gz: 7a570f8a7836ff4a3404525d41323697850c46c4dae447512ab517acf036799afd7c14a57028f374a5cea8aa8b489d134e32a3bd8278563039242d33546a8f0a
@@ -0,0 +1,33 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ ruby: [ '2.5', '2.6', '2.7' ]
23
+ name: RSpec for Ruby version ${{ matrix.ruby }}
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - uses: supercharge/redis-github-action@1.1.0
27
+ - name: Set up Ruby
28
+ uses: actions/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ - run: gem install bundler
32
+ - run: bundle install
33
+ - run: bundle exec rspec
data/Gemfile CHANGED
@@ -3,16 +3,19 @@ source "https://rubygems.org"
3
3
  gem "activerecord", ">= 4.2.6"
4
4
  gem "actionpack", ">= 4.2.6"
5
5
  gem "virtus"
6
+ gem "ruby2_keywords"
6
7
 
7
8
  # Add dependencies to develop your gem here.
8
9
  # Include everything needed to run rake, tests, features, etc.
9
10
 
10
11
  group :development do
11
12
  gem "rspec"
12
- gem "bundler", "~> 1.0"
13
+ gem "bundler", "~> 2.0"
13
14
  gem "juwelier", "~> 2.1.0"
14
15
  end
15
16
 
16
17
  group "test" do
17
18
  gem "pry-rails"
19
+ gem "rspec_junit_formatter"
20
+ gem "redis"
18
21
  end
@@ -1,59 +1,61 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- actionpack (5.0.2)
5
- actionview (= 5.0.2)
6
- activesupport (= 5.0.2)
7
- rack (~> 2.0)
8
- rack-test (~> 0.6.3)
4
+ actionpack (6.0.3.2)
5
+ actionview (= 6.0.3.2)
6
+ activesupport (= 6.0.3.2)
7
+ rack (~> 2.0, >= 2.0.8)
8
+ rack-test (>= 0.6.3)
9
9
  rails-dom-testing (~> 2.0)
10
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
11
- actionview (5.0.2)
12
- activesupport (= 5.0.2)
10
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
11
+ actionview (6.0.3.2)
12
+ activesupport (= 6.0.3.2)
13
13
  builder (~> 3.1)
14
- erubis (~> 2.7.0)
14
+ erubi (~> 1.4)
15
15
  rails-dom-testing (~> 2.0)
16
- rails-html-sanitizer (~> 1.0, >= 1.0.3)
17
- activemodel (5.0.2)
18
- activesupport (= 5.0.2)
19
- activerecord (5.0.2)
20
- activemodel (= 5.0.2)
21
- activesupport (= 5.0.2)
22
- arel (~> 7.0)
23
- activesupport (5.0.2)
16
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
17
+ activemodel (6.0.3.2)
18
+ activesupport (= 6.0.3.2)
19
+ activerecord (6.0.3.2)
20
+ activemodel (= 6.0.3.2)
21
+ activesupport (= 6.0.3.2)
22
+ activesupport (6.0.3.2)
24
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
25
- i18n (~> 0.7)
24
+ i18n (>= 0.7, < 2)
26
25
  minitest (~> 5.1)
27
26
  tzinfo (~> 1.1)
28
- addressable (2.4.0)
29
- arel (7.1.4)
27
+ zeitwerk (~> 2.2, >= 2.2.2)
28
+ addressable (2.7.0)
29
+ public_suffix (>= 2.0.2, < 5.0)
30
30
  axiom-types (0.1.1)
31
31
  descendants_tracker (~> 0.0.4)
32
32
  ice_nine (~> 0.11.0)
33
33
  thread_safe (~> 0.3, >= 0.3.1)
34
- builder (3.2.3)
35
- coderay (1.1.1)
34
+ builder (3.2.4)
35
+ coderay (1.1.2)
36
36
  coercible (1.0.0)
37
37
  descendants_tracker (~> 0.0.1)
38
- concurrent-ruby (1.0.5)
38
+ concurrent-ruby (1.1.6)
39
+ crass (1.0.6)
39
40
  descendants_tracker (0.0.4)
40
41
  thread_safe (~> 0.3, >= 0.3.1)
41
42
  diff-lcs (1.3)
42
43
  equalizer (0.0.11)
43
- erubis (2.7.0)
44
- faraday (0.9.2)
44
+ erubi (1.9.0)
45
+ faraday (0.17.3)
45
46
  multipart-post (>= 1.2, < 3)
46
- git (1.3.0)
47
- github_api (0.16.0)
48
- addressable (~> 2.4.0)
47
+ git (1.7.0)
48
+ rchardet (~> 1.8)
49
+ github_api (0.18.2)
50
+ addressable (~> 2.4)
49
51
  descendants_tracker (~> 0.0.4)
50
- faraday (~> 0.8, < 0.10)
51
- hashie (>= 3.4)
52
- mime-types (>= 1.16, < 3.0)
52
+ faraday (~> 0.8)
53
+ hashie (~> 3.5, >= 3.5.2)
53
54
  oauth2 (~> 1.0)
54
- hashie (3.5.5)
55
- highline (1.7.8)
56
- i18n (0.8.1)
55
+ hashie (3.6.0)
56
+ highline (2.0.3)
57
+ i18n (1.8.3)
58
+ concurrent-ruby (~> 1.0)
57
59
  ice_nine (0.11.2)
58
60
  juwelier (2.1.3)
59
61
  builder
@@ -65,63 +67,68 @@ GEM
65
67
  rake
66
68
  rdoc
67
69
  semver
68
- jwt (1.5.6)
69
- loofah (2.0.3)
70
+ jwt (2.2.1)
71
+ loofah (2.6.0)
72
+ crass (~> 1.0.2)
70
73
  nokogiri (>= 1.5.9)
71
- method_source (0.8.2)
72
- mime-types (2.99.3)
73
- mini_portile2 (2.1.0)
74
- minitest (5.10.1)
75
- multi_json (1.12.1)
74
+ method_source (0.9.0)
75
+ mini_portile2 (2.4.0)
76
+ minitest (5.14.1)
77
+ multi_json (1.14.1)
76
78
  multi_xml (0.6.0)
77
- multipart-post (2.0.0)
78
- nokogiri (1.7.1)
79
- mini_portile2 (~> 2.1.0)
80
- oauth2 (1.3.1)
81
- faraday (>= 0.8, < 0.12)
82
- jwt (~> 1.0)
79
+ multipart-post (2.1.1)
80
+ nokogiri (1.10.9)
81
+ mini_portile2 (~> 2.4.0)
82
+ oauth2 (1.4.4)
83
+ faraday (>= 0.8, < 2.0)
84
+ jwt (>= 1.0, < 3.0)
83
85
  multi_json (~> 1.3)
84
86
  multi_xml (~> 0.5)
85
87
  rack (>= 1.2, < 3)
86
- pry (0.10.4)
88
+ pry (0.11.3)
87
89
  coderay (~> 1.1.0)
88
- method_source (~> 0.8.1)
89
- slop (~> 3.4)
90
+ method_source (~> 0.9.0)
90
91
  pry-rails (0.3.6)
91
92
  pry (>= 0.10.4)
92
- rack (2.0.1)
93
- rack-test (0.6.3)
94
- rack (>= 1.0)
95
- rails-dom-testing (2.0.2)
96
- activesupport (>= 4.2.0, < 6.0)
97
- nokogiri (~> 1.6)
98
- rails-html-sanitizer (1.0.3)
99
- loofah (~> 2.0)
100
- rake (12.0.0)
101
- rdoc (5.1.0)
102
- rspec (3.5.0)
103
- rspec-core (~> 3.5.0)
104
- rspec-expectations (~> 3.5.0)
105
- rspec-mocks (~> 3.5.0)
106
- rspec-core (3.5.4)
107
- rspec-support (~> 3.5.0)
108
- rspec-expectations (3.5.0)
93
+ public_suffix (4.0.5)
94
+ rack (2.2.3)
95
+ rack-test (1.1.0)
96
+ rack (>= 1.0, < 3)
97
+ rails-dom-testing (2.0.3)
98
+ activesupport (>= 4.2.0)
99
+ nokogiri (>= 1.6)
100
+ rails-html-sanitizer (1.3.0)
101
+ loofah (~> 2.3)
102
+ rake (13.0.1)
103
+ rchardet (1.8.0)
104
+ rdoc (6.2.1)
105
+ redis (4.1.4)
106
+ rspec (3.7.0)
107
+ rspec-core (~> 3.7.0)
108
+ rspec-expectations (~> 3.7.0)
109
+ rspec-mocks (~> 3.7.0)
110
+ rspec-core (3.7.1)
111
+ rspec-support (~> 3.7.0)
112
+ rspec-expectations (3.7.0)
109
113
  diff-lcs (>= 1.2.0, < 2.0)
110
- rspec-support (~> 3.5.0)
111
- rspec-mocks (3.5.0)
114
+ rspec-support (~> 3.7.0)
115
+ rspec-mocks (3.7.0)
112
116
  diff-lcs (>= 1.2.0, < 2.0)
113
- rspec-support (~> 3.5.0)
114
- rspec-support (3.5.0)
117
+ rspec-support (~> 3.7.0)
118
+ rspec-support (3.7.1)
119
+ rspec_junit_formatter (0.3.0)
120
+ rspec-core (>= 2, < 4, != 2.12.0)
121
+ ruby2_keywords (0.0.2)
115
122
  semver (1.0.1)
116
- slop (3.6.0)
117
123
  thread_safe (0.3.6)
118
- tzinfo (1.2.3)
124
+ tzinfo (1.2.7)
119
125
  thread_safe (~> 0.1)
120
126
  virtus (1.0.5)
121
127
  axiom-types (~> 0.1)
122
128
  coercible (~> 1.0)
123
129
  descendants_tracker (~> 0.0, >= 0.0.3)
124
130
  equalizer (~> 0.0, >= 0.0.9)
131
+ zeitwerk (2.3.0)
125
132
 
126
133
  PLATFORMS
127
134
  ruby
@@ -129,11 +136,14 @@ PLATFORMS
129
136
  DEPENDENCIES
130
137
  actionpack (>= 4.2.6)
131
138
  activerecord (>= 4.2.6)
132
- bundler (~> 1.0)
139
+ bundler (~> 2.0)
133
140
  juwelier (~> 2.1.0)
134
141
  pry-rails
142
+ redis
135
143
  rspec
144
+ rspec_junit_formatter
145
+ ruby2_keywords
136
146
  virtus
137
147
 
138
148
  BUNDLED WITH
139
- 1.14.6
149
+ 2.1.4
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
+ ![](https://github.com/Selleo/pattern/workflows/Ruby/badge.svg)
2
+
1
3
  # Pattern
2
4
 
3
- A collection of lightweight, standardized, rails-oriented patterns.
5
+ A collection of lightweight, standardized, rails-oriented patterns used by [RubyOnRails Developers @ Selleo](https://selleo.com/ruby-on-rails)
4
6
 
5
7
  - [Query - complex querying on active record relation](#query)
6
8
  - [Service - useful for handling processes involving multiple steps](#service)
7
- - [Collection - when in need to add a method that relates to the collection a whole](#collection)
9
+ - [Collection - when in need to add a method that relates to the collection as whole](#collection)
8
10
  - [Form - when you need a place for callbacks, want to replace strong parameters or handle virtual/composite resources](#form)
11
+ - [Calculation - when you need a place for calculating a simple value (numeric, array, hash) and/or cache it](#calculation)
12
+ - [Rule and Ruleset - when you need a place for conditional logic](#rule-and-ruleset)
9
13
 
10
14
  ## Installation
11
15
 
@@ -27,6 +31,7 @@ One should consider using query objects pattern when in need to perform complex
27
31
  Usually one should avoid using scopes for such purpose.
28
32
  As a rule of thumb, if scope interacts with more than one column and/or joins in other tables, it should be moved to query object.
29
33
  Also whenever a chain of scopes is to be used, one should consider using query object too.
34
+ Some more information on using query objects can be found in [this article](https://medium.com/@blazejkosmowski/essential-rubyonrails-patterns-part-2-query-objects-4b253f4f4539).
30
35
 
31
36
  ### Assumptions and rules
32
37
 
@@ -73,7 +78,7 @@ RecentlyActivatedUsersQuery.call(date_range: Date.today.beginning_of_day..Date.t
73
78
  RecentlyActivatedUsersQuery.call(User.without_test_users, date_range: Date.today.beginning_of_day..Date.today.end_of_day)
74
79
 
75
80
  class User < ApplicationRecord
76
- scope :recenty_activated, RecentlyActivatedUsersQuery
81
+ scope :recently_activated, RecentlyActivatedUsersQuery
77
82
  end
78
83
  ```
79
84
 
@@ -82,7 +87,7 @@ end
82
87
  ### When to use it
83
88
 
84
89
  Service objects are commonly used to mitigate problems with model callbacks that interact with external classes ([read more...](http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/)).
85
- Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring.
90
+ Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring. In many cases service object can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using services can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-part-1-service-objects-1af9f9573ca1).
86
91
 
87
92
  ### Assumptions and rules
88
93
 
@@ -93,6 +98,10 @@ Service objects are also useful for handling processes involving multiple steps.
93
98
  * It is recommended for `#call` method to be the only public method of service object (besides state readers)
94
99
  * It is recommended to name service object classes after commands (e.g. `ActivateUser` instead of `UserActivation`)
95
100
 
101
+ ### Other
102
+
103
+ A bit higher level of abstraction is provided by [business_process gem](https://github.com/Selleo/business_process).
104
+
96
105
  ### Examples
97
106
 
98
107
  #### Declaration
@@ -162,7 +171,7 @@ class CustomerEventsByTypeCollection < Patterns::Collection
162
171
  subject.
163
172
  events.
164
173
  group_by(&:type).
165
- transform_values{ |event| event.public_send(options.fetch(:label_method, "description")) }
174
+ transform_values{ |events| events.map{ |e| e.public_send(options.fetch(:label_method, "description")) }}
166
175
  end
167
176
  end
168
177
  ```
@@ -183,6 +192,7 @@ Form objects, just like service objects, are commonly used to mitigate problems
183
192
  Form objects can also be used as replacement for `ActionController::StrongParameters` strategy, as all writable attributes are re-defined within each form.
184
193
  Finally form objects can be used as wrappers for virtual (with no model representation) or composite (saving multiple models at once) resources.
185
194
  In the latter case this may act as replacement for `ActiveRecord::NestedAttributes`.
195
+ In some cases FormObject can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using form objects can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-form-objects-b199aada6ec9).
186
196
 
187
197
  ### Assumptions and rules
188
198
 
@@ -191,7 +201,7 @@ In the latter case this may act as replacement for `ActiveRecord::NestedAttribut
191
201
  * Forms can be initialized using `.new`.
192
202
  * Forms accept optional resource object as first constructor argument.
193
203
  * Forms accept optional attributes hash as latter constructor argument.
194
- * forms have to implement `#persist` method that returns falsey (if failed) or truthy (if succeeded) value.
204
+ * Forms have to implement `#persist` method that returns falsey (if failed) or truthy (if succeeded) value.
195
205
  * Forms provide access to first constructor argument using `#resource`.
196
206
  * Forms are saved using their `#save` or `#save!` methods.
197
207
  * Forms will attempt to pre-populate their fields using `resource#attributes` and public getters for `resource`
@@ -269,6 +279,154 @@ ReportConfigurationForm.new
269
279
  ReportConfigurationForm.new({ include_extra_data: true, dump_as_csv: true })
270
280
  ```
271
281
 
282
+ ## Calculation
283
+
284
+ ### When to use it
285
+
286
+ Calculation objects provide a place to calculate simple values (i.e. numeric, arrays, hashes), especially when calculations require interacting with multiple classes, and thus do not fit into any particular one.
287
+ Calculation objects also provide simple abstraction for caching their results.
288
+
289
+ ### Assumptions and rules
290
+
291
+ * Calculations have to implement `#result` method that returns any value (result of calculation).
292
+ * Calculations do provide `.set_cache_expiry_every` method, that allows defining caching period.
293
+ * When `.set_cache_expiry_every` is not used, result is not being cached.
294
+ * Calculations return result by calling any of following methods: `.calculate`, `.result_for` or `.result`.
295
+ * First argument passed to calculation is accessible by `#subject` private method.
296
+ * Arguments hash passed to calculation is accessible by `#options` private method.
297
+ * Caching takes into account arguments passed when building cache key.
298
+ * To build cache key, `#cache_key` of each argument value is used if possible.
299
+ * By default `Rails.cache` is used as cache store.
300
+
301
+ ### Examples
302
+
303
+ #### Declaration
304
+
305
+ ```ruby
306
+ class AverageHotelDailyRevenue < Patterns::Calculation
307
+ set_cache_expiry_every 1.day
308
+
309
+ private
310
+
311
+ def result
312
+ reservations.sum(:price) / days_in_year
313
+ end
314
+
315
+ def reservations
316
+ Reservation.where(
317
+ date: (beginning_of_year..end_of_year),
318
+ hotel_id: subject.id
319
+ )
320
+ end
321
+
322
+ def days_in_year
323
+ end_of_year.yday
324
+ end
325
+
326
+ def year
327
+ options.fetch(:year, Date.current.year)
328
+ end
329
+
330
+ def beginning_of_year
331
+ Date.new(year).beginning_of_year
332
+ end
333
+
334
+ def end_of_year
335
+ Date.new(year).end_of_year
336
+ end
337
+ end
338
+ ```
339
+
340
+ #### Usage
341
+
342
+ ```ruby
343
+ hotel = Hotel.find(123)
344
+ AverageHotelDailyRevenue.result_for(hotel)
345
+ AverageHotelDailyRevenue.result_for(hotel, year: 2015)
346
+
347
+ TotalCurrentRevenue.calculate
348
+ AverageDailyRevenue.result
349
+ ```
350
+
351
+ ## Rule and Ruleset
352
+
353
+ ### When to use it
354
+
355
+ Rule objects provide a place for dislocating/extracting conditional logic.
356
+
357
+ Use it when:
358
+ - given complex condition is duplicated in multiple places in your codebase
359
+ - part of condition logic can be reused in some other place
360
+ - there is a need to instantiate condition itself for some reason (i.e. to represent it in the interface)
361
+ - responsibility of your class is blurred by complex conditional logic, and as a result...
362
+ - ...tests for your class require multiple condition branches / nested contexts
363
+
364
+ ### Assumptions and rules
365
+
366
+ * Rule has `#satisfied?`, `#applicable?`, `#not_applicable?` and `#forceable?` methods available.
367
+ * Rule has to implement at least `#satisfied?` method. `#not_applicable?` and `#forceable?` are meant to be overridable.
368
+ * `#forceable?` makes sense in scenario where condition is capable of being force-satisfied regardless if its actually satisfied or not. Is `true` by default.
369
+ * Override `#not_applicable?` when method is applicable only under some specific conditions. Is `false` by default.
370
+ * Rule requires a subject as first argument.
371
+ * Multiple rules and rulesets can be combined into new ruleset as both share same interface and can be used interchangeably (composite pattern).
372
+
373
+ #### Forcing rules
374
+
375
+ On some occasions there is a situation in which some condition should be overridable.
376
+ Let's say we may want send shipping notification even though given order was not paid for and under regular circumstances such notification should not be sent.
377
+ In this case, while regular logic with some automated process would not trigger delivery, an action triggered by user from UI could do it, by passing `force: true` option to `#satisified?` methods.
378
+
379
+ It might be good idea to test for `#forceable?` on the UI level to control visibility of such link/button.
380
+
381
+ Overriding `#forceable` can be useful to prevent some edge cases, i.e. `ContactInformationProvidedRule` might check if customer for given order has provided any contact means by which a notification could be delivered.
382
+ If not, ruleset containing such rule (and the rule itself) would not be "forceable" and UI could reflect that by querying `#forceable?`.
383
+
384
+ #### Regular and strong rulesets
385
+
386
+ While regular `Ruleset` can be satisfied or forced if any of its rules in not applicable, the
387
+ `StrongRuleset` is not satisfied and not "forceable" if any of its rules is not applicable.
388
+
389
+ #### `#not_applicable?` vs `#applicable?`
390
+
391
+ It might be surprising that is is the negated version of the `#applicable?` predicate methods that is overridable.
392
+ However, from the actual usage perspective, it usually easier to conceptually define when condition makes no sense than other way around.
393
+
394
+ ### Examples
395
+
396
+ #### Declaration
397
+
398
+ ```ruby
399
+ class OrderIsSentRule < Rule
400
+ def satisfied?
401
+ subject.sent?
402
+ end
403
+ end
404
+
405
+ class OrderIsPaidRule < Rule
406
+ def satisfied?
407
+ subject.paid?
408
+ end
409
+
410
+ def forceable?
411
+ true
412
+ end
413
+ end
414
+
415
+ OrderCompletedNotificationRuleset = Class.new(Ruleset)
416
+ OrderCompletedNotificationRuleset.
417
+ add_rule(:order_is_sent_rule).
418
+ add_rule(:order_is_paid_rule)
419
+ ```
420
+
421
+ #### Usage
422
+
423
+ ```ruby
424
+ OrderIsPaidRule.new(order).satisfied?
425
+ OrderCompletedNotificationRuleset.new(order).satisfied?
426
+
427
+ ResendOrderNotification.call(order) if OrderCompletedNotificationRuleset.new(order).satisfied?(force: true)
428
+ ```
429
+
272
430
  ## Further reading
273
431
 
274
432
  * [7 ways to decompose fat active record models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)