Rails: Implement the Policy object
- Tram Ho
This article requires you to have knowledge of object-oriented, Ruby and Ruby on Rails.
What is the policy object design pattern?
Policy objects encapsulate and represent a single business rule.
In our applications we can have different business rules coded mainly as if – else or switch statement. These rules represent concepts in your domain, for example:
- Is “a customer eligible for a discount?”
- Will the “email be sent or not?”
- Does “player get points or not?”
Source: Bài viết này
Let’s get started! (The author uses the Rails API as an example but this article can also be implemented in normal Rails)
1. Make a problem
Suppose we have the following controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <span class="token comment"># app/controllers/discounts_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">DiscountsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> <span class="token keyword">if</span> can_user_get_discount<span class="token operator">?</span> code <span class="token operator">=</span> <span class="token constant">GenerateDiscountVoucherCode</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span>call render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"OK"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"Your Discount Voucher Code: <span class="token interpolation"><span class="token delimiter tag">#{</span>code<span class="token delimiter tag">}</span></span>"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">201</span> <span class="token keyword">else</span> render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"Failed"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"You are not allowed to get Discount Voucher"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">422</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">can_user_get_discount</span></span><span class="token operator">?</span> is_premium<span class="token operator">?</span> <span class="token operator">&&</span> last_discount_more_than_10_days_ago<span class="token operator">?</span> <span class="token operator">&&</span> high_buyer<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">is_premium</span></span><span class="token operator">?</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">last_discount_more_than_10_days_ago</span></span><span class="token operator">?</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>last_discount_sent_at <span class="token operator"><</span> ten_days_ago <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">ten_days_ago</span></span> <span class="token builtin">Time</span><span class="token punctuation">.</span>now <span class="token operator">-</span> <span class="token number">10.</span>days <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">high_buyer</span></span><span class="token operator">?</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>total_purchase_this_month <span class="token operator">></span> <span class="token number">5</span>_000 <span class="token keyword">end</span> <span class="token keyword">end</span> |
You put all the policy logic (business rules) in your controller. This is not a good solution. Especially in case you have to deal with a lot of complex logic (controller should only use it for navigation, avoid dealing with a lot of complex logic).
But, we can ignore the class GenerateDiscountVoucherCode
. We just need to know that this class is responsible for generating discount voucher codes.
2. Move the Policy logic into the model
Well, we can move the policy logic into the model. So it turns like this:
app/controllers/discounts_controller.rb
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">DiscountsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> <span class="token keyword">if</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>can_get_discount<span class="token operator">?</span> code <span class="token operator">=</span> <span class="token constant">GenerateDiscountVoucherCode</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span>call render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"OK"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"Your Discount Voucher Code: <span class="token interpolation"><span class="token delimiter tag">#{</span>code<span class="token delimiter tag">}</span></span>"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">201</span> <span class="token keyword">else</span> render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"Failed"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"You are not allowed to get Discount Voucher"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">422</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
app/models/user.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> enum membership<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'regular'</span><span class="token punctuation">,</span> <span class="token string">'premium'</span><span class="token punctuation">]</span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token operator">=</span> <span class="token number">5</span>_000 <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">can_get_discount</span></span><span class="token operator">?</span> <span class="token keyword">self</span><span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token operator">&&</span> <span class="token keyword">self</span><span class="token punctuation">.</span>last_discount_more_than_10_days_ago<span class="token operator">?</span> <span class="token operator">&&</span> <span class="token keyword">self</span><span class="token punctuation">.</span>high_buyer<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">last_discount_more_than_10_days_ago</span></span><span class="token operator">?</span> <span class="token keyword">self</span><span class="token punctuation">.</span>last_discount_sent_at <span class="token operator"><</span> ten_days_ago <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">ten_days_ago</span></span> <span class="token builtin">Time</span><span class="token punctuation">.</span>now <span class="token operator">-</span> <span class="token number">10.</span>days <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">high_buyer</span></span><span class="token operator">?</span> <span class="token keyword">self</span><span class="token punctuation">.</span>total_purchase_this_month <span class="token operator">></span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
But this is still not the optimal solution. Adding a policy object logic, although it helps to optimize the controller, but inflates the model. Now let’s implement the policy object design pattern!
3. Create a separate layer
Now, we will create a separate class containing the following policy logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <span class="token comment"># app/lib/discount_voucher_policy.rb</span> <span class="token keyword">class</span> <span class="token class-name">DiscountVoucherPolicy</span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token operator">=</span> <span class="token number">5</span>_000 <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>user<span class="token punctuation">)</span> <span class="token variable">@user</span> <span class="token operator">=</span> user <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">allowed</span></span><span class="token operator">?</span> is_premium<span class="token operator">?</span> <span class="token operator">&&</span> last_discount_more_than_10_days_ago<span class="token operator">?</span> <span class="token operator">&&</span> high_buyer<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">is_premium</span></span><span class="token operator">?</span> <span class="token variable">@user</span><span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">last_discount_more_than_10_days_ago</span></span><span class="token operator">?</span> <span class="token variable">@user</span><span class="token punctuation">.</span>last_discount_sent_at <span class="token operator"><</span> ten_days_ago <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">ten_days_ago</span></span> <span class="token builtin">Time</span><span class="token punctuation">.</span>now <span class="token operator">-</span> <span class="token number">10.</span>days <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">high_buyer</span></span><span class="token operator">?</span> <span class="token variable">@user</span><span class="token punctuation">.</span>total_purchase_this_month <span class="token operator">></span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Thus, the controller and model will look like this:
app/controllers/discounts_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">class</span> <span class="token class-name">DiscountsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> <span class="token keyword">if</span> policy<span class="token punctuation">.</span>allowed<span class="token operator">?</span> code <span class="token operator">=</span> <span class="token constant">GenerateDiscountVoucherCode</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span>call render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"OK"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"Your Discount Voucher Code: <span class="token interpolation"><span class="token delimiter tag">#{</span>code<span class="token delimiter tag">}</span></span>"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">201</span> <span class="token keyword">else</span> render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"Failed"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"You are not allowed to get Discount Voucher"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">422</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">policy</span></span> <span class="token constant">DiscountVoucherPolicy</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
app/models/user.rb
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> enum membership<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'regular'</span><span class="token punctuation">,</span> <span class="token string">'premium'</span><span class="token punctuation">]</span> <span class="token keyword">end</span> |
4. Summary
Compared to our original controller, this is “just” putting the policy logic in a separate class. That’s true, since our main purpose is to move the policy logic out of the controller and model.
Policy objects encapsulate and represent a single business rule.
You will find this design pattern useful in cases where you have complex policy logic. The example that the author gives is a simple policy logic.
References
The above article is translated from the source Rails: Policy Objects Implementation
Thank you for reading, have a good day